From 63bf2b32701b2887533f1051f7e81d95b3b77c51 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Fri, 20 Sep 2013 15:46:10 +0800 Subject: [PATCH 001/268] 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 002/268] 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 003/268] 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 004/268] 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 005/268] 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 006/268] 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 007/268] 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 008/268] 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 009/268] 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 2e61be739fee2e0947fe4d8c0b9f86f8a3262979 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 17 Nov 2013 14:17:01 -0800 Subject: [PATCH 010/268] Don't copy the context menu of ViewBoxes. This allows customization of the context menu of a ViewBox simply by calling viewbox.menu.addAction(...). See issue #13. Also some cleanup. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 19 ++++--------------- .../graphicsItems/ViewBox/ViewBoxMenu.py | 2 +- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 5ab118f7..6e0a20d2 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1054,26 +1054,15 @@ class ViewBox(GraphicsWidget): if ev.button() == QtCore.Qt.RightButton and self.menuEnabled(): ev.accept() self.raiseContextMenu(ev) - + def raiseContextMenu(self, ev): - #print "viewbox.raiseContextMenu called." - - #menu = self.getMenu(ev) menu = self.getMenu(ev) self.scene().addParentContextMenus(self, menu, ev) - #print "2:", [str(a.text()) for a in self.menu.actions()] - pos = ev.screenPos() - #pos2 = ev.scenePos() - #print "3:", [str(a.text()) for a in self.menu.actions()] - #self.sigActionPositionChanged.emit(pos2) + menu.popup(ev.screenPos().toPoint()) - menu.popup(QtCore.QPoint(pos.x(), pos.y())) - #print "4:", [str(a.text()) for a in self.menu.actions()] - def getMenu(self, ev): - self._menuCopy = self.menu.copy() ## temporary storage to prevent menu disappearing - return self._menuCopy - + return self.menu + def getContextMenus(self, event): if self.menuEnabled(): return self.menu.subMenus() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 5242ecdd..b508fd4b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -275,4 +275,4 @@ class ViewBoxMenu(QtGui.QMenu): from .ViewBox import ViewBox - \ No newline at end of file + From 5b905cde8b71c1a940c1ef1214ea356b4c8d9fc4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 19 Nov 2013 07:46:17 -0500 Subject: [PATCH 011/268] Override ViewBox.popup() to update menu before showing Extend ViewBox menu in examples/contextMenu --- examples/contextMenu.py | 142 ++++++++++++++++++ .../graphicsItems/ViewBox/ViewBoxMenu.py | 22 +-- 2 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 examples/contextMenu.py diff --git a/examples/contextMenu.py b/examples/contextMenu.py new file mode 100644 index 00000000..c2c5918d --- /dev/null +++ b/examples/contextMenu.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +""" +Demonstrates adding a custom context menu to a GraphicsItem +and extending the context menu of a ViewBox. + +PyQtGraph implements a system that allows each item in a scene to implement its +own context menu, and for the menus of its parent items to be automatically +displayed as well. + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +win = pg.GraphicsWindow() +win.setWindowTitle('pyqtgraph example: context menu') + + +view = win.addViewBox() + +# add two new actions to the ViewBox context menu: +zoom1 = view.menu.addAction('Zoom to box 1') +zoom2 = view.menu.addAction('Zoom to box 2') + +# define callbacks for these actions +def zoomTo1(): + # note that box1 is defined below + view.autoRange(items=[box1]) +zoom1.triggered.connect(zoomTo1) + +def zoomTo2(): + # note that box1 is defined below + view.autoRange(items=[box2]) +zoom2.triggered.connect(zoomTo2) + + + +class MenuBox(pg.GraphicsObject): + """ + This class draws a rectangular area. Right-clicking inside the area will + raise a custom context menu which also includes the context menus of + its parents. + """ + def __init__(self, name): + self.name = name + self.pen = pg.mkPen('r') + + # menu creation is deferred because it is expensive and often + # the user will never see the menu anyway. + self.menu = None + + # note that the use of super() is often avoided because Qt does not + # allow to inherit from multiple QObject subclasses. + pg.GraphicsObject.__init__(self) + + + # All graphics items must have paint() and boundingRect() defined. + def boundingRect(self): + return QtCore.QRectF(0, 0, 10, 10) + + def paint(self, p, *args): + p.setPen(self.pen) + p.drawRect(self.boundingRect()) + + + # On right-click, raise the context menu + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + if self.raiseContextMenu(ev): + ev.accept() + + def raiseContextMenu(self, ev): + menu = self.getContextMenus() + + # Let the scene add on to the end of our context menu + # (this is optional) + menu = self.scene().addParentContextMenus(self, menu, ev) + + pos = ev.screenPos() + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + return True + + # This method will be called when this item's _children_ want to raise + # a context menu that includes their parents' menus. + def getContextMenus(self, event=None): + if self.menu is None: + self.menu = QtGui.QMenu() + self.menu.setTitle(self.name+ " options..") + + green = QtGui.QAction("Turn green", self.menu) + green.triggered.connect(self.setGreen) + self.menu.addAction(green) + self.menu.green = green + + blue = QtGui.QAction("Turn blue", self.menu) + blue.triggered.connect(self.setBlue) + self.menu.addAction(blue) + self.menu.green = blue + + alpha = QtGui.QWidgetAction(self.menu) + alphaSlider = QtGui.QSlider() + alphaSlider.setOrientation(QtCore.Qt.Horizontal) + alphaSlider.setMaximum(255) + alphaSlider.setValue(255) + alphaSlider.valueChanged.connect(self.setAlpha) + alpha.setDefaultWidget(alphaSlider) + self.menu.addAction(alpha) + self.menu.alpha = alpha + self.menu.alphaSlider = alphaSlider + return self.menu + + # Define context menu callbacks + def setGreen(self): + self.pen = pg.mkPen('g') + # inform Qt that this item must be redrawn. + self.update() + + def setBlue(self): + self.pen = pg.mkPen('b') + self.update() + + def setAlpha(self, a): + self.setOpacity(a/255.) + + +# This box's context menu will include the ViewBox's menu +box1 = MenuBox("Menu Box #1") +view.addItem(box1) + +# This box's context menu will include both the ViewBox's menu and box1's menu +box2 = MenuBox("Menu Box #2") +box2.setParentItem(box1) +box2.setPos(5, 5) +box2.scale(0.2, 0.2) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index b508fd4b..99c3c3fb 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -88,15 +88,15 @@ class ViewBoxMenu(QtGui.QMenu): self.updateState() - def copy(self): - m = QtGui.QMenu() - for sm in self.subMenus(): - if isinstance(sm, QtGui.QMenu): - m.addMenu(sm) - else: - m.addAction(sm) - m.setTitle(self.title()) - return m + #def copy(self): + #m = QtGui.QMenu() + #for sm in self.subMenus(): + #if isinstance(sm, QtGui.QMenu): + #m.addMenu(sm) + #else: + #m.addAction(sm) + #m.setTitle(self.title()) + #return m def subMenus(self): if not self.valid: @@ -159,6 +159,10 @@ class ViewBoxMenu(QtGui.QMenu): self.ctrl[1].invertCheck.setChecked(state['yInverted']) self.valid = True + def popup(self, *args): + if not self.valid: + self.updateState() + QtGui.QMenu.popup(self, *args) def autoRange(self): self.view().autoRange() ## don't let signal call this directly--it'll add an unwanted argument From 0a5cb62a6f6f8687f52037f80691e719abe75d81 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 20 Nov 2013 13:51:39 -0500 Subject: [PATCH 012/268] ImageItem now has auto downsampling; seems to be working properly. Still need auto clipping as well. --- pyqtgraph/functions.py | 40 ++++++++++++++++++++++++++++ pyqtgraph/graphicsItems/ImageItem.py | 39 ++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 337dfb67..859ff758 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1068,6 +1068,46 @@ def colorToAlpha(data, color): #raise Exception() return np.clip(output, 0, 255).astype(np.ubyte) +def downsample(data, n, axis=0, xvals='subsample'): + """Downsample by averaging points together across axis. + If multiple axes are specified, runs once per axis. + If a metaArray is given, then the axis values can be either subsampled + or downsampled to match. + """ + ma = None + if (hasattr(data, 'implements') and data.implements('MetaArray')): + ma = data + data = data.view(np.ndarray) + + + if hasattr(axis, '__len__'): + if not hasattr(n, '__len__'): + n = [n]*len(axis) + for i in range(len(axis)): + data = downsample(data, n[i], axis[i]) + return data + + nPts = int(data.shape[axis] / n) + s = list(data.shape) + s[axis] = nPts + s.insert(axis+1, n) + sl = [slice(None)] * data.ndim + sl[axis] = slice(0, nPts*n) + d1 = data[tuple(sl)] + #print d1.shape, s + d1.shape = tuple(s) + d2 = d1.mean(axis+1) + + if ma is None: + return d2 + else: + info = ma.infoCopy() + if 'values' in info[axis]: + if xvals == 'subsample': + info[axis]['values'] = info[axis]['values'][::n][:nPts] + elif xvals == 'downsample': + info[axis]['values'] = downsample(info[axis]['values'], n) + return MetaArray(d2, info=info) def arrayToQPath(x, y, connect='all'): diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 530db7fb..47250cf2 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -1,3 +1,4 @@ +import pyqtgraph as pg from pyqtgraph.Qt import QtGui, QtCore import numpy as np import collections @@ -44,6 +45,7 @@ class ImageItem(GraphicsObject): self.levels = None ## [min, max] or [[redMin, redMax], ...] self.lut = None + self.autoDownsample = False #self.clipLevel = None self.drawKernel = None @@ -140,6 +142,11 @@ class ImageItem(GraphicsObject): if update: self.updateImage() + def setAutoDownsample(self, ads): + self.autoDownsample = ads + self.qimage = None + self.update() + def setOpts(self, update=True, **kargs): if 'lut' in kargs: self.setLookupTable(kargs['lut'], update=update) @@ -156,6 +163,10 @@ class ImageItem(GraphicsObject): if 'removable' in kargs: self.removable = kargs['removable'] self.menu = None + if 'autoDownsample' in kargs: + self.setAutoDownsample(kargs['autoDownsample']) + if update: + self.update() def setRect(self, rect): """Scale and translate the image to fit within rect (must be a QRect or QRectF).""" @@ -198,6 +209,9 @@ class ImageItem(GraphicsObject): gotNewData = True shapeChanged = (self.image is None or image.shape != self.image.shape) self.image = image.view(np.ndarray) + if self.image.shape[0] > 2**15-1 or self.image.shape[1] > 2**15-1: + if 'autoDownsample' not in kargs: + kargs['autoDownsample'] = True if shapeChanged: self.prepareGeometryChange() self.informViewBoundsChanged() @@ -259,8 +273,22 @@ class ImageItem(GraphicsObject): lut = self.lut #print lut.shape #print self.lut - - argb, alpha = fn.makeARGB(self.image, lut=lut, levels=self.levels) + if self.autoDownsample: + # reduce dimensions of image based on screen resolution + o = self.mapToDevice(QtCore.QPointF(0,0)) + x = self.mapToDevice(QtCore.QPointF(1,0)) + y = self.mapToDevice(QtCore.QPointF(0,1)) + w = pg.Point(x-o).length() + h = pg.Point(y-o).length() + xds = max(1, int(1/w)) + yds = max(1, int(1/h)) + image = fn.downsample(self.image, xds, axis=0) + image = fn.downsample(image, yds, axis=1) + else: + image = self.image + + + argb, alpha = fn.makeARGB(image, lut=lut, levels=self.levels) self.qimage = fn.makeQImage(argb, alpha) prof.finish() @@ -278,7 +306,7 @@ class ImageItem(GraphicsObject): p.setCompositionMode(self.paintMode) prof.mark('set comp mode') - p.drawImage(QtCore.QPointF(0,0), self.qimage) + p.drawImage(QtCore.QRectF(0,0,self.image.shape[0],self.image.shape[1]), self.qimage) prof.mark('p.drawImage') if self.border is not None: p.setPen(self.border) @@ -327,6 +355,11 @@ class ImageItem(GraphicsObject): if self.image is None: return 1,1 return br.width()/self.width(), br.height()/self.height() + + def viewTransformChanged(self): + if self.autoDownsample: + self.qimage = None + self.update() #def mousePressEvent(self, ev): #if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: From 23a0d6d7c07aba4c45e14d72b9e3647eaa52d8bd Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 20 Nov 2013 12:13:35 -0800 Subject: [PATCH 013/268] Use actions of ViewBox's contextMenu in full menu. The main change is on `ViewBox.getContextMenus`, which now returns an up-to-date of actions that `GraphicsScene.addParentContextMenus` can use. Also, `getContextMenus` was given a default implementation in the base class (falling back on `getMenu` if defined), and some cleanup was done. --- pyqtgraph/GraphicsScene/GraphicsScene.py | 29 +++++++------------ pyqtgraph/flowchart/Node.py | 3 -- pyqtgraph/flowchart/Terminal.py | 4 --- pyqtgraph/graphicsItems/GraphicsItem.py | 3 ++ pyqtgraph/graphicsItems/ROI.py | 6 +--- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 7 +---- .../graphicsItems/ViewBox/ViewBoxMenu.py | 16 ---------- 7 files changed, 15 insertions(+), 53 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 8729d085..6850371a 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -489,7 +489,7 @@ class GraphicsScene(QtGui.QGraphicsScene): #return v #else: #return widget - + def addParentContextMenus(self, item, menu, event): """ Can be called by any item in the scene to expand its context menu to include parent context menus. @@ -519,30 +519,21 @@ class GraphicsScene(QtGui.QGraphicsScene): event The original event that triggered the menu to appear. ============== ================================================== """ - - #items = self.itemsNearEvent(ev) + menusToAdd = [] while item is not self: item = item.parentItem() - if item is None: item = self - - if not hasattr(item, "getContextMenus"): - continue - - subMenus = item.getContextMenus(event) - if subMenus is None: - continue - if type(subMenus) is not list: ## so that some items (like FlowchartViewBox) can return multiple menus - subMenus = [subMenus] - - for sm in subMenus: - menusToAdd.append(sm) - - if len(menusToAdd) > 0: + subMenus = item.getContextMenus(event) or [] + if isinstance(subMenus, list): ## so that some items (like FlowchartViewBox) can return multiple menus + menusToAdd.extend(subMenus) + else: + menusToAdd.append(subMenus) + + if menusToAdd: menu.addSeparator() - + for m in menusToAdd: if isinstance(m, QtGui.QMenu): menu.addMenu(m) diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py index cd73b42b..f1de40d6 100644 --- a/pyqtgraph/flowchart/Node.py +++ b/pyqtgraph/flowchart/Node.py @@ -617,9 +617,6 @@ class NodeGraphicsItem(GraphicsObject): def getMenu(self): return self.menu - - def getContextMenus(self, event): - return [self.menu] def raiseContextMenu(self, ev): menu = self.scene().addParentContextMenus(self, self.getMenu(), ev) diff --git a/pyqtgraph/flowchart/Terminal.py b/pyqtgraph/flowchart/Terminal.py index 45805cd8..fea60dee 100644 --- a/pyqtgraph/flowchart/Terminal.py +++ b/pyqtgraph/flowchart/Terminal.py @@ -436,10 +436,6 @@ class TerminalGraphicsItem(GraphicsObject): def toggleMulti(self): multi = self.menu.multiAct.isChecked() self.term.setMultiValue(multi) - - ## probably never need this - #def getContextMenus(self, ev): - #return [self.getMenu()] def removeSelf(self): self.term.node().removeTerminal(self.term) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index a129436e..19cddd8a 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -585,3 +585,6 @@ class GraphicsItem(object): #def update(self): #self._qtBaseClass.update(self) #print "Update:", self + + def getContextMenus(self, event): + return [self.getMenu()] if hasattr(self, "getMenu") else [] diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index f6ce4680..cb5f4f30 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1202,11 +1202,7 @@ class Handle(UIGraphicsItem): def getMenu(self): return self.menu - - - def getContextMenus(self, event): - return [self.menu] - + def raiseContextMenu(self, ev): menu = self.scene().addParentContextMenus(self, self.getMenu(), ev) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 6e0a20d2..c60923b0 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1064,12 +1064,7 @@ class ViewBox(GraphicsWidget): return self.menu def getContextMenus(self, event): - if self.menuEnabled(): - return self.menu.subMenus() - else: - return None - #return [self.getMenu(event)] - + return self.menu.actions() if self.menuEnabled() else [] def mouseDragEvent(self, ev, axis=None): ## if axis is specified, event will only affect that axis. diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 99c3c3fb..15d0be06 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -88,22 +88,6 @@ class ViewBoxMenu(QtGui.QMenu): self.updateState() - #def copy(self): - #m = QtGui.QMenu() - #for sm in self.subMenus(): - #if isinstance(sm, QtGui.QMenu): - #m.addMenu(sm) - #else: - #m.addAction(sm) - #m.setTitle(self.title()) - #return m - - def subMenus(self): - if not self.valid: - self.updateState() - return [self.viewAll] + self.axes + [self.leftMenu] - - def setExportMethods(self, methods): self.exportMethods = methods self.export.clear() From 19cf49bc7d5286eb8b0d9a826c1293ba97a120fc Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 21 Nov 2013 23:29:03 -0500 Subject: [PATCH 014/268] fixed context menu handling for non-GraphicsItems --- pyqtgraph/GraphicsScene/GraphicsScene.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 6850371a..3fdd5924 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -525,6 +525,8 @@ class GraphicsScene(QtGui.QGraphicsScene): item = item.parentItem() if item is None: item = self + if not hasattr(item, "getContextMenus"): + continue subMenus = item.getContextMenus(event) or [] if isinstance(subMenus, list): ## so that some items (like FlowchartViewBox) can return multiple menus menusToAdd.extend(subMenus) From d34bdb1be71ebd42aa00f7adcc767124af5f44a2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 22 Nov 2013 09:33:02 -0500 Subject: [PATCH 015/268] corrected GradientWidget.__all__ --- pyqtgraph/widgets/GradientWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/GradientWidget.py b/pyqtgraph/widgets/GradientWidget.py index 1723a94b..7cbc032e 100644 --- a/pyqtgraph/widgets/GradientWidget.py +++ b/pyqtgraph/widgets/GradientWidget.py @@ -5,7 +5,7 @@ from pyqtgraph.graphicsItems.GradientEditorItem import GradientEditorItem import weakref import numpy as np -__all__ = ['TickSlider', 'GradientWidget', 'BlackWhiteSlider'] +__all__ = ['GradientWidget'] class GradientWidget(GraphicsView): From 85d7116482e4827b7f6ec20105983732fd5905be Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 23 Nov 2013 23:02:19 -0600 Subject: [PATCH 016/268] Speedups for making ARGB arrays --- pyqtgraph/functions.py | 602 +++++++++++++++++++++-------------------- 1 file changed, 308 insertions(+), 294 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 3f0c6a3e..df7ead47 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -16,7 +16,7 @@ Colors = { 'y': (255,255,0,255), 'k': (0,0,0,255), 'w': (255,255,255,255), -} +} SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY') SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' @@ -47,16 +47,16 @@ from . import debug def siScale(x, minVal=1e-25, allowUnicode=True): """ Return the recommended scale factor and SI prefix string for x. - + Example:: - + siScale(0.0001) # returns (1e6, 'μ') # This indicates that the number 0.0001 is best represented as 0.0001 * 1e6 = 100 μUnits """ - + if isinstance(x, decimal.Decimal): x = float(x) - + try: if np.isnan(x) or np.isinf(x): return(1, '') @@ -68,7 +68,7 @@ def siScale(x, minVal=1e-25, allowUnicode=True): x = 0 else: m = int(np.clip(np.floor(np.log(abs(x))/np.log(1000)), -9.0, 9.0)) - + if m == 0: pref = '' elif m < -8 or m > 8: @@ -79,27 +79,27 @@ def siScale(x, minVal=1e-25, allowUnicode=True): else: pref = SI_PREFIXES_ASCII[m+8] p = .001**m - - return (p, pref) + + return (p, pref) def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True): """ Return the number x formatted in engineering notation with SI prefix. - + Example:: siFormat(0.0001, suffix='V') # returns "100 μV" """ - + if space is True: space = ' ' if space is False: space = '' - - + + (p, pref) = siScale(x, minVal, allowUnicode) if not (len(pref) > 0 and pref[0] == 'e'): pref = space + pref - + if error is None: fmt = "%." + str(precision) + "g%s%s" return fmt % (x*p, pref, suffix) @@ -110,16 +110,16 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al plusminus = " +/- " fmt = "%." + str(precision) + "g%s%s%s%s" return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal)) - + def siEval(s): """ Convert a value written in SI notation to its equivalent prefixless value - + Example:: - + siEval("100 μV") # returns 0.0001 """ - + s = asUnicode(s) m = re.match(r'(-?((\d+(\.\d*)?)|(\.\d+))([eE]-?\d+)?)\s*([u' + SI_PREFIXES + r']?).*$', s) if m is None: @@ -135,35 +135,35 @@ def siEval(s): else: n = SI_PREFIXES.index(p) - 8 return v * 1000**n - + class Color(QtGui.QColor): def __init__(self, *args): QtGui.QColor.__init__(self, mkColor(*args)) - + def glColor(self): """Return (r,g,b,a) normalized for use in opengl""" return (self.red()/255., self.green()/255., self.blue()/255., self.alpha()/255.) - + def __getitem__(self, ind): return (self.red, self.green, self.blue, self.alpha)[ind]() - - + + def mkColor(*args): """ Convenience function for constructing QColor from a variety of argument types. Accepted arguments are: - + ================ ================================================ - 'c' one of: r, g, b, c, m, y, k, w + 'c' one of: r, g, b, c, m, y, k, w R, G, B, [A] integers 0-255 (R, G, B, [A]) tuple of integers 0-255 float greyscale, 0.0-1.0 int see :func:`intColor() ` (int, hues) see :func:`intColor() ` "RGB" hexadecimal strings; may begin with '#' - "RGBA" - "RRGGBB" - "RRGGBBAA" + "RGBA" + "RRGGBB" + "RRGGBBAA" QColor QColor instance; makes a copy. ================ ================================================ """ @@ -221,7 +221,7 @@ def mkColor(*args): (r, g, b, a) = args else: raise Exception(err) - + args = [r,g,b,a] args = [0 if np.isnan(a) or np.isinf(a) else a for a in args] args = list(map(int, args)) @@ -250,25 +250,25 @@ def mkBrush(*args, **kwds): def mkPen(*args, **kargs): """ - Convenience function for constructing QPen. - + Convenience function for constructing QPen. + Examples:: - + mkPen(color) mkPen(color, width=2) mkPen(cosmetic=False, width=4.5, color='r') mkPen({'color': "FF0", width: 2}) mkPen(None) # (no pen) - + In these examples, *color* may be replaced with any arguments accepted by :func:`mkColor() ` """ - + color = kargs.get('color', None) width = kargs.get('width', 1) style = kargs.get('style', None) dash = kargs.get('dash', None) cosmetic = kargs.get('cosmetic', True) hsv = kargs.get('hsv', None) - + if len(args) == 1: arg = args[0] if isinstance(arg, dict): @@ -281,14 +281,14 @@ def mkPen(*args, **kargs): color = arg if len(args) > 1: color = args - + if color is None: color = mkColor(200, 200, 200) if hsv is not None: color = hsvColor(*hsv) else: color = mkColor(color) - + pen = QtGui.QPen(QtGui.QBrush(color), width) pen.setCosmetic(cosmetic) if style is not None: @@ -303,7 +303,7 @@ def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0): c.setHsvF(hue, sat, val, alpha) return c - + def colorTuple(c): """Return a tuple (R,G,B,A) from a QColor""" return (c.red(), c.green(), c.blue(), c.alpha()) @@ -315,10 +315,10 @@ def colorStr(c): def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs): """ Creates a QColor from a single index. Useful for stepping through a predefined list of colors. - + The argument *index* determines which color from the set will be returned. All other arguments determine what the set of predefined colors will be - - Colors are chosen by cycling across hues while varying the value (brightness). + + Colors are chosen by cycling across hues while varying the value (brightness). By default, this selects from a list of 9 hues.""" hues = int(hues) values = int(values) @@ -330,7 +330,7 @@ def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, mi else: v = maxValue h = minHue + (indh * (maxHue-minHue)) / hues - + c = QtGui.QColor() c.setHsv(h, sat, v) c.setAlpha(alpha) @@ -344,7 +344,7 @@ def glColor(*args, **kargs): c = mkColor(*args, **kargs) return (c.red()/255., c.green()/255., c.blue()/255., c.alpha()/255.) - + def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0): """ @@ -370,25 +370,25 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0) path.lineTo(headLen, headWidth) path.lineTo(0,0) return path - - - + + + def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs): """ Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data. - + The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates (see the scipy documentation for more information about this). - + For a graphical interface to this function, see :func:`ROI.getArrayRegion ` - + ============== ==================================================================================================== Arguments: *data* (ndarray) the original dataset *shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape)) *origin* the location in the original dataset that will become the origin of the sliced data. - *vectors* list of unit vectors which point in the direction of the slice axes. Each vector must have the same - length as *axes*. If the vectors are not unit length, the result will be scaled relative to the - original data. If the vectors are not orthogonal, the result will be sheared relative to the + *vectors* list of unit vectors which point in the direction of the slice axes. Each vector must have the same + length as *axes*. If the vectors are not unit length, the result will be scaled relative to the + original data. If the vectors are not orthogonal, the result will be sheared relative to the original data. *axes* The axes in the original dataset which correspond to the slice *vectors* *order* The order of spline interpolation. Default is 1 (linear). See scipy.ndimage.map_coordinates @@ -398,23 +398,23 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, *All extra keyword arguments are passed to scipy.ndimage.map_coordinates.* -------------------------------------------------------------------------------------------------------------------- ============== ==================================================================================================== - - Note the following must be true: - - | len(shape) == len(vectors) + + Note the following must be true: + + | len(shape) == len(vectors) | len(origin) == len(axes) == len(vectors[i]) - + Example: start with a 4D fMRI data set, take a diagonal-planar slice out of the last 3 axes - + * data = array with dims (time, x, y, z) = (100, 40, 40, 40) - * The plane to pull out is perpendicular to the vector (x,y,z) = (1,1,1) + * The plane to pull out is perpendicular to the vector (x,y,z) = (1,1,1) * The origin of the slice will be at (x,y,z) = (40, 0, 0) * We will slice a 20x20 plane from each timepoint, giving a final shape (100, 20, 20) - + The call for this example would look like:: - + affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) - + """ if not HAVE_SCIPY: raise Exception("This function requires the scipy library, but it does not appear to be importable.") @@ -427,7 +427,7 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, for v in vectors: if len(v) != len(axes): raise Exception("each vector must be same length as axes.") - + shape = list(map(np.ceil, shape)) ## transpose data so slice axes come first @@ -438,7 +438,7 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, data = data.transpose(tr1) #print "tr1:", tr1 ## dims are now [(slice axes), (other axes)] - + ## make sure vectors are arrays if not isinstance(vectors, np.ndarray): @@ -446,8 +446,8 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, if not isinstance(origin, np.ndarray): origin = np.array(origin) origin.shape = (len(axes),) + (1,)*len(shape) - - ## Build array of sample locations. + + ## Build array of sample locations. grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes #print shape, grid.shape x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic @@ -461,7 +461,7 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, ind = (Ellipsis,) + inds #print data[ind].shape, x.shape, output[ind].shape, output.shape output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs) - + tr = list(range(output.ndim)) trb = [] for i in range(min(axes)): @@ -481,20 +481,20 @@ def transformToArray(tr): """ Given a QTransform, return a 3x3 numpy array. Given a QMatrix4x4, return a 4x4 numpy array. - + Example: map an array of x,y coordinates through a transform:: - + ## coordinates to map are (1,5), (2,6), (3,7), and (4,8) coords = np.array([[1,2,3,4], [5,6,7,8], [1,1,1,1]]) # the extra '1' coordinate is needed for translation to work - + ## Make an example transform tr = QtGui.QTransform() tr.translate(3,4) tr.scale(2, 0.1) - + ## convert to array m = pg.transformToArray()[:2] # ignore the perspective portion of the transformation - + ## map coordinates through transform mapped = np.dot(m, coords) """ @@ -515,24 +515,24 @@ def transformCoordinates(tr, coords, transpose=False): Map a set of 2D or 3D coordinates through a QTransform or QMatrix4x4. The shape of coords must be (2,...) or (3,...) The mapping will _ignore_ any perspective transformations. - + For coordinate arrays with ndim=2, this is basically equivalent to matrix multiplication. - Most arrays, however, prefer to put the coordinate axis at the end (eg. shape=(...,3)). To + Most arrays, however, prefer to put the coordinate axis at the end (eg. shape=(...,3)). To allow this, use transpose=True. - + """ - + if transpose: ## move last axis to beginning. This transposition will be reversed before returning the mapped coordinates. coords = coords.transpose((coords.ndim-1,) + tuple(range(0,coords.ndim-1))) - + nd = coords.shape[0] if isinstance(tr, np.ndarray): m = tr else: m = transformToArray(tr) m = m[:m.shape[0]-1] # remove perspective - + ## If coords are 3D and tr is 2D, assume no change for Z axis if m.shape == (2,3) and nd == 3: m2 = np.zeros((3,4)) @@ -540,34 +540,34 @@ def transformCoordinates(tr, coords, transpose=False): m2[:2, 3] = m[:2,2] m2[2,2] = 1 m = m2 - + ## if coords are 2D and tr is 3D, ignore Z axis if m.shape == (3,4) and nd == 2: m2 = np.empty((2,3)) m2[:,:2] = m[:2,:2] m2[:,2] = m[:2,3] m = m2 - + ## reshape tr and coords to prepare for multiplication m = m.reshape(m.shape + (1,)*(coords.ndim-1)) coords = coords[np.newaxis, ...] - - # separate scale/rotate and translation - translate = m[:,-1] + + # separate scale/rotate and translation + translate = m[:,-1] m = m[:, :-1] - + ## map coordinates and return mapped = (m*coords).sum(axis=1) ## apply scale/rotate mapped += translate - + if transpose: ## move first axis to end. mapped = mapped.transpose(tuple(range(1,mapped.ndim)) + (0,)) return mapped - - - + + + def solve3DTransform(points1, points2): """ Find a 3D transformation matrix that maps points1 onto points2. @@ -577,21 +577,21 @@ def solve3DTransform(points1, points2): raise Exception("This function depends on the scipy library, but it does not appear to be importable.") A = np.array([[points1[i].x(), points1[i].y(), points1[i].z(), 1] for i in range(4)]) B = np.array([[points2[i].x(), points2[i].y(), points2[i].z(), 1] for i in range(4)]) - + ## solve 3 sets of linear equations to determine transformation matrix elements matrix = np.zeros((4,4)) for i in range(3): matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix - + return matrix - + def solveBilinearTransform(points1, points2): """ Find a bilinear transformation matrix (2x4) that maps points1 onto points2. Points must be specified as a list of 4 Vector, Point, QPointF, etc. - + To use this matrix to map a point [x,y]:: - + mapped = np.dot(matrix, [x*y, x, y, 1]) """ if not HAVE_SCIPY: @@ -600,30 +600,30 @@ def solveBilinearTransform(points1, points2): ## B is 4 rows (points) x 2 columns (x, y) A = np.array([[points1[i].x()*points1[i].y(), points1[i].x(), points1[i].y(), 1] for i in range(4)]) B = np.array([[points2[i].x(), points2[i].y()] for i in range(4)]) - + ## solve 2 sets of linear equations to determine transformation matrix elements matrix = np.zeros((2,4)) for i in range(2): matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix - + return matrix - + def rescaleData(data, scale, offset, dtype=None): """Return data rescaled and optionally cast to a new dtype:: - + data => (data-offset) * scale - + Uses scipy.weave (if available) to improve performance. """ if dtype is None: dtype = data.dtype else: dtype = np.dtype(dtype) - + try: if not pg.getConfigOption('useWeave'): raise Exception('Weave is disabled; falling back to slower version.') - + ## require native dtype when using weave if not data.dtype.isnative: data = data.astype(data.dtype.newbyteorder('=')) @@ -631,11 +631,11 @@ def rescaleData(data, scale, offset, dtype=None): weaveDtype = dtype.newbyteorder('=') else: weaveDtype = dtype - + newData = np.empty((data.size,), dtype=weaveDtype) flat = np.ascontiguousarray(data).reshape(data.size) size = data.size - + code = """ double sc = (double)scale; double off = (double)offset; @@ -652,61 +652,61 @@ def rescaleData(data, scale, offset, dtype=None): if pg.getConfigOption('weaveDebug'): debug.printExc("Error; disabling weave.") pg.setConfigOption('useWeave', False) - + #p = np.poly1d([scale, -offset*scale]) #data = p(data).astype(dtype) d2 = data-offset d2 *= scale data = d2.astype(dtype) return data - + def applyLookupTable(data, lut): """ Uses values in *data* as indexes to select values from *lut*. The returned data has shape data.shape + lut.shape[1:] - + Uses scipy.weave to improve performance if it is available. Note: color gradient lookup tables can be generated using GradientWidget. """ if data.dtype.kind not in ('i', 'u'): data = data.astype(int) - + ## using np.take appears to be faster than even the scipy.weave method and takes care of clipping as well. - return np.take(lut, data, axis=0, mode='clip') - - ### old methods: + return np.take(lut, data, axis=0, mode='clip') + + ### old methods: #data = np.clip(data, 0, lut.shape[0]-1) - + #try: #if not USE_WEAVE: #raise Exception('Weave is disabled; falling back to slower version.') - + ### number of values to copy for each LUT lookup #if lut.ndim == 1: #ncol = 1 #else: #ncol = sum(lut.shape[1:]) - + ### output array #newData = np.empty((data.size, ncol), dtype=lut.dtype) - + ### flattened input arrays #flatData = data.flatten() #flatLut = lut.reshape((lut.shape[0], ncol)) - + #dataSize = data.size - - ### strides for accessing each item + + ### strides for accessing each item #newStride = newData.strides[0] / newData.dtype.itemsize #lutStride = flatLut.strides[0] / flatLut.dtype.itemsize #dataStride = flatData.strides[0] / flatData.dtype.itemsize - + ### strides for accessing individual values within a single LUT lookup #newColStride = newData.strides[1] / newData.dtype.itemsize #lutColStride = flatLut.strides[1] / flatLut.dtype.itemsize - + #code = """ - + #for( int i=0; i0 and max->*scale*:: - + rescaled = (clip(data, min, max) - min) * (*scale* / (max - min)) - + It is also possible to use a 2D (N,2) array of values for levels. In this case, - it is assumed that each pair of min,max values in the levels array should be - applied to a different subset of the input data (for example, the input data may - already have RGB values and the levels are used to independently scale each + it is assumed that each pair of min,max values in the levels array should be + applied to a different subset of the input data (for example, the input data may + already have RGB values and the levels are used to independently scale each channel). The use of this feature requires that levels.shape[0] == data.shape[-1]. - scale The maximum value to which data will be rescaled before being passed through the + scale The maximum value to which data will be rescaled before being passed through the lookup table (or returned if there is no lookup table). By default this will be set to the length of the lookup table, or 256 is no lookup table is provided. For OpenGL color specifications (as in GLColor4f) use scale=1.0 lut Optional lookup table (array with dtype=ubyte). Values in data will be converted to color by indexing directly from lut. The output data shape will be input.shape + lut.shape[1:]. - + Note: the output of makeARGB will have the same dtype as the lookup table, so for conversion to QImage, the dtype must be ubyte. - + Lookup tables can be built using GradientWidget. - useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures). - The default is False, which returns in ARGB order for use with QImage - (Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order + useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures). + The default is False, which returns in ARGB order for use with QImage + (Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order is BGRA). + transpose Whether to pre-transpose the data in preparation for use in Qt ============ ================================================================================== """ prof = debug.Profiler('functions.makeARGB', disabled=True) - + if lut is not None and not isinstance(lut, np.ndarray): lut = np.array(lut) if levels is not None and not isinstance(levels, np.ndarray): levels = np.array(levels) - + ## sanity checks #if data.ndim == 3: #if data.shape[2] not in (3,4): @@ -791,7 +792,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): ##raise Exception("can not use lookup table with 3D data") #elif data.ndim != 2: #raise Exception("data must be 2D or 3D") - + #if lut is not None: ##if lut.ndim == 2: ##if lut.shape[1] : @@ -800,7 +801,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): ##raise Exception("lut must be 1D or 2D") #if lut.dtype != np.ubyte: #raise Exception('lookup table must have dtype=ubyte (got %s instead)' % str(lut.dtype)) - + if levels is not None: if levels.ndim == 1: @@ -824,7 +825,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): #raise Exception('Can not use 2D levels and lookup table together.') #else: #raise Exception("Levels must have shape (2,) or (3,2) or (4,2)") - + prof.mark('1') if scale is None: @@ -835,7 +836,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): ## Apply levels if given if levels is not None: - + if isinstance(levels, np.ndarray) and levels.ndim == 2: ## we are going to rescale each channel independently if levels.shape[0] != data.shape[-1]: @@ -865,9 +866,10 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): ## copy data into ARGB ordered array - imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte) - if data.ndim == 2: - data = data[..., np.newaxis] + if transpose: + imgData = np.empty((data.shape[1], data.shape[0], 4), dtype=np.ubyte) + else: + imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte) prof.mark('4') @@ -875,27 +877,39 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): order = [0,1,2,3] ## array comes out RGBA else: order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. - - if data.shape[2] == 1: + + if data.ndim == 2: for i in range(3): - imgData[..., order[i]] = data[..., 0] + if transpose: + imgData[..., i] = data.T + else: + imgData[..., i] = data + elif data.shape[2] == 1: + for i in range(3): + if transpose: + imgData[..., i] = data[..., 0].T + else: + imgData[..., i] = data[..., 0] else: for i in range(0, data.shape[2]): - imgData[..., order[i]] = data[..., i] - + if transpose: + imgData[..., order[i]] = data[..., order[i]].T + else: + imgData[..., order[i]] = data[..., order[i]] + prof.mark('5') - - if data.shape[2] == 4: - alpha = True - else: + + if data.ndim == 2 or data.shape[2] == 3: alpha = False imgData[..., 3] = 255 - + else: + alpha = True + prof.mark('6') - + prof.finish() return imgData, alpha - + def makeQImage(imgData, alpha=None, copy=True, transpose=True): """ @@ -904,11 +918,11 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): be reflected in the image. The image will be given a 'data' attribute pointing to the array which shares its data to prevent python freeing that memory while the image is in use. - + =========== =================================================================== Arguments: - imgData Array of data to convert. Must have shape (width, height, 3 or 4) - and dtype=ubyte. The order of values in the 3rd axis must be + imgData Array of data to convert. Must have shape (width, height, 3 or 4) + and dtype=ubyte. The order of values in the 3rd axis must be (b, g, r, a). alpha If True, the QImage returned will have format ARGB32. If False, the format will be RGB32. By default, _alpha_ is True if @@ -917,19 +931,19 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): If False, the new QImage points directly to the data in the array. Note that the array must be contiguous for this to work (see numpy.ascontiguousarray). - transpose If True (the default), the array x/y axes are transposed before - creating the image. Note that Qt expects the axes to be in - (height, width) order whereas pyqtgraph usually prefers the + transpose If True (the default), the array x/y axes are transposed before + creating the image. Note that Qt expects the axes to be in + (height, width) order whereas pyqtgraph usually prefers the opposite. - =========== =================================================================== + =========== =================================================================== """ ## create QImage from buffer prof = debug.Profiler('functions.makeQImage', disabled=True) - + ## If we didn't explicitly specify alpha, check the array shape. if alpha is None: alpha = (imgData.shape[2] == 4) - + copied = False if imgData.shape[2] == 3: ## need to make alpha channel (even if alpha==False; QImage requires 32 bpp) if copy is True: @@ -940,25 +954,25 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): copied = True else: raise Exception('Array has only 3 channels; cannot make QImage without copying.') - + if alpha: imgFormat = QtGui.QImage.Format_ARGB32 else: imgFormat = QtGui.QImage.Format_RGB32 - + if transpose: imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite - + if not imgData.flags['C_CONTIGUOUS']: if copy is False: extra = ' (try setting transpose=False)' if transpose else '' raise Exception('Array is not contiguous; cannot make QImage without copying.'+extra) imgData = np.ascontiguousarray(imgData) copied = True - + if copy is True and copied is False: imgData = imgData.copy() - + if USE_PYSIDE: ch = ctypes.c_char.from_buffer(imgData, 0) img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) @@ -969,7 +983,7 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): #addr = ctypes.c_char.from_buffer(imgData, 0) #try: #img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) - #except TypeError: + #except TypeError: #addr = ctypes.addressof(addr) #img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) try: @@ -981,14 +995,14 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): else: # mutable, but leaks memory img = QtGui.QImage(memoryview(imgData), imgData.shape[1], imgData.shape[0], imgFormat) - + img.data = imgData return img #try: #buf = imgData.data #except AttributeError: ## happens when image data is non-contiguous #buf = imgData.data - + #prof.mark('1') #qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat) #prof.mark('2') @@ -999,7 +1013,7 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): def imageToArray(img, copy=False, transpose=True): """ Convert a QImage into numpy array. The image must have format RGB32, ARGB32, or ARGB32_Premultiplied. - By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if + By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if the QImage is collected before the array, there may be trouble). The array will have shape (width, height, (b,g,r,a)). """ @@ -1010,34 +1024,34 @@ def imageToArray(img, copy=False, transpose=True): else: ptr.setsize(img.byteCount()) arr = np.asarray(ptr) - + if fmt == img.Format_RGB32: arr = arr.reshape(img.height(), img.width(), 3) elif fmt == img.Format_ARGB32 or fmt == img.Format_ARGB32_Premultiplied: arr = arr.reshape(img.height(), img.width(), 4) - + if copy: arr = arr.copy() - + if transpose: return arr.transpose((1,0,2)) else: return arr - + def colorToAlpha(data, color): """ - Given an RGBA image in *data*, convert *color* to be transparent. - *data* must be an array (w, h, 3 or 4) of ubyte values and *color* must be + Given an RGBA image in *data*, convert *color* to be transparent. + *data* must be an array (w, h, 3 or 4) of ubyte values and *color* must be an array (3) of ubyte values. This is particularly useful for use with images that have a black or white background. - + Algorithm is taken from Gimp's color-to-alpha function in plug-ins/common/colortoalpha.c Credit: /* * Color To Alpha plug-in v1.0 by Seth Burgess, sjburges@gimp.org 1999/05/14 * with algorithm by clahey */ - + """ data = data.astype(float) if data.shape[-1] == 3: ## add alpha channel if needed @@ -1045,11 +1059,11 @@ def colorToAlpha(data, color): d2[...,:3] = data d2[...,3] = 255 data = d2 - + color = color.astype(float) alpha = np.zeros(data.shape[:2]+(3,), dtype=float) output = data.copy() - + for i in [0,1,2]: d = data[...,i] c = color[i] @@ -1057,18 +1071,18 @@ def colorToAlpha(data, color): alpha[...,i][mask] = (d[mask] - c) / (255. - c) imask = d < c alpha[...,i][imask] = (c - d[imask]) / c - + output[...,3] = alpha.max(axis=2) * 255. - + mask = output[...,3] >= 1.0 ## avoid zero division while processing alpha channel correction = 255. / output[...,3][mask] ## increase value to compensate for decreased alpha for i in [0,1,2]: output[...,i][mask] = ((output[...,i][mask]-color[i]) * correction) + color[i] output[...,3][mask] *= data[...,3][mask] / 255. ## combine computed and previous alpha values - + #raise Exception() return np.clip(output, 0, 255).astype(np.ubyte) - + def arrayToQPath(x, y, connect='all'): @@ -1170,28 +1184,28 @@ def arrayToQPath(x, y, connect='all'): #""" #Generate isosurface from volumetric data using marching tetrahedra algorithm. #See Paul Bourke, "Polygonising a Scalar Field Using Tetrahedrons" (http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/) - + #*data* 3D numpy array of scalar values #*level* The level at which to generate an isosurface #""" - + #facets = [] - + ### mark everything below the isosurface level #mask = data < level - - #### make eight sub-fields + + #### make eight sub-fields #fields = np.empty((2,2,2), dtype=object) #slices = [slice(0,-1), slice(1,None)] #for i in [0,1]: #for j in [0,1]: #for k in [0,1]: #fields[i,j,k] = mask[slices[i], slices[j], slices[k]] - - - + + + ### split each cell into 6 tetrahedra - ### these all have the same 'orienation'; points 1,2,3 circle + ### these all have the same 'orienation'; points 1,2,3 circle ### clockwise around point 0 #tetrahedra = [ #[(0,1,0), (1,1,1), (0,1,1), (1,0,1)], @@ -1201,15 +1215,15 @@ def arrayToQPath(x, y, connect='all'): #[(0,1,0), (1,0,0), (1,1,0), (1,0,1)], #[(0,1,0), (1,1,0), (1,1,1), (1,0,1)] #] - + ### each tetrahedron will be assigned an index ### which determines how to generate its facets. - ### this structure is: + ### this structure is: ### facets[index][facet1, facet2, ...] - ### where each facet is triangular and its points are each + ### where each facet is triangular and its points are each ### interpolated between two points on the tetrahedron ### facet = [(p1a, p1b), (p2a, p2b), (p3a, p3b)] - ### facet points always circle clockwise if you are looking + ### facet points always circle clockwise if you are looking ### at them from below the isosurface. #indexFacets = [ #[], ## all above @@ -1229,15 +1243,15 @@ def arrayToQPath(x, y, connect='all'): #[[(0,1), (0,3), (0,2)]], # 1,2,3 below #[] ## all below #] - + #for tet in tetrahedra: - + ### get the 4 fields for this tetrahedron #tetFields = [fields[c] for c in tet] - + ### generate an index for each grid cell #index = tetFields[0] + tetFields[1]*2 + tetFields[2]*4 + tetFields[3]*8 - + ### add facets #for i in xrange(index.shape[0]): # data x-axis #for j in xrange(index.shape[1]): # data y-axis @@ -1251,32 +1265,32 @@ def arrayToQPath(x, y, connect='all'): #facets.append(pts) #return facets - + def isocurve(data, level, connected=False, extendToEdge=False, path=False): """ Generate isocurve from 2D data using marching squares algorithm. - + ============= ========================================================= Arguments data 2D numpy array of scalar values level The level at which to generate an isosurface connected If False, return a single long list of point pairs - If True, return multiple long lists of connected point - locations. (This is slower but better for drawing + If True, return multiple long lists of connected point + locations. (This is slower but better for drawing continuous lines) - extendToEdge If True, extend the curves to reach the exact edges of - the data. - path if True, return a QPainterPath rather than a list of + extendToEdge If True, extend the curves to reach the exact edges of + the data. + path if True, return a QPainterPath rather than a list of vertex coordinates. This forces connected=True. ============= ========================================================= - + This function is SLOW; plenty of room for optimization here. - """ - + """ + if path is True: connected = True - + if extendToEdge: d2 = np.empty((data.shape[0]+2, data.shape[1]+2), dtype=data.dtype) d2[1:-1, 1:-1] = data @@ -1289,7 +1303,7 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): d2[-1,0] = d2[-1,1] d2[-1,-1] = d2[-1,-2] data = d2 - + sideTable = [ [], [0,1], @@ -1308,20 +1322,20 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): [0,1], [] ] - + edgeKey=[ [(0,1), (0,0)], [(0,0), (1,0)], [(1,0), (1,1)], [(1,1), (0,1)] ] - - + + lines = [] - + ## mark everything below the isosurface level mask = data < level - + ### make four sub-fields and compute indexes for grid cells index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) fields = np.empty((2,2), dtype=object) @@ -1335,10 +1349,10 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): index += fields[i,j] * 2**vertIndex #print index #print index - + ## add lines for i in range(index.shape[0]): # data x-axis - for j in range(index.shape[1]): # data y-axis + for j in range(index.shape[1]): # data y-axis sides = sideTable[index[i,j]] for l in range(0, len(sides), 2): ## faces for this grid cell edges = sides[l:l+2] @@ -1351,26 +1365,26 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): f = (level-v1) / (v2-v1) fi = 1.0 - f p = ( ## interpolate between corners - p1[0]*fi + p2[0]*f + i + 0.5, + p1[0]*fi + p2[0]*f + i + 0.5, p1[1]*fi + p2[1]*f + j + 0.5 ) if extendToEdge: ## check bounds p = ( min(data.shape[0]-2, max(0, p[0]-1)), - min(data.shape[1]-2, max(0, p[1]-1)), + min(data.shape[1]-2, max(0, p[1]-1)), ) if connected: gridKey = i + (1 if edges[m]==2 else 0), j + (1 if edges[m]==3 else 0), edges[m]%2 pts.append((p, gridKey)) ## give the actual position and a key identifying the grid location (for connecting segments) else: pts.append(p) - + lines.append(pts) if not connected: return lines - + ## turn disjoint list of segments into continuous lines #lines = [[2,5], [5,4], [3,4], [1,3], [6,7], [7,8], [8,6], [11,12], [12,15], [11,13], [13,14]] @@ -1397,9 +1411,9 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): while True: if x == chain[-1][1]: break ## nothing left to do on this chain - + x = chain[-1][1] - if x == k: + if x == k: break ## chain has looped; we're done and can ignore the opposite chain y = chain[-2][1] connects = points[x] @@ -1412,9 +1426,9 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): if chain[0][1] == chain[-1][1]: # looped chain; no need to continue the other direction chains.pop() break - - ## extract point locations + + ## extract point locations lines = [] for chain in points.values(): if len(chain) == 2: @@ -1422,25 +1436,25 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): else: chain = chain[0] lines.append([p[0] for p in chain]) - + if not path: return lines ## a list of pairs of points - + path = QtGui.QPainterPath() for line in lines: path.moveTo(*line[0]) for p in line[1:]: path.lineTo(*p) - + return path - - + + def traceImage(image, values, smooth=0.5): """ Convert an image to a set of QPainterPath curves. One curve will be generated for each item in *values*; each curve outlines the area of the image that is closer to its value than to any others. - + If image is RGB or RGBA, then the shape of values should be (nvals, 3/4) The parameter *smooth* is expressed in pixels. """ @@ -1452,11 +1466,11 @@ def traceImage(image, values, smooth=0.5): diff = np.abs(image-values) if values.ndim == 4: diff = diff.sum(axis=2) - + labels = np.argmin(diff, axis=2) - + paths = [] - for i in range(diff.shape[-1]): + for i in range(diff.shape[-1]): d = (labels==i).astype(float) d = ndi.gaussian_filter(d, (smooth, smooth)) lines = isocurve(d, 0.5, connected=True, extendToEdge=True) @@ -1465,31 +1479,31 @@ def traceImage(image, values, smooth=0.5): path.moveTo(*line[0]) for p in line[1:]: path.lineTo(*p) - + paths.append(path) return paths - - - + + + IsosurfaceDataCache = None def isosurface(data, level): """ Generate isosurface from volumetric data using marching cubes algorithm. - See Paul Bourke, "Polygonising a Scalar Field" + See Paul Bourke, "Polygonising a Scalar Field" (http://paulbourke.net/geometry/polygonise/) - + *data* 3D numpy array of scalar values *level* The level at which to generate an isosurface - - Returns an array of vertex coordinates (Nv, 3) and an array of - per-face vertex indexes (Nf, 3) + + Returns an array of vertex coordinates (Nv, 3) and an array of + per-face vertex indexes (Nf, 3) """ ## For improvement, see: - ## + ## ## Efficient implementation of Marching Cubes' cases with topological guarantees. ## Thomas Lewiner, Helio Lopes, Antonio Wilson Vieira and Geovan Tavares. ## Journal of Graphics Tools 8(2): pp. 1-15 (december 2003) - + ## Precompute lookup tables on the first run global IsosurfaceDataCache if IsosurfaceDataCache is None: @@ -1530,7 +1544,7 @@ def isosurface(data, level): 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190, 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ], dtype=np.uint16) - + ## Table of triangles to use for filling each grid cell. ## Each set of three integers tells us which three edges to ## draw a triangle between. @@ -1792,9 +1806,9 @@ def isosurface(data, level): [0, 9, 1], [0, 3, 8], [] - ] + ] edgeShifts = np.array([ ## maps edge ID (0-11) to (x,y,z) cell offset and edge ID (0-2) - [0, 0, 0, 0], + [0, 0, 0, 0], [1, 0, 0, 1], [0, 1, 0, 0], [0, 0, 0, 1], @@ -1817,23 +1831,23 @@ def isosurface(data, level): faceTableI[faceTableInds[:,0]] = np.array([triTable[j] for j in faceTableInds]) faceTableI = faceTableI.reshape((len(triTable), i, 3)) faceShiftTables.append(edgeShifts[faceTableI]) - + ## Let's try something different: #faceTable = np.empty((256, 5, 3, 4), dtype=np.ubyte) # (grid cell index, faces, vertexes, edge lookup) #for i,f in enumerate(triTable): #f = np.array(f + [12] * (15-len(f))).reshape(5,3) #faceTable[i] = edgeShifts[f] - - + + IsosurfaceDataCache = (faceShiftTables, edgeShifts, edgeTable, nTableFaces) else: faceShiftTables, edgeShifts, edgeTable, nTableFaces = IsosurfaceDataCache - + ## mark everything below the isosurface level mask = data < level - + ### make eight sub-fields and compute indexes for grid cells index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) fields = np.empty((2,2,2), dtype=object) @@ -1844,23 +1858,23 @@ def isosurface(data, level): fields[i,j,k] = mask[slices[i], slices[j], slices[k]] vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme index += fields[i,j,k] * 2**vertIndex - + ### Generate table of edges that have been cut cutEdges = np.zeros([x+1 for x in index.shape]+[3], dtype=np.uint32) edges = edgeTable[index] - for i, shift in enumerate(edgeShifts[:12]): + for i, shift in enumerate(edgeShifts[:12]): slices = [slice(shift[j],cutEdges.shape[j]+(shift[j]-1)) for j in range(3)] cutEdges[slices[0], slices[1], slices[2], shift[3]] += edges & 2**i - + ## for each cut edge, interpolate to see where exactly the edge is cut and generate vertex positions m = cutEdges > 0 vertexInds = np.argwhere(m) ## argwhere is slow! vertexes = vertexInds[:,:3].astype(np.float32) dataFlat = data.reshape(data.shape[0]*data.shape[1]*data.shape[2]) - + ## re-use the cutEdges array as a lookup table for vertex IDs cutEdges[vertexInds[:,0], vertexInds[:,1], vertexInds[:,2], vertexInds[:,3]] = np.arange(vertexInds.shape[0]) - + for i in [0,1,2]: vim = vertexInds[:,3] == i vi = vertexInds[vim, :3] @@ -1868,9 +1882,9 @@ def isosurface(data, level): v1 = dataFlat[viFlat] v2 = dataFlat[viFlat + data.strides[i]//data.itemsize] vertexes[vim,i] += (level-v1) / (v2-v1) - - ### compute the set of vertex indexes for each face. - + + ### compute the set of vertex indexes for each face. + ## This works, but runs a bit slower. #cells = np.argwhere((index != 0) & (index != 255)) ## all cells with at least one face #cellInds = index[cells[:,0], cells[:,1], cells[:,2]] @@ -1879,9 +1893,9 @@ def isosurface(data, level): #verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges #verts = verts[mask] #faces = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want. - - - ## To allow this to be vectorized efficiently, we count the number of faces in each + + + ## To allow this to be vectorized efficiently, we count the number of faces in each ## grid cell and handle each group of cells with the same number together. ## determine how many faces to assign to each grid cell nFaces = nTableFaces[index] @@ -1890,7 +1904,7 @@ def isosurface(data, level): ptr = 0 #import debug #p = debug.Profiler('isosurface', disabled=False) - + ## this helps speed up an indexing operation later on cs = np.array(cutEdges.strides)//cutEdges.itemsize cutEdges = cutEdges.flatten() @@ -1909,14 +1923,14 @@ def isosurface(data, level): #cellInds = index[(cells*ins[np.newaxis,:]).sum(axis=1)] cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round #p.mark('3') - + ### expensive: verts = faceShiftTables[i][cellInds] #p.mark('4') verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges verts = verts.reshape((verts.shape[0]*i,)+verts.shape[2:]) #p.mark('5') - + ### expensive: #print verts.shape verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2) @@ -1928,15 +1942,15 @@ def isosurface(data, level): faces[ptr:ptr+nv] = vertInds #.reshape((nv, 3)) #p.mark('8') ptr += nv - + return vertexes, faces - + def invertQTransform(tr): """Return a QTransform that is the inverse of *tr*. Rasises an exception if tr is not invertible. - + Note that this function is preferred over QTransform.inverted() due to bugs in that method. (specifically, Qt has floating-point precision issues when determining whether a matrix is invertible) @@ -1949,25 +1963,25 @@ def invertQTransform(tr): arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]]) inv = scipy.linalg.inv(arr) return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1]) - - + + def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): """ Used for examining the distribution of values in a set. Produces scattering as in beeswarm or column scatter plots. - + Given a list of x-values, construct a set of y-values such that an x,y scatter-plot will not have overlapping points (it will look similar to a histogram). """ inds = np.arange(len(data)) if shuffle: np.random.shuffle(inds) - + data = data[inds] - + if spacing is None: spacing = 2.*np.std(data)/len(data)**0.5 s2 = spacing**2 - + yvals = np.empty(len(data)) if len(data) == 0: return yvals @@ -1977,10 +1991,10 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): x0 = data[:i] # all x values already placed y0 = yvals[:i] # all y values already placed y = 0 - + dx = (x0-x)**2 # x-distance to each previous point xmask = dx < s2 # exclude anything too far away - + if xmask.sum() > 0: if bidir: dirs = [-1, 1] @@ -1990,24 +2004,24 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): for direction in dirs: y = 0 dx2 = dx[xmask] - dy = (s2 - dx2)**0.5 + dy = (s2 - dx2)**0.5 limits = np.empty((2,len(dy))) # ranges of y-values to exclude limits[0] = y0[xmask] - dy - limits[1] = y0[xmask] + dy + limits[1] = y0[xmask] + dy while True: # ignore anything below this y-value if direction > 0: mask = limits[1] >= y else: mask = limits[0] <= y - + limits2 = limits[:,mask] - + # are we inside an excluded region? mask = (limits2[0] < y) & (limits2[1] > y) if mask.sum() == 0: break - + if direction > 0: y = limits2[:,mask].max() else: @@ -2018,5 +2032,5 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): else: y = yopts[0] yvals[i] = y - + return yvals[np.argsort(inds)] ## un-shuffle values before returning From ddce17dc62164e6899040ef263ade106091b933f Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 23 Nov 2013 23:08:18 -0600 Subject: [PATCH 017/268] Undo remove trailing whitespace --- pyqtgraph/functions.py | 572 ++++++++++++++++++++--------------------- 1 file changed, 286 insertions(+), 286 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index df7ead47..bd2ed314 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -16,7 +16,7 @@ Colors = { 'y': (255,255,0,255), 'k': (0,0,0,255), 'w': (255,255,255,255), -} +} SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY') SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' @@ -47,16 +47,16 @@ from . import debug def siScale(x, minVal=1e-25, allowUnicode=True): """ Return the recommended scale factor and SI prefix string for x. - + Example:: - + siScale(0.0001) # returns (1e6, 'μ') # This indicates that the number 0.0001 is best represented as 0.0001 * 1e6 = 100 μUnits """ - + if isinstance(x, decimal.Decimal): x = float(x) - + try: if np.isnan(x) or np.isinf(x): return(1, '') @@ -68,7 +68,7 @@ def siScale(x, minVal=1e-25, allowUnicode=True): x = 0 else: m = int(np.clip(np.floor(np.log(abs(x))/np.log(1000)), -9.0, 9.0)) - + if m == 0: pref = '' elif m < -8 or m > 8: @@ -79,27 +79,27 @@ def siScale(x, minVal=1e-25, allowUnicode=True): else: pref = SI_PREFIXES_ASCII[m+8] p = .001**m - - return (p, pref) + + return (p, pref) def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True): """ Return the number x formatted in engineering notation with SI prefix. - + Example:: siFormat(0.0001, suffix='V') # returns "100 μV" """ - + if space is True: space = ' ' if space is False: space = '' - - + + (p, pref) = siScale(x, minVal, allowUnicode) if not (len(pref) > 0 and pref[0] == 'e'): pref = space + pref - + if error is None: fmt = "%." + str(precision) + "g%s%s" return fmt % (x*p, pref, suffix) @@ -110,16 +110,16 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al plusminus = " +/- " fmt = "%." + str(precision) + "g%s%s%s%s" return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal)) - + def siEval(s): """ Convert a value written in SI notation to its equivalent prefixless value - + Example:: - + siEval("100 μV") # returns 0.0001 """ - + s = asUnicode(s) m = re.match(r'(-?((\d+(\.\d*)?)|(\.\d+))([eE]-?\d+)?)\s*([u' + SI_PREFIXES + r']?).*$', s) if m is None: @@ -135,35 +135,35 @@ def siEval(s): else: n = SI_PREFIXES.index(p) - 8 return v * 1000**n - + class Color(QtGui.QColor): def __init__(self, *args): QtGui.QColor.__init__(self, mkColor(*args)) - + def glColor(self): """Return (r,g,b,a) normalized for use in opengl""" return (self.red()/255., self.green()/255., self.blue()/255., self.alpha()/255.) - + def __getitem__(self, ind): return (self.red, self.green, self.blue, self.alpha)[ind]() - - + + def mkColor(*args): """ Convenience function for constructing QColor from a variety of argument types. Accepted arguments are: - + ================ ================================================ - 'c' one of: r, g, b, c, m, y, k, w + 'c' one of: r, g, b, c, m, y, k, w R, G, B, [A] integers 0-255 (R, G, B, [A]) tuple of integers 0-255 float greyscale, 0.0-1.0 int see :func:`intColor() ` (int, hues) see :func:`intColor() ` "RGB" hexadecimal strings; may begin with '#' - "RGBA" - "RRGGBB" - "RRGGBBAA" + "RGBA" + "RRGGBB" + "RRGGBBAA" QColor QColor instance; makes a copy. ================ ================================================ """ @@ -221,7 +221,7 @@ def mkColor(*args): (r, g, b, a) = args else: raise Exception(err) - + args = [r,g,b,a] args = [0 if np.isnan(a) or np.isinf(a) else a for a in args] args = list(map(int, args)) @@ -250,25 +250,25 @@ def mkBrush(*args, **kwds): def mkPen(*args, **kargs): """ - Convenience function for constructing QPen. - + Convenience function for constructing QPen. + Examples:: - + mkPen(color) mkPen(color, width=2) mkPen(cosmetic=False, width=4.5, color='r') mkPen({'color': "FF0", width: 2}) mkPen(None) # (no pen) - + In these examples, *color* may be replaced with any arguments accepted by :func:`mkColor() ` """ - + color = kargs.get('color', None) width = kargs.get('width', 1) style = kargs.get('style', None) dash = kargs.get('dash', None) cosmetic = kargs.get('cosmetic', True) hsv = kargs.get('hsv', None) - + if len(args) == 1: arg = args[0] if isinstance(arg, dict): @@ -281,14 +281,14 @@ def mkPen(*args, **kargs): color = arg if len(args) > 1: color = args - + if color is None: color = mkColor(200, 200, 200) if hsv is not None: color = hsvColor(*hsv) else: color = mkColor(color) - + pen = QtGui.QPen(QtGui.QBrush(color), width) pen.setCosmetic(cosmetic) if style is not None: @@ -303,7 +303,7 @@ def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0): c.setHsvF(hue, sat, val, alpha) return c - + def colorTuple(c): """Return a tuple (R,G,B,A) from a QColor""" return (c.red(), c.green(), c.blue(), c.alpha()) @@ -315,10 +315,10 @@ def colorStr(c): def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs): """ Creates a QColor from a single index. Useful for stepping through a predefined list of colors. - + The argument *index* determines which color from the set will be returned. All other arguments determine what the set of predefined colors will be - - Colors are chosen by cycling across hues while varying the value (brightness). + + Colors are chosen by cycling across hues while varying the value (brightness). By default, this selects from a list of 9 hues.""" hues = int(hues) values = int(values) @@ -330,7 +330,7 @@ def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, mi else: v = maxValue h = minHue + (indh * (maxHue-minHue)) / hues - + c = QtGui.QColor() c.setHsv(h, sat, v) c.setAlpha(alpha) @@ -344,7 +344,7 @@ def glColor(*args, **kargs): c = mkColor(*args, **kargs) return (c.red()/255., c.green()/255., c.blue()/255., c.alpha()/255.) - + def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0): """ @@ -370,25 +370,25 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0) path.lineTo(headLen, headWidth) path.lineTo(0,0) return path - - - + + + def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs): """ Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data. - + The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates (see the scipy documentation for more information about this). - + For a graphical interface to this function, see :func:`ROI.getArrayRegion ` - + ============== ==================================================================================================== Arguments: *data* (ndarray) the original dataset *shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape)) *origin* the location in the original dataset that will become the origin of the sliced data. - *vectors* list of unit vectors which point in the direction of the slice axes. Each vector must have the same - length as *axes*. If the vectors are not unit length, the result will be scaled relative to the - original data. If the vectors are not orthogonal, the result will be sheared relative to the + *vectors* list of unit vectors which point in the direction of the slice axes. Each vector must have the same + length as *axes*. If the vectors are not unit length, the result will be scaled relative to the + original data. If the vectors are not orthogonal, the result will be sheared relative to the original data. *axes* The axes in the original dataset which correspond to the slice *vectors* *order* The order of spline interpolation. Default is 1 (linear). See scipy.ndimage.map_coordinates @@ -398,23 +398,23 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, *All extra keyword arguments are passed to scipy.ndimage.map_coordinates.* -------------------------------------------------------------------------------------------------------------------- ============== ==================================================================================================== - - Note the following must be true: - - | len(shape) == len(vectors) + + Note the following must be true: + + | len(shape) == len(vectors) | len(origin) == len(axes) == len(vectors[i]) - + Example: start with a 4D fMRI data set, take a diagonal-planar slice out of the last 3 axes - + * data = array with dims (time, x, y, z) = (100, 40, 40, 40) - * The plane to pull out is perpendicular to the vector (x,y,z) = (1,1,1) + * The plane to pull out is perpendicular to the vector (x,y,z) = (1,1,1) * The origin of the slice will be at (x,y,z) = (40, 0, 0) * We will slice a 20x20 plane from each timepoint, giving a final shape (100, 20, 20) - + The call for this example would look like:: - + affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) - + """ if not HAVE_SCIPY: raise Exception("This function requires the scipy library, but it does not appear to be importable.") @@ -427,7 +427,7 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, for v in vectors: if len(v) != len(axes): raise Exception("each vector must be same length as axes.") - + shape = list(map(np.ceil, shape)) ## transpose data so slice axes come first @@ -438,7 +438,7 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, data = data.transpose(tr1) #print "tr1:", tr1 ## dims are now [(slice axes), (other axes)] - + ## make sure vectors are arrays if not isinstance(vectors, np.ndarray): @@ -446,8 +446,8 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, if not isinstance(origin, np.ndarray): origin = np.array(origin) origin.shape = (len(axes),) + (1,)*len(shape) - - ## Build array of sample locations. + + ## Build array of sample locations. grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes #print shape, grid.shape x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic @@ -461,7 +461,7 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, ind = (Ellipsis,) + inds #print data[ind].shape, x.shape, output[ind].shape, output.shape output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs) - + tr = list(range(output.ndim)) trb = [] for i in range(min(axes)): @@ -481,20 +481,20 @@ def transformToArray(tr): """ Given a QTransform, return a 3x3 numpy array. Given a QMatrix4x4, return a 4x4 numpy array. - + Example: map an array of x,y coordinates through a transform:: - + ## coordinates to map are (1,5), (2,6), (3,7), and (4,8) coords = np.array([[1,2,3,4], [5,6,7,8], [1,1,1,1]]) # the extra '1' coordinate is needed for translation to work - + ## Make an example transform tr = QtGui.QTransform() tr.translate(3,4) tr.scale(2, 0.1) - + ## convert to array m = pg.transformToArray()[:2] # ignore the perspective portion of the transformation - + ## map coordinates through transform mapped = np.dot(m, coords) """ @@ -515,24 +515,24 @@ def transformCoordinates(tr, coords, transpose=False): Map a set of 2D or 3D coordinates through a QTransform or QMatrix4x4. The shape of coords must be (2,...) or (3,...) The mapping will _ignore_ any perspective transformations. - + For coordinate arrays with ndim=2, this is basically equivalent to matrix multiplication. - Most arrays, however, prefer to put the coordinate axis at the end (eg. shape=(...,3)). To + Most arrays, however, prefer to put the coordinate axis at the end (eg. shape=(...,3)). To allow this, use transpose=True. - + """ - + if transpose: ## move last axis to beginning. This transposition will be reversed before returning the mapped coordinates. coords = coords.transpose((coords.ndim-1,) + tuple(range(0,coords.ndim-1))) - + nd = coords.shape[0] if isinstance(tr, np.ndarray): m = tr else: m = transformToArray(tr) m = m[:m.shape[0]-1] # remove perspective - + ## If coords are 3D and tr is 2D, assume no change for Z axis if m.shape == (2,3) and nd == 3: m2 = np.zeros((3,4)) @@ -540,34 +540,34 @@ def transformCoordinates(tr, coords, transpose=False): m2[:2, 3] = m[:2,2] m2[2,2] = 1 m = m2 - + ## if coords are 2D and tr is 3D, ignore Z axis if m.shape == (3,4) and nd == 2: m2 = np.empty((2,3)) m2[:,:2] = m[:2,:2] m2[:,2] = m[:2,3] m = m2 - + ## reshape tr and coords to prepare for multiplication m = m.reshape(m.shape + (1,)*(coords.ndim-1)) coords = coords[np.newaxis, ...] - - # separate scale/rotate and translation - translate = m[:,-1] + + # separate scale/rotate and translation + translate = m[:,-1] m = m[:, :-1] - + ## map coordinates and return mapped = (m*coords).sum(axis=1) ## apply scale/rotate mapped += translate - + if transpose: ## move first axis to end. mapped = mapped.transpose(tuple(range(1,mapped.ndim)) + (0,)) return mapped + + - - - + def solve3DTransform(points1, points2): """ Find a 3D transformation matrix that maps points1 onto points2. @@ -577,21 +577,21 @@ def solve3DTransform(points1, points2): raise Exception("This function depends on the scipy library, but it does not appear to be importable.") A = np.array([[points1[i].x(), points1[i].y(), points1[i].z(), 1] for i in range(4)]) B = np.array([[points2[i].x(), points2[i].y(), points2[i].z(), 1] for i in range(4)]) - + ## solve 3 sets of linear equations to determine transformation matrix elements matrix = np.zeros((4,4)) for i in range(3): matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix - + return matrix - + def solveBilinearTransform(points1, points2): """ Find a bilinear transformation matrix (2x4) that maps points1 onto points2. Points must be specified as a list of 4 Vector, Point, QPointF, etc. - + To use this matrix to map a point [x,y]:: - + mapped = np.dot(matrix, [x*y, x, y, 1]) """ if not HAVE_SCIPY: @@ -600,30 +600,30 @@ def solveBilinearTransform(points1, points2): ## B is 4 rows (points) x 2 columns (x, y) A = np.array([[points1[i].x()*points1[i].y(), points1[i].x(), points1[i].y(), 1] for i in range(4)]) B = np.array([[points2[i].x(), points2[i].y()] for i in range(4)]) - + ## solve 2 sets of linear equations to determine transformation matrix elements matrix = np.zeros((2,4)) for i in range(2): matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix - + return matrix - + def rescaleData(data, scale, offset, dtype=None): """Return data rescaled and optionally cast to a new dtype:: - + data => (data-offset) * scale - + Uses scipy.weave (if available) to improve performance. """ if dtype is None: dtype = data.dtype else: dtype = np.dtype(dtype) - + try: if not pg.getConfigOption('useWeave'): raise Exception('Weave is disabled; falling back to slower version.') - + ## require native dtype when using weave if not data.dtype.isnative: data = data.astype(data.dtype.newbyteorder('=')) @@ -631,11 +631,11 @@ def rescaleData(data, scale, offset, dtype=None): weaveDtype = dtype.newbyteorder('=') else: weaveDtype = dtype - + newData = np.empty((data.size,), dtype=weaveDtype) flat = np.ascontiguousarray(data).reshape(data.size) size = data.size - + code = """ double sc = (double)scale; double off = (double)offset; @@ -652,61 +652,61 @@ def rescaleData(data, scale, offset, dtype=None): if pg.getConfigOption('weaveDebug'): debug.printExc("Error; disabling weave.") pg.setConfigOption('useWeave', False) - + #p = np.poly1d([scale, -offset*scale]) #data = p(data).astype(dtype) d2 = data-offset d2 *= scale data = d2.astype(dtype) return data - + def applyLookupTable(data, lut): """ Uses values in *data* as indexes to select values from *lut*. The returned data has shape data.shape + lut.shape[1:] - + Uses scipy.weave to improve performance if it is available. Note: color gradient lookup tables can be generated using GradientWidget. """ if data.dtype.kind not in ('i', 'u'): data = data.astype(int) - + ## using np.take appears to be faster than even the scipy.weave method and takes care of clipping as well. - return np.take(lut, data, axis=0, mode='clip') - - ### old methods: + return np.take(lut, data, axis=0, mode='clip') + + ### old methods: #data = np.clip(data, 0, lut.shape[0]-1) - + #try: #if not USE_WEAVE: #raise Exception('Weave is disabled; falling back to slower version.') - + ### number of values to copy for each LUT lookup #if lut.ndim == 1: #ncol = 1 #else: #ncol = sum(lut.shape[1:]) - + ### output array #newData = np.empty((data.size, ncol), dtype=lut.dtype) - + ### flattened input arrays #flatData = data.flatten() #flatLut = lut.reshape((lut.shape[0], ncol)) - + #dataSize = data.size - - ### strides for accessing each item + + ### strides for accessing each item #newStride = newData.strides[0] / newData.dtype.itemsize #lutStride = flatLut.strides[0] / flatLut.dtype.itemsize #dataStride = flatData.strides[0] / flatData.dtype.itemsize - + ### strides for accessing individual values within a single LUT lookup #newColStride = newData.strides[1] / newData.dtype.itemsize #lutColStride = flatLut.strides[1] / flatLut.dtype.itemsize - + #code = """ - + #for( int i=0; i0 and max->*scale*:: - + rescaled = (clip(data, min, max) - min) * (*scale* / (max - min)) - + It is also possible to use a 2D (N,2) array of values for levels. In this case, - it is assumed that each pair of min,max values in the levels array should be - applied to a different subset of the input data (for example, the input data may - already have RGB values and the levels are used to independently scale each + it is assumed that each pair of min,max values in the levels array should be + applied to a different subset of the input data (for example, the input data may + already have RGB values and the levels are used to independently scale each channel). The use of this feature requires that levels.shape[0] == data.shape[-1]. - scale The maximum value to which data will be rescaled before being passed through the + scale The maximum value to which data will be rescaled before being passed through the lookup table (or returned if there is no lookup table). By default this will be set to the length of the lookup table, or 256 is no lookup table is provided. For OpenGL color specifications (as in GLColor4f) use scale=1.0 lut Optional lookup table (array with dtype=ubyte). Values in data will be converted to color by indexing directly from lut. The output data shape will be input.shape + lut.shape[1:]. - + Note: the output of makeARGB will have the same dtype as the lookup table, so for conversion to QImage, the dtype must be ubyte. - + Lookup tables can be built using GradientWidget. - useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures). - The default is False, which returns in ARGB order for use with QImage - (Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order + useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures). + The default is False, which returns in ARGB order for use with QImage + (Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order is BGRA). transpose Whether to pre-transpose the data in preparation for use in Qt ============ ================================================================================== """ prof = debug.Profiler('functions.makeARGB', disabled=True) - + if lut is not None and not isinstance(lut, np.ndarray): lut = np.array(lut) if levels is not None and not isinstance(levels, np.ndarray): levels = np.array(levels) - + ## sanity checks #if data.ndim == 3: #if data.shape[2] not in (3,4): @@ -792,7 +792,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, transpose=F ##raise Exception("can not use lookup table with 3D data") #elif data.ndim != 2: #raise Exception("data must be 2D or 3D") - + #if lut is not None: ##if lut.ndim == 2: ##if lut.shape[1] : @@ -801,7 +801,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, transpose=F ##raise Exception("lut must be 1D or 2D") #if lut.dtype != np.ubyte: #raise Exception('lookup table must have dtype=ubyte (got %s instead)' % str(lut.dtype)) - + if levels is not None: if levels.ndim == 1: @@ -825,7 +825,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, transpose=F #raise Exception('Can not use 2D levels and lookup table together.') #else: #raise Exception("Levels must have shape (2,) or (3,2) or (4,2)") - + prof.mark('1') if scale is None: @@ -836,7 +836,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, transpose=F ## Apply levels if given if levels is not None: - + if isinstance(levels, np.ndarray) and levels.ndim == 2: ## we are going to rescale each channel independently if levels.shape[0] != data.shape[-1]: @@ -877,7 +877,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, transpose=F order = [0,1,2,3] ## array comes out RGBA else: order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. - + if data.ndim == 2: for i in range(3): if transpose: @@ -895,21 +895,21 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, transpose=F if transpose: imgData[..., order[i]] = data[..., order[i]].T else: - imgData[..., order[i]] = data[..., order[i]] - + imgData[..., order[i]] = data[..., order[i]] + prof.mark('5') - + if data.ndim == 2 or data.shape[2] == 3: alpha = False imgData[..., 3] = 255 else: alpha = True - + prof.mark('6') - + prof.finish() return imgData, alpha - + def makeQImage(imgData, alpha=None, copy=True, transpose=True): """ @@ -918,11 +918,11 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): be reflected in the image. The image will be given a 'data' attribute pointing to the array which shares its data to prevent python freeing that memory while the image is in use. - + =========== =================================================================== Arguments: - imgData Array of data to convert. Must have shape (width, height, 3 or 4) - and dtype=ubyte. The order of values in the 3rd axis must be + imgData Array of data to convert. Must have shape (width, height, 3 or 4) + and dtype=ubyte. The order of values in the 3rd axis must be (b, g, r, a). alpha If True, the QImage returned will have format ARGB32. If False, the format will be RGB32. By default, _alpha_ is True if @@ -931,19 +931,19 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): If False, the new QImage points directly to the data in the array. Note that the array must be contiguous for this to work (see numpy.ascontiguousarray). - transpose If True (the default), the array x/y axes are transposed before - creating the image. Note that Qt expects the axes to be in - (height, width) order whereas pyqtgraph usually prefers the + transpose If True (the default), the array x/y axes are transposed before + creating the image. Note that Qt expects the axes to be in + (height, width) order whereas pyqtgraph usually prefers the opposite. - =========== =================================================================== + =========== =================================================================== """ ## create QImage from buffer prof = debug.Profiler('functions.makeQImage', disabled=True) - + ## If we didn't explicitly specify alpha, check the array shape. if alpha is None: alpha = (imgData.shape[2] == 4) - + copied = False if imgData.shape[2] == 3: ## need to make alpha channel (even if alpha==False; QImage requires 32 bpp) if copy is True: @@ -954,25 +954,25 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): copied = True else: raise Exception('Array has only 3 channels; cannot make QImage without copying.') - + if alpha: imgFormat = QtGui.QImage.Format_ARGB32 else: imgFormat = QtGui.QImage.Format_RGB32 - + if transpose: imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite - + if not imgData.flags['C_CONTIGUOUS']: if copy is False: extra = ' (try setting transpose=False)' if transpose else '' raise Exception('Array is not contiguous; cannot make QImage without copying.'+extra) imgData = np.ascontiguousarray(imgData) copied = True - + if copy is True and copied is False: imgData = imgData.copy() - + if USE_PYSIDE: ch = ctypes.c_char.from_buffer(imgData, 0) img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) @@ -983,7 +983,7 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): #addr = ctypes.c_char.from_buffer(imgData, 0) #try: #img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) - #except TypeError: + #except TypeError: #addr = ctypes.addressof(addr) #img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) try: @@ -995,14 +995,14 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): else: # mutable, but leaks memory img = QtGui.QImage(memoryview(imgData), imgData.shape[1], imgData.shape[0], imgFormat) - + img.data = imgData return img #try: #buf = imgData.data #except AttributeError: ## happens when image data is non-contiguous #buf = imgData.data - + #prof.mark('1') #qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat) #prof.mark('2') @@ -1013,7 +1013,7 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): def imageToArray(img, copy=False, transpose=True): """ Convert a QImage into numpy array. The image must have format RGB32, ARGB32, or ARGB32_Premultiplied. - By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if + By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if the QImage is collected before the array, there may be trouble). The array will have shape (width, height, (b,g,r,a)). """ @@ -1024,34 +1024,34 @@ def imageToArray(img, copy=False, transpose=True): else: ptr.setsize(img.byteCount()) arr = np.asarray(ptr) - + if fmt == img.Format_RGB32: arr = arr.reshape(img.height(), img.width(), 3) elif fmt == img.Format_ARGB32 or fmt == img.Format_ARGB32_Premultiplied: arr = arr.reshape(img.height(), img.width(), 4) - + if copy: arr = arr.copy() - + if transpose: return arr.transpose((1,0,2)) else: return arr - + def colorToAlpha(data, color): """ - Given an RGBA image in *data*, convert *color* to be transparent. - *data* must be an array (w, h, 3 or 4) of ubyte values and *color* must be + Given an RGBA image in *data*, convert *color* to be transparent. + *data* must be an array (w, h, 3 or 4) of ubyte values and *color* must be an array (3) of ubyte values. This is particularly useful for use with images that have a black or white background. - + Algorithm is taken from Gimp's color-to-alpha function in plug-ins/common/colortoalpha.c Credit: /* * Color To Alpha plug-in v1.0 by Seth Burgess, sjburges@gimp.org 1999/05/14 * with algorithm by clahey */ - + """ data = data.astype(float) if data.shape[-1] == 3: ## add alpha channel if needed @@ -1059,11 +1059,11 @@ def colorToAlpha(data, color): d2[...,:3] = data d2[...,3] = 255 data = d2 - + color = color.astype(float) alpha = np.zeros(data.shape[:2]+(3,), dtype=float) output = data.copy() - + for i in [0,1,2]: d = data[...,i] c = color[i] @@ -1071,18 +1071,18 @@ def colorToAlpha(data, color): alpha[...,i][mask] = (d[mask] - c) / (255. - c) imask = d < c alpha[...,i][imask] = (c - d[imask]) / c - + output[...,3] = alpha.max(axis=2) * 255. - + mask = output[...,3] >= 1.0 ## avoid zero division while processing alpha channel correction = 255. / output[...,3][mask] ## increase value to compensate for decreased alpha for i in [0,1,2]: output[...,i][mask] = ((output[...,i][mask]-color[i]) * correction) + color[i] output[...,3][mask] *= data[...,3][mask] / 255. ## combine computed and previous alpha values - + #raise Exception() return np.clip(output, 0, 255).astype(np.ubyte) - + def arrayToQPath(x, y, connect='all'): @@ -1184,28 +1184,28 @@ def arrayToQPath(x, y, connect='all'): #""" #Generate isosurface from volumetric data using marching tetrahedra algorithm. #See Paul Bourke, "Polygonising a Scalar Field Using Tetrahedrons" (http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/) - + #*data* 3D numpy array of scalar values #*level* The level at which to generate an isosurface #""" - + #facets = [] - + ### mark everything below the isosurface level #mask = data < level - - #### make eight sub-fields + + #### make eight sub-fields #fields = np.empty((2,2,2), dtype=object) #slices = [slice(0,-1), slice(1,None)] #for i in [0,1]: #for j in [0,1]: #for k in [0,1]: #fields[i,j,k] = mask[slices[i], slices[j], slices[k]] - - - + + + ### split each cell into 6 tetrahedra - ### these all have the same 'orienation'; points 1,2,3 circle + ### these all have the same 'orienation'; points 1,2,3 circle ### clockwise around point 0 #tetrahedra = [ #[(0,1,0), (1,1,1), (0,1,1), (1,0,1)], @@ -1215,15 +1215,15 @@ def arrayToQPath(x, y, connect='all'): #[(0,1,0), (1,0,0), (1,1,0), (1,0,1)], #[(0,1,0), (1,1,0), (1,1,1), (1,0,1)] #] - + ### each tetrahedron will be assigned an index ### which determines how to generate its facets. - ### this structure is: + ### this structure is: ### facets[index][facet1, facet2, ...] - ### where each facet is triangular and its points are each + ### where each facet is triangular and its points are each ### interpolated between two points on the tetrahedron ### facet = [(p1a, p1b), (p2a, p2b), (p3a, p3b)] - ### facet points always circle clockwise if you are looking + ### facet points always circle clockwise if you are looking ### at them from below the isosurface. #indexFacets = [ #[], ## all above @@ -1243,15 +1243,15 @@ def arrayToQPath(x, y, connect='all'): #[[(0,1), (0,3), (0,2)]], # 1,2,3 below #[] ## all below #] - + #for tet in tetrahedra: - + ### get the 4 fields for this tetrahedron #tetFields = [fields[c] for c in tet] - + ### generate an index for each grid cell #index = tetFields[0] + tetFields[1]*2 + tetFields[2]*4 + tetFields[3]*8 - + ### add facets #for i in xrange(index.shape[0]): # data x-axis #for j in xrange(index.shape[1]): # data y-axis @@ -1265,32 +1265,32 @@ def arrayToQPath(x, y, connect='all'): #facets.append(pts) #return facets - + def isocurve(data, level, connected=False, extendToEdge=False, path=False): """ Generate isocurve from 2D data using marching squares algorithm. - + ============= ========================================================= Arguments data 2D numpy array of scalar values level The level at which to generate an isosurface connected If False, return a single long list of point pairs - If True, return multiple long lists of connected point - locations. (This is slower but better for drawing + If True, return multiple long lists of connected point + locations. (This is slower but better for drawing continuous lines) - extendToEdge If True, extend the curves to reach the exact edges of - the data. - path if True, return a QPainterPath rather than a list of + extendToEdge If True, extend the curves to reach the exact edges of + the data. + path if True, return a QPainterPath rather than a list of vertex coordinates. This forces connected=True. ============= ========================================================= - + This function is SLOW; plenty of room for optimization here. - """ - + """ + if path is True: connected = True - + if extendToEdge: d2 = np.empty((data.shape[0]+2, data.shape[1]+2), dtype=data.dtype) d2[1:-1, 1:-1] = data @@ -1303,7 +1303,7 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): d2[-1,0] = d2[-1,1] d2[-1,-1] = d2[-1,-2] data = d2 - + sideTable = [ [], [0,1], @@ -1322,20 +1322,20 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): [0,1], [] ] - + edgeKey=[ [(0,1), (0,0)], [(0,0), (1,0)], [(1,0), (1,1)], [(1,1), (0,1)] ] - - + + lines = [] - + ## mark everything below the isosurface level mask = data < level - + ### make four sub-fields and compute indexes for grid cells index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) fields = np.empty((2,2), dtype=object) @@ -1349,10 +1349,10 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): index += fields[i,j] * 2**vertIndex #print index #print index - + ## add lines for i in range(index.shape[0]): # data x-axis - for j in range(index.shape[1]): # data y-axis + for j in range(index.shape[1]): # data y-axis sides = sideTable[index[i,j]] for l in range(0, len(sides), 2): ## faces for this grid cell edges = sides[l:l+2] @@ -1365,26 +1365,26 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): f = (level-v1) / (v2-v1) fi = 1.0 - f p = ( ## interpolate between corners - p1[0]*fi + p2[0]*f + i + 0.5, + p1[0]*fi + p2[0]*f + i + 0.5, p1[1]*fi + p2[1]*f + j + 0.5 ) if extendToEdge: ## check bounds p = ( min(data.shape[0]-2, max(0, p[0]-1)), - min(data.shape[1]-2, max(0, p[1]-1)), + min(data.shape[1]-2, max(0, p[1]-1)), ) if connected: gridKey = i + (1 if edges[m]==2 else 0), j + (1 if edges[m]==3 else 0), edges[m]%2 pts.append((p, gridKey)) ## give the actual position and a key identifying the grid location (for connecting segments) else: pts.append(p) - + lines.append(pts) if not connected: return lines - + ## turn disjoint list of segments into continuous lines #lines = [[2,5], [5,4], [3,4], [1,3], [6,7], [7,8], [8,6], [11,12], [12,15], [11,13], [13,14]] @@ -1411,9 +1411,9 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): while True: if x == chain[-1][1]: break ## nothing left to do on this chain - + x = chain[-1][1] - if x == k: + if x == k: break ## chain has looped; we're done and can ignore the opposite chain y = chain[-2][1] connects = points[x] @@ -1426,9 +1426,9 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): if chain[0][1] == chain[-1][1]: # looped chain; no need to continue the other direction chains.pop() break + - - ## extract point locations + ## extract point locations lines = [] for chain in points.values(): if len(chain) == 2: @@ -1436,25 +1436,25 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): else: chain = chain[0] lines.append([p[0] for p in chain]) - + if not path: return lines ## a list of pairs of points - + path = QtGui.QPainterPath() for line in lines: path.moveTo(*line[0]) for p in line[1:]: path.lineTo(*p) - + return path - - + + def traceImage(image, values, smooth=0.5): """ Convert an image to a set of QPainterPath curves. One curve will be generated for each item in *values*; each curve outlines the area of the image that is closer to its value than to any others. - + If image is RGB or RGBA, then the shape of values should be (nvals, 3/4) The parameter *smooth* is expressed in pixels. """ @@ -1466,11 +1466,11 @@ def traceImage(image, values, smooth=0.5): diff = np.abs(image-values) if values.ndim == 4: diff = diff.sum(axis=2) - + labels = np.argmin(diff, axis=2) - + paths = [] - for i in range(diff.shape[-1]): + for i in range(diff.shape[-1]): d = (labels==i).astype(float) d = ndi.gaussian_filter(d, (smooth, smooth)) lines = isocurve(d, 0.5, connected=True, extendToEdge=True) @@ -1479,31 +1479,31 @@ def traceImage(image, values, smooth=0.5): path.moveTo(*line[0]) for p in line[1:]: path.lineTo(*p) - + paths.append(path) return paths - - - + + + IsosurfaceDataCache = None def isosurface(data, level): """ Generate isosurface from volumetric data using marching cubes algorithm. - See Paul Bourke, "Polygonising a Scalar Field" + See Paul Bourke, "Polygonising a Scalar Field" (http://paulbourke.net/geometry/polygonise/) - + *data* 3D numpy array of scalar values *level* The level at which to generate an isosurface - - Returns an array of vertex coordinates (Nv, 3) and an array of - per-face vertex indexes (Nf, 3) + + Returns an array of vertex coordinates (Nv, 3) and an array of + per-face vertex indexes (Nf, 3) """ ## For improvement, see: - ## + ## ## Efficient implementation of Marching Cubes' cases with topological guarantees. ## Thomas Lewiner, Helio Lopes, Antonio Wilson Vieira and Geovan Tavares. ## Journal of Graphics Tools 8(2): pp. 1-15 (december 2003) - + ## Precompute lookup tables on the first run global IsosurfaceDataCache if IsosurfaceDataCache is None: @@ -1544,7 +1544,7 @@ def isosurface(data, level): 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190, 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ], dtype=np.uint16) - + ## Table of triangles to use for filling each grid cell. ## Each set of three integers tells us which three edges to ## draw a triangle between. @@ -1806,9 +1806,9 @@ def isosurface(data, level): [0, 9, 1], [0, 3, 8], [] - ] + ] edgeShifts = np.array([ ## maps edge ID (0-11) to (x,y,z) cell offset and edge ID (0-2) - [0, 0, 0, 0], + [0, 0, 0, 0], [1, 0, 0, 1], [0, 1, 0, 0], [0, 0, 0, 1], @@ -1831,23 +1831,23 @@ def isosurface(data, level): faceTableI[faceTableInds[:,0]] = np.array([triTable[j] for j in faceTableInds]) faceTableI = faceTableI.reshape((len(triTable), i, 3)) faceShiftTables.append(edgeShifts[faceTableI]) - + ## Let's try something different: #faceTable = np.empty((256, 5, 3, 4), dtype=np.ubyte) # (grid cell index, faces, vertexes, edge lookup) #for i,f in enumerate(triTable): #f = np.array(f + [12] * (15-len(f))).reshape(5,3) #faceTable[i] = edgeShifts[f] - - + + IsosurfaceDataCache = (faceShiftTables, edgeShifts, edgeTable, nTableFaces) else: faceShiftTables, edgeShifts, edgeTable, nTableFaces = IsosurfaceDataCache - + ## mark everything below the isosurface level mask = data < level - + ### make eight sub-fields and compute indexes for grid cells index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) fields = np.empty((2,2,2), dtype=object) @@ -1858,23 +1858,23 @@ def isosurface(data, level): fields[i,j,k] = mask[slices[i], slices[j], slices[k]] vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme index += fields[i,j,k] * 2**vertIndex - + ### Generate table of edges that have been cut cutEdges = np.zeros([x+1 for x in index.shape]+[3], dtype=np.uint32) edges = edgeTable[index] - for i, shift in enumerate(edgeShifts[:12]): + for i, shift in enumerate(edgeShifts[:12]): slices = [slice(shift[j],cutEdges.shape[j]+(shift[j]-1)) for j in range(3)] cutEdges[slices[0], slices[1], slices[2], shift[3]] += edges & 2**i - + ## for each cut edge, interpolate to see where exactly the edge is cut and generate vertex positions m = cutEdges > 0 vertexInds = np.argwhere(m) ## argwhere is slow! vertexes = vertexInds[:,:3].astype(np.float32) dataFlat = data.reshape(data.shape[0]*data.shape[1]*data.shape[2]) - + ## re-use the cutEdges array as a lookup table for vertex IDs cutEdges[vertexInds[:,0], vertexInds[:,1], vertexInds[:,2], vertexInds[:,3]] = np.arange(vertexInds.shape[0]) - + for i in [0,1,2]: vim = vertexInds[:,3] == i vi = vertexInds[vim, :3] @@ -1882,9 +1882,9 @@ def isosurface(data, level): v1 = dataFlat[viFlat] v2 = dataFlat[viFlat + data.strides[i]//data.itemsize] vertexes[vim,i] += (level-v1) / (v2-v1) - - ### compute the set of vertex indexes for each face. - + + ### compute the set of vertex indexes for each face. + ## This works, but runs a bit slower. #cells = np.argwhere((index != 0) & (index != 255)) ## all cells with at least one face #cellInds = index[cells[:,0], cells[:,1], cells[:,2]] @@ -1893,9 +1893,9 @@ def isosurface(data, level): #verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges #verts = verts[mask] #faces = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want. - - - ## To allow this to be vectorized efficiently, we count the number of faces in each + + + ## To allow this to be vectorized efficiently, we count the number of faces in each ## grid cell and handle each group of cells with the same number together. ## determine how many faces to assign to each grid cell nFaces = nTableFaces[index] @@ -1904,7 +1904,7 @@ def isosurface(data, level): ptr = 0 #import debug #p = debug.Profiler('isosurface', disabled=False) - + ## this helps speed up an indexing operation later on cs = np.array(cutEdges.strides)//cutEdges.itemsize cutEdges = cutEdges.flatten() @@ -1923,14 +1923,14 @@ def isosurface(data, level): #cellInds = index[(cells*ins[np.newaxis,:]).sum(axis=1)] cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round #p.mark('3') - + ### expensive: verts = faceShiftTables[i][cellInds] #p.mark('4') verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges verts = verts.reshape((verts.shape[0]*i,)+verts.shape[2:]) #p.mark('5') - + ### expensive: #print verts.shape verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2) @@ -1942,15 +1942,15 @@ def isosurface(data, level): faces[ptr:ptr+nv] = vertInds #.reshape((nv, 3)) #p.mark('8') ptr += nv - + return vertexes, faces - + def invertQTransform(tr): """Return a QTransform that is the inverse of *tr*. Rasises an exception if tr is not invertible. - + Note that this function is preferred over QTransform.inverted() due to bugs in that method. (specifically, Qt has floating-point precision issues when determining whether a matrix is invertible) @@ -1963,25 +1963,25 @@ def invertQTransform(tr): arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]]) inv = scipy.linalg.inv(arr) return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1]) - - + + def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): """ Used for examining the distribution of values in a set. Produces scattering as in beeswarm or column scatter plots. - + Given a list of x-values, construct a set of y-values such that an x,y scatter-plot will not have overlapping points (it will look similar to a histogram). """ inds = np.arange(len(data)) if shuffle: np.random.shuffle(inds) - + data = data[inds] - + if spacing is None: spacing = 2.*np.std(data)/len(data)**0.5 s2 = spacing**2 - + yvals = np.empty(len(data)) if len(data) == 0: return yvals @@ -1991,10 +1991,10 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): x0 = data[:i] # all x values already placed y0 = yvals[:i] # all y values already placed y = 0 - + dx = (x0-x)**2 # x-distance to each previous point xmask = dx < s2 # exclude anything too far away - + if xmask.sum() > 0: if bidir: dirs = [-1, 1] @@ -2004,24 +2004,24 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): for direction in dirs: y = 0 dx2 = dx[xmask] - dy = (s2 - dx2)**0.5 + dy = (s2 - dx2)**0.5 limits = np.empty((2,len(dy))) # ranges of y-values to exclude limits[0] = y0[xmask] - dy - limits[1] = y0[xmask] + dy + limits[1] = y0[xmask] + dy while True: # ignore anything below this y-value if direction > 0: mask = limits[1] >= y else: mask = limits[0] <= y - + limits2 = limits[:,mask] - + # are we inside an excluded region? mask = (limits2[0] < y) & (limits2[1] > y) if mask.sum() == 0: break - + if direction > 0: y = limits2[:,mask].max() else: @@ -2032,5 +2032,5 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): else: y = yopts[0] yvals[i] = y - + return yvals[np.argsort(inds)] ## un-shuffle values before returning From a08b28c958e8b36a5476e63081230266297fb6ab Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sun, 24 Nov 2013 15:50:28 -0600 Subject: [PATCH 018/268] Simplify to take transpose logic out of makeARGB function --- pyqtgraph/functions.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index bd2ed314..b1934d0a 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -733,7 +733,7 @@ def makeRGBA(*args, **kwds): kwds['useRGBA'] = True return makeARGB(*args, **kwds) -def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, transpose=False): +def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): """ Convert an array of values into an ARGB array suitable for building QImages, OpenGL textures, etc. @@ -774,7 +774,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, transpose=F The default is False, which returns in ARGB order for use with QImage (Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order is BGRA). - transpose Whether to pre-transpose the data in preparation for use in Qt ============ ================================================================================== """ prof = debug.Profiler('functions.makeARGB', disabled=True) @@ -866,10 +865,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, transpose=F ## copy data into ARGB ordered array - if transpose: - imgData = np.empty((data.shape[1], data.shape[0], 4), dtype=np.ubyte) - else: - imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte) + imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte) prof.mark('4') @@ -880,22 +876,13 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, transpose=F if data.ndim == 2: for i in range(3): - if transpose: - imgData[..., i] = data.T - else: - imgData[..., i] = data + imgData[..., i] = data elif data.shape[2] == 1: for i in range(3): - if transpose: - imgData[..., i] = data[..., 0].T - else: - imgData[..., i] = data[..., 0] + imgData[..., i] = data[..., 0] else: for i in range(0, data.shape[2]): - if transpose: - imgData[..., order[i]] = data[..., order[i]].T - else: - imgData[..., order[i]] = data[..., order[i]] + imgData[..., order[i]] = data[..., order[i]] prof.mark('5') From bd2330af9f4c38d7f50818f797d95ce874a318f0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 24 Nov 2013 20:45:10 -0500 Subject: [PATCH 019/268] ImageItem performance boost by avoiding makeQImage(transpose=True) --- pyqtgraph/functions.py | 3 +++ pyqtgraph/graphicsItems/ImageItem.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index b1934d0a..2930b0e7 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -875,6 +875,9 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. if data.ndim == 2: + # This is tempting: + # imgData[..., :3] = data[..., np.newaxis] + # ..but it turns out this is faster: for i in range(3): imgData[..., i] = data elif data.shape[2] == 1: diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 530db7fb..82807982 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -260,8 +260,8 @@ class ImageItem(GraphicsObject): #print lut.shape #print self.lut - argb, alpha = fn.makeARGB(self.image, lut=lut, levels=self.levels) - self.qimage = fn.makeQImage(argb, alpha) + argb, alpha = fn.makeARGB(self.image.T, lut=lut, levels=self.levels) + self.qimage = fn.makeQImage(argb, alpha, transpose=False) prof.finish() From 71ee4deb8402f635ddb785b15adca3b77277fb73 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 24 Nov 2013 21:10:06 -0500 Subject: [PATCH 020/268] - fixed ImageItem handling of rgb images - fixed makeARGB re-ordering of color channels --- pyqtgraph/functions.py | 31 +--------------------------- pyqtgraph/graphicsItems/ImageItem.py | 2 +- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 2930b0e7..2f10f85c 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -783,25 +783,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): if levels is not None and not isinstance(levels, np.ndarray): levels = np.array(levels) - ## sanity checks - #if data.ndim == 3: - #if data.shape[2] not in (3,4): - #raise Exception("data.shape[2] must be 3 or 4") - ##if lut is not None: - ##raise Exception("can not use lookup table with 3D data") - #elif data.ndim != 2: - #raise Exception("data must be 2D or 3D") - - #if lut is not None: - ##if lut.ndim == 2: - ##if lut.shape[1] : - ##raise Exception("lut.shape[1] must be 3 or 4") - ##elif lut.ndim != 1: - ##raise Exception("lut must be 1D or 2D") - #if lut.dtype != np.ubyte: - #raise Exception('lookup table must have dtype=ubyte (got %s instead)' % str(lut.dtype)) - - if levels is not None: if levels.ndim == 1: if len(levels) != 2: @@ -814,16 +795,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): else: print(levels) raise Exception("levels argument must be 1D or 2D.") - #levels = np.array(levels) - #if levels.shape == (2,): - #pass - #elif levels.shape in [(3,2), (4,2)]: - #if data.ndim == 3: - #raise Exception("Can not use 2D levels with 3D data.") - #if lut is not None: - #raise Exception('Can not use 2D levels and lookup table together.') - #else: - #raise Exception("Levels must have shape (2,) or (3,2) or (4,2)") prof.mark('1') @@ -885,7 +856,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): imgData[..., i] = data[..., 0] else: for i in range(0, data.shape[2]): - imgData[..., order[i]] = data[..., order[i]] + imgData[..., i] = data[..., order[i]] prof.mark('5') diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 82807982..5c4a09ef 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -260,7 +260,7 @@ class ImageItem(GraphicsObject): #print lut.shape #print self.lut - argb, alpha = fn.makeARGB(self.image.T, lut=lut, levels=self.levels) + argb, alpha = fn.makeARGB(self.image.transpose((1, 0, 2)[:self.image.ndim]), lut=lut, levels=self.levels) self.qimage = fn.makeQImage(argb, alpha, transpose=False) prof.finish() From 4486272737ca6b303c4bed4e6c79d2d2b20da4ae Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 25 Nov 2013 01:16:21 -0500 Subject: [PATCH 021/268] Catch OverflowError from Point.length() --- pyqtgraph/graphicsItems/PlotCurveItem.py | 10 ++++++++-- pyqtgraph/graphicsItems/ROI.py | 10 ++++++++-- pyqtgraph/graphicsItems/ScatterPlotItem.py | 10 ++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 28214552..5df78607 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -162,8 +162,14 @@ class PlotCurveItem(GraphicsObject): if pxPad > 0: # determine length of pixel in local x, y directions px, py = self.pixelVectors() - px = 0 if px is None else px.length() - py = 0 if py is None else py.length() + try: + px = 0 if px is None else px.length() + except OverflowError: + px = 0 + try: + py = 0 if py is None else py.length() + except OverflowError: + py = 0 # return bounds expanded by pixel size px *= pxPad diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index cb5f4f30..9ecc611b 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -664,7 +664,10 @@ class ROI(GraphicsObject): if not self.rotateAllowed: return ## If the handle is directly over its center point, we can't compute an angle. - if lp1.length() == 0 or lp0.length() == 0: + try: + if lp1.length() == 0 or lp0.length() == 0: + return + except OverflowError: return ## determine new rotation angle, constrained if necessary @@ -704,7 +707,10 @@ class ROI(GraphicsObject): else: scaleAxis = 0 - if lp1.length() == 0 or lp0.length() == 0: + try: + if lp1.length() == 0 or lp0.length() == 0: + return + except OverflowError: return ang = newState['angle'] - lp0.angle(lp1) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index f1a5201d..2e620f9f 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -652,8 +652,14 @@ class ScatterPlotItem(GraphicsObject): if pxPad > 0: # determine length of pixel in local x, y directions px, py = self.pixelVectors() - px = 0 if px is None else px.length() - py = 0 if py is None else py.length() + try: + px = 0 if px is None else px.length() + except OverflowError: + px = 0 + try: + py = 0 if py is None else py.length() + except OverflowError: + py = 0 # return bounds expanded by pixel size px *= pxPad From f136b330334aecbef4d37ebaf7b0f1c607241a9e Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 26 Nov 2013 22:16:13 -0800 Subject: [PATCH 022/268] Profilers controllable via PYQTGRAPHPROFILE. A new function profiling system is implemented. Most importantly, this allows one to profile various internal functions directly by setting the `PYQTGRAPHPROFILE` environment variable to a comma separated list of function and method names, e.g. PYQTGRAPHPROFILE=functions.makeARGB,ImageItem.render \ python -mexamples Specifically, items in `PYQTGRAPHPROFILE` must be of the form `classname.methodname` or `dotted_module_name.functionname`, with the initial "pyqtgraph." stripped from the dotted module name. Moreover, the overhead of inactive profilers has been kept minimal: an introspective check of the caller's name (only if `PYQTGRAPHPROFILE` is set) and a trivial function (not method) call per profiler call. The new profilers rely on `sys._getframe` to find the caller's name, although the previous system (passing the caller's name explicitely) could certainly have been kept instead. Finally the API of profilers has been changed: register a profiling point simply by calling the profiler, and profilers are automatically flushed on garbage collection. See the docstring of `pyqtgraph.debug.Profiler` for more details. --- pyqtgraph/debug.py | 163 ++++++++++--------- pyqtgraph/exporters/SVGExporter.py | 11 +- pyqtgraph/functions.py | 65 ++++---- pyqtgraph/graphicsItems/AxisItem.py | 33 ++-- pyqtgraph/graphicsItems/HistogramLUTItem.py | 9 +- pyqtgraph/graphicsItems/ImageItem.py | 42 +++-- pyqtgraph/graphicsItems/LinearRegionItem.py | 3 +- pyqtgraph/graphicsItems/PlotCurveItem.py | 24 ++- pyqtgraph/graphicsItems/PlotDataItem.py | 12 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 3 +- pyqtgraph/graphicsItems/ScatterPlotItem.py | 9 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 9 +- pyqtgraph/imageview/ImageView.py | 34 ++-- 13 files changed, 203 insertions(+), 214 deletions(-) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index a175be9c..0ee65a33 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -5,6 +5,8 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ +from __future__ import print_function + import sys, traceback, time, gc, re, types, weakref, inspect, os, cProfile from . import ptime from numpy import ndarray @@ -365,84 +367,99 @@ class GarbageWatcher(object): return self.objs[item] -class Profiler: + + +class Profiler(object): """Simple profiler allowing measurement of multiple time intervals. - Arguments: - msg: message to print at start and finish of profiling - disabled: If true, profiler does nothing (so you can leave it in place) - delayed: If true, all messages are printed after call to finish() - (this can result in more accurate time step measurements) - globalDelay: if True, all nested profilers delay printing until the top level finishes - + + By default, profilers are disabled. To enable profiling, set the + environment variable `PYQTGRAPHPROFILE` to a comma-separated list of + fully-qualified names of profiled functions. + + Calling a profiler registers a message (defaulting to an increasing + counter) that contains the time elapsed since the last call. When the + profiler is about to be garbage-collected, the messages are passed to the + outer profiler if one is running, or printed to stdout otherwise. + + If `delayed` is set to False, messages are immediately printed instead. + Example: - prof = Profiler('Function') - ... do stuff ... - prof.mark('did stuff') - ... do other stuff ... - prof.mark('did other stuff') - prof.finish() + def function(...): + profiler = Profiler() + ... do stuff ... + profiler('did stuff') + ... do other stuff ... + profiler('did other stuff') + # profiler is garbage-collected and flushed at function end + + If this function is a method of class C, setting `PYQTGRAPHPROFILE` to + "C.function" (without the module name) will enable this profiler. + + For regular functions, use the qualified name of the function, stripping + only the initial "pyqtgraph." prefix from the module. """ - depth = 0 - msgs = [] - - def __init__(self, msg="Profiler", disabled=False, delayed=True, globalDelay=True): - self.disabled = disabled - if disabled: - return - - self.markCount = 0 - self.finished = False - self.depth = Profiler.depth - Profiler.depth += 1 - if not globalDelay: - self.msgs = [] - self.delayed = delayed - self.msg = " "*self.depth + msg - msg2 = self.msg + " >>> Started" - if self.delayed: - self.msgs.append(msg2) - else: - print(msg2) - self.t0 = ptime.time() - self.t1 = self.t0 - - def mark(self, msg=None): - if self.disabled: - return - + + _profilers = os.environ.get("PYQTGRAPHPROFILE", "") + _depth = 0 + _msgs = [] + + if _profilers: + _profilers = _profilers.split(",") + def __new__(cls, delayed=True): + """Optionally create a new profiler based on caller's qualname. + """ + # determine the qualified name of the caller function + caller_frame = sys._getframe(1) + try: + caller_object_type = type(caller_frame.f_locals["self"]) + except KeyError: # we are in a regular function + qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1] + else: # we are in a method + qualifier = caller_object_type.__name__ + func_qualname = qualifier + "." + caller_frame.f_code.co_name + if func_qualname not in cls._profilers: # don't do anything + return lambda msg=None: None + # create an actual profiling object + cls._depth += 1 + obj = super(Profiler, cls).__new__(cls) + obj._name = func_qualname + obj._delayed = delayed + obj._markCount = 0 + obj._firstTime = obj._lastTime = ptime.time() + obj._newMsg("> Entering " + func_qualname) + return obj + else: + def __new__(cls, delayed=True): + return lambda msg=None: None + + def __call__(self, msg=None): + """Register or print a new message with timing information. + """ if msg is None: - msg = str(self.markCount) - self.markCount += 1 - - t1 = ptime.time() - msg2 = " "+self.msg+" "+msg+" "+"%gms" % ((t1-self.t1)*1000) - if self.delayed: - self.msgs.append(msg2) - else: - print(msg2) - self.t1 = ptime.time() ## don't measure time it took to print - - def finish(self, msg=None): - if self.disabled or self.finished: - return - - if msg is not None: - self.mark(msg) - t1 = ptime.time() - msg = self.msg + ' <<< Finished, total time: %gms' % ((t1-self.t0)*1000) - if self.delayed: - self.msgs.append(msg) - if self.depth == 0: - for line in self.msgs: - print(line) - Profiler.msgs = [] + msg = str(self._markCount) + self._markCount += 1 + newTime = ptime.time() + self._newMsg( + msg + ": " + str((newTime - self._lastTime) * 1000) + "ms") + self._lastTime = newTime + + def _newMsg(self, msg): + msg = " " * (self._depth - 1) + msg + if self._delayed: + self._msgs.append(msg) else: print(msg) - Profiler.depth = self.depth - self.finished = True - - - + + def __del__(self): + """Add a final message; flush the message list if no parent profiler. + """ + self._newMsg("< Exiting " + self._name + ", total time: " + + str((ptime.time() - self._firstTime) * 1000) + "ms") + type(self)._depth -= 1 + if not self._depth and self._msgs: + print("\n".join(self._msgs)) + type(self)._msgs = [] + def profile(code, name='profile_run', sort='cumulative', num=30): """Common-use for cProfile""" @@ -943,4 +960,4 @@ class PrintDetector(object): traceback.print_stack() def flush(self): - self.stdout.flush() \ No newline at end of file + self.stdout.flush() diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 62b49d30..19a7a6a7 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -156,7 +156,7 @@ def _generateItemSvg(item, nodes=None, root=None): ## ## Both 2 and 3 can be addressed by drawing all items in world coordinates. - prof = pg.debug.Profiler('generateItemSvg %s' % str(item), disabled=True) + profiler = pg.debug.Profiler() if nodes is None: ## nodes maps all node IDs to their XML element. ## this allows us to ensure all elements receive unique names. @@ -235,12 +235,12 @@ def _generateItemSvg(item, nodes=None, root=None): print(doc.toxml()) raise - prof.mark('render') + profiler('render') ## Get rid of group transformation matrices by applying ## transformation to inner coordinates correctCoordinates(g1, item) - prof.mark('correct') + profiler('correct') ## make sure g1 has the transformation matrix #m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32()) #g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m) @@ -290,7 +290,7 @@ def _generateItemSvg(item, nodes=None, root=None): childGroup = g1.ownerDocument.createElement('g') childGroup.setAttribute('clip-path', 'url(#%s)' % clip) g1.appendChild(childGroup) - prof.mark('clipping') + profiler('clipping') ## Add all child items as sub-elements. childs.sort(key=lambda c: c.zValue()) @@ -299,8 +299,7 @@ def _generateItemSvg(item, nodes=None, root=None): if cg is None: continue childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now) - prof.mark('children') - prof.finish() + profiler('children') return g1 def correctCoordinates(node, item): diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 3e16fe48..80e61404 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -775,7 +775,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): is BGRA). ============ ================================================================================== """ - prof = debug.Profiler('functions.makeARGB', disabled=True) + profile = debug.Profiler() if lut is not None and not isinstance(lut, np.ndarray): lut = np.array(lut) @@ -794,8 +794,8 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): else: print(levels) raise Exception("levels argument must be 1D or 2D.") - - prof.mark('1') + + profile() if scale is None: if lut is not None: @@ -822,8 +822,8 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): if minVal == maxVal: maxVal += 1e-16 data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int) - prof.mark('2') + profile() ## apply LUT if given if lut is not None: @@ -831,13 +831,13 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): else: if data.dtype is not np.ubyte: data = np.clip(data, 0, 255).astype(np.ubyte) - prof.mark('3') + profile() ## copy data into ARGB ordered array imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte) - prof.mark('4') + profile() if useRGBA: order = [0,1,2,3] ## array comes out RGBA @@ -857,7 +857,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): for i in range(0, data.shape[2]): imgData[..., i] = data[..., order[i]] - prof.mark('5') + profile() if data.ndim == 2 or data.shape[2] == 3: alpha = False @@ -865,11 +865,9 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): else: alpha = True - prof.mark('6') - - prof.finish() + profile() return imgData, alpha - + def makeQImage(imgData, alpha=None, copy=True, transpose=True): """ @@ -898,7 +896,7 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): =========== =================================================================== """ ## create QImage from buffer - prof = debug.Profiler('functions.makeQImage', disabled=True) + profile = debug.Profiler() ## If we didn't explicitly specify alpha, check the array shape. if alpha is None: @@ -922,7 +920,9 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): if transpose: imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite - + + profile() + if not imgData.flags['C_CONTIGUOUS']: if copy is False: extra = ' (try setting transpose=False)' if transpose else '' @@ -963,11 +963,10 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): #except AttributeError: ## happens when image data is non-contiguous #buf = imgData.data - #prof.mark('1') + #profiler() #qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat) - #prof.mark('2') + #profiler() #qimage.data = imgData - #prof.finish() #return qimage def imageToArray(img, copy=False, transpose=True): @@ -1087,16 +1086,16 @@ def arrayToQPath(x, y, connect='all'): path = QtGui.QPainterPath() - #prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True) + #profiler = debug.Profiler() n = x.shape[0] # create empty array, pad with extra space on either end arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')]) # write first two integers - #prof.mark('allocate empty') + #profiler('allocate empty') byteview = arr.view(dtype=np.ubyte) byteview[:12] = 0 byteview.data[12:20] = struct.pack('>ii', n, 0) - #prof.mark('pack header') + #profiler('pack header') # Fill array with vertex values arr[1:-1]['x'] = x arr[1:-1]['y'] = y @@ -1117,11 +1116,11 @@ def arrayToQPath(x, y, connect='all'): else: raise Exception('connect argument must be "all", "pairs", or array') - #prof.mark('fill array') + #profiler('fill array') # write last 0 lastInd = 20*(n+1) byteview.data[lastInd:lastInd+4] = struct.pack('>i', 0) - #prof.mark('footer') + #profiler('footer') # create datastream object and stream into path ## Avoiding this method because QByteArray(str) leaks memory in PySide @@ -1132,13 +1131,11 @@ def arrayToQPath(x, y, connect='all'): buf = QtCore.QByteArray.fromRawData(path.strn) except TypeError: buf = QtCore.QByteArray(bytes(path.strn)) - #prof.mark('create buffer') + #profiler('create buffer') ds = QtCore.QDataStream(buf) ds >> path - #prof.mark('load') - - #prof.finish() + #profiler('load') return path @@ -1865,7 +1862,7 @@ def isosurface(data, level): faces = np.empty((totFaces, 3), dtype=np.uint32) ptr = 0 #import debug - #p = debug.Profiler('isosurface', disabled=False) + #p = debug.Profiler() ## this helps speed up an indexing operation later on cs = np.array(cutEdges.strides)//cutEdges.itemsize @@ -1877,32 +1874,32 @@ def isosurface(data, level): for i in range(1,6): ### expensive: - #p.mark('1') + #profiler() cells = np.argwhere(nFaces == i) ## all cells which require i faces (argwhere is expensive) - #p.mark('2') + #profiler() if cells.shape[0] == 0: continue #cellInds = index[(cells*ins[np.newaxis,:]).sum(axis=1)] cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round - #p.mark('3') + #profiler() ### expensive: verts = faceShiftTables[i][cellInds] - #p.mark('4') + #profiler() verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges verts = verts.reshape((verts.shape[0]*i,)+verts.shape[2:]) - #p.mark('5') + #profiler() ### expensive: #print verts.shape verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2) #vertInds = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want. vertInds = cutEdges[verts] - #p.mark('6') + #profiler() nv = vertInds.shape[0] - #p.mark('7') + #profiler() faces[ptr:ptr+nv] = vertInds #.reshape((nv, 3)) - #p.mark('8') + #profiler() ptr += nv return vertexes, faces diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 429ff49c..3dd98cef 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -404,25 +404,22 @@ class AxisItem(GraphicsWidget): return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) def paint(self, p, opt, widget): - prof = debug.Profiler('AxisItem.paint', disabled=True) + profiler = debug.Profiler() if self.picture is None: try: picture = QtGui.QPicture() painter = QtGui.QPainter(picture) specs = self.generateDrawSpecs(painter) - prof.mark('generate specs') + profiler('generate specs') if specs is not None: self.drawPicture(painter, *specs) - prof.mark('draw picture') + profiler('draw picture') finally: painter.end() self.picture = picture #p.setRenderHint(p.Antialiasing, False) ## Sometimes we get a segfault here ??? #p.setRenderHint(p.TextAntialiasing, True) self.picture.play(p) - prof.finish() - - def setTicks(self, ticks): """Explicitly determine which ticks to display. @@ -626,8 +623,8 @@ class AxisItem(GraphicsWidget): be drawn, then generates from this a set of drawing commands to be interpreted by drawPicture(). """ - prof = debug.Profiler("AxisItem.generateDrawSpecs", disabled=True) - + profiler = debug.Profiler() + #bounds = self.boundingRect() bounds = self.mapRectFromParent(self.geometry()) @@ -706,7 +703,7 @@ class AxisItem(GraphicsWidget): xMin = min(xRange) xMax = max(xRange) - prof.mark('init') + profiler('init') tickPositions = [] # remembers positions of previously drawn ticks @@ -744,7 +741,7 @@ class AxisItem(GraphicsWidget): color.setAlpha(lineAlpha) tickPen.setColor(color) tickSpecs.append((tickPen, Point(p1), Point(p2))) - prof.mark('compute ticks') + profiler('compute ticks') ## This is where the long axis line should be drawn @@ -857,7 +854,7 @@ class AxisItem(GraphicsWidget): #p.setPen(self.pen()) #p.drawText(rect, textFlags, vstr) textSpecs.append((rect, textFlags, vstr)) - prof.mark('compute text') + profiler('compute text') ## update max text size if needed. self._updateMaxTextSize(textSize2) @@ -865,8 +862,8 @@ class AxisItem(GraphicsWidget): return (axisSpec, tickSpecs, textSpecs) def drawPicture(self, p, axisSpec, tickSpecs, textSpecs): - prof = debug.Profiler("AxisItem.drawPicture", disabled=True) - + profiler = debug.Profiler() + p.setRenderHint(p.Antialiasing, False) p.setRenderHint(p.TextAntialiasing, True) @@ -880,8 +877,8 @@ class AxisItem(GraphicsWidget): for pen, p1, p2 in tickSpecs: p.setPen(pen) p.drawLine(p1, p2) - prof.mark('draw ticks') - + profiler('draw ticks') + ## Draw all text if self.tickFont is not None: p.setFont(self.tickFont) @@ -889,10 +886,8 @@ class AxisItem(GraphicsWidget): for rect, flags, text in textSpecs: p.drawText(rect, flags, text) #p.drawRect(rect) - - prof.mark('draw text') - prof.finish() - + profiler('draw text') + def show(self): if self.orientation in ['left', 'right']: diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 5a3b63d6..70d8662f 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -184,19 +184,18 @@ class HistogramLUTItem(GraphicsWidget): self.update() def imageChanged(self, autoLevel=False, autoRange=False): - prof = debug.Profiler('HistogramLUTItem.imageChanged', disabled=True) + profiler = debug.Profiler() h = self.imageItem.getHistogram() - prof.mark('get histogram') + profiler('get histogram') if h[0] is None: return self.plot.setData(*h) - prof.mark('set plot') + profiler('set plot') if autoLevel: mn = h[0][0] mx = h[0][-1] self.region.setRegion([mn, mx]) - prof.mark('set region') - prof.finish() + profiler('set region') def getLevels(self): return self.region.getRegion() diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 5c4a09ef..f7a211d9 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -188,8 +188,8 @@ class ImageItem(GraphicsObject): border Sets the pen used when drawing the image border. Default is None. ================= ========================================================================= """ - prof = debug.Profiler('ImageItem.setImage', disabled=True) - + profile = debug.Profiler() + gotNewData = False if image is None: if self.image is None: @@ -201,9 +201,9 @@ class ImageItem(GraphicsObject): if shapeChanged: self.prepareGeometryChange() self.informViewBoundsChanged() - - prof.mark('1') - + + profile() + if autoLevels is None: if 'levels' in kargs: autoLevels = False @@ -218,23 +218,22 @@ class ImageItem(GraphicsObject): mn = 0 mx = 255 kargs['levels'] = [mn,mx] - prof.mark('2') - + + profile() + self.setOpts(update=False, **kargs) - prof.mark('3') - + + profile() + self.qimage = None self.update() - prof.mark('4') + + profile() if gotNewData: self.sigImageChanged.emit() - prof.finish() - - - def updateImage(self, *args, **kargs): ## used for re-rendering qimage from self.image. @@ -250,7 +249,7 @@ class ImageItem(GraphicsObject): def render(self): - prof = debug.Profiler('ImageItem.render', disabled=True) + profile = debug.Profiler() if self.image is None or self.image.size == 0: return if isinstance(self.lut, collections.Callable): @@ -262,28 +261,25 @@ class ImageItem(GraphicsObject): argb, alpha = fn.makeARGB(self.image.transpose((1, 0, 2)[:self.image.ndim]), lut=lut, levels=self.levels) self.qimage = fn.makeQImage(argb, alpha, transpose=False) - prof.finish() - def paint(self, p, *args): - prof = debug.Profiler('ImageItem.paint', disabled=True) + profile = debug.Profiler() if self.image is None: return if self.qimage is None: self.render() if self.qimage is None: return - prof.mark('render QImage') + profile('render QImage') if self.paintMode is not None: p.setCompositionMode(self.paintMode) - prof.mark('set comp mode') - + profile('set comp mode') + p.drawImage(QtCore.QPointF(0,0), self.qimage) - prof.mark('p.drawImage') + profile('p.drawImage') if self.border is not None: p.setPen(self.border) p.drawRect(self.boundingRect()) - prof.finish() def save(self, fileName, *args): """Save this image to file. Note that this saves the visible image (after scale/color changes), not the original data.""" diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py index a35e8efc..08f7e198 100644 --- a/pyqtgraph/graphicsItems/LinearRegionItem.py +++ b/pyqtgraph/graphicsItems/LinearRegionItem.py @@ -140,12 +140,11 @@ class LinearRegionItem(UIGraphicsItem): return br.normalized() def paint(self, p, *args): - #prof = debug.Profiler('LinearRegionItem.paint') + profiler = debug.Profiler() UIGraphicsItem.paint(self, p, *args) p.setBrush(self.currentBrush) p.setPen(fn.mkPen(None)) p.drawRect(self.boundingRect()) - #prof.finish() def dataBounds(self, axis, frac=1.0, orthoRange=None): if axis == self.orientation: diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 28214552..7ee06338 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -281,7 +281,7 @@ class PlotCurveItem(GraphicsObject): self.updateData(*args, **kargs) def updateData(self, *args, **kargs): - prof = debug.Profiler('PlotCurveItem.updateData', disabled=True) + profiler = debug.Profiler() if len(args) == 1: kargs['y'] = args[0] @@ -304,7 +304,7 @@ class PlotCurveItem(GraphicsObject): if 'complex' in str(data.dtype): raise Exception("Can not plot complex data types.") - prof.mark("data checks") + profiler("data checks") #self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly ## Test this bug with test_PlotWidget and zoom in on the animated plot @@ -314,7 +314,7 @@ class PlotCurveItem(GraphicsObject): self.yData = kargs['y'].view(np.ndarray) self.xData = kargs['x'].view(np.ndarray) - prof.mark('copy') + profiler('copy') if 'stepMode' in kargs: self.opts['stepMode'] = kargs['stepMode'] @@ -346,12 +346,11 @@ class PlotCurveItem(GraphicsObject): self.opts['antialias'] = kargs['antialias'] - prof.mark('set') + profiler('set') self.update() - prof.mark('update') + profiler('update') self.sigPlotChanged.emit(self) - prof.mark('emit') - prof.finish() + profiler('emit') def generatePath(self, x, y): if self.opts['stepMode']: @@ -387,7 +386,7 @@ class PlotCurveItem(GraphicsObject): @pg.debug.warnOnException ## raising an exception here causes crash def paint(self, p, opt, widget): - prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) + profiler = debug.Profiler() if self.xData is None: return @@ -405,7 +404,7 @@ class PlotCurveItem(GraphicsObject): self.fillPath = None path = self.path - prof.mark('generate path') + profiler('generate path') if self._exportOpts is not False: aa = self._exportOpts.get('antialias', True) @@ -426,9 +425,9 @@ class PlotCurveItem(GraphicsObject): p2.closeSubpath() self.fillPath = p2 - prof.mark('generate fill path') + profiler('generate fill path') p.fillPath(self.fillPath, self.opts['brush']) - prof.mark('draw fill path') + profiler('draw fill path') sp = fn.mkPen(self.opts['shadowPen']) cp = fn.mkPen(self.opts['pen']) @@ -451,10 +450,9 @@ class PlotCurveItem(GraphicsObject): p.drawPath(path) p.setPen(cp) p.drawPath(path) - prof.mark('drawPath') + profiler('drawPath') #print "Render hints:", int(p.renderHints()) - prof.finish() #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) #p.drawRect(self.boundingRect()) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 87b47227..2235711c 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -333,7 +333,7 @@ class PlotDataItem(GraphicsObject): See :func:`__init__() ` for details; it accepts the same arguments. """ #self.clear() - prof = debug.Profiler('PlotDataItem.setData (0x%x)' % id(self), disabled=True) + profiler = debug.Profiler() y = None x = None if len(args) == 1: @@ -383,7 +383,7 @@ class PlotDataItem(GraphicsObject): if 'y' in kargs: y = kargs['y'] - prof.mark('interpret data') + profiler('interpret data') ## pull in all style arguments. ## Use self.opts to fill in anything not present in kargs. @@ -432,10 +432,10 @@ class PlotDataItem(GraphicsObject): self.xClean = self.yClean = None self.xDisp = None self.yDisp = None - prof.mark('set data') + profiler('set data') self.updateItems() - prof.mark('update items') + profiler('update items') self.informViewBoundsChanged() #view = self.getViewBox() @@ -443,9 +443,7 @@ class PlotDataItem(GraphicsObject): #view.itemBoundsChanged(self) ## inform view so it can update its range if it wants self.sigPlotChanged.emit(self) - prof.mark('emit') - prof.finish() - + profiler('emit') def updateItems(self): diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index ec0960ba..7f817f81 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -339,9 +339,8 @@ class PlotItem(GraphicsWidget): self.ctrl.gridAlphaSlider.setValue(v) #def paint(self, *args): - #prof = debug.Profiler('PlotItem.paint', disabled=True) + #prof = debug.Profiler() #QtGui.QGraphicsWidget.paint(self, *args) - #prof.finish() ## bad idea. #def __getattr__(self, attr): ## wrap ms diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index f1a5201d..15be8be0 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -219,7 +219,7 @@ class ScatterPlotItem(GraphicsObject): """ Accepts the same arguments as setData() """ - prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True) + profiler = debug.Profiler() GraphicsObject.__init__(self) self.picture = None # QPicture used for rendering when pxmode==False @@ -240,11 +240,10 @@ class ScatterPlotItem(GraphicsObject): self.setBrush(100,100,150, update=False) self.setSymbol('o', update=False) self.setSize(7, update=False) - prof.mark('1') + profiler() self.setData(*args, **kargs) - prof.mark('setData') - prof.finish() - + profiler('setData') + #self.setCacheMode(self.DeviceCoordinateCache) def setData(self, *args, **kargs): diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 55d15757..17bd2207 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1205,7 +1205,7 @@ class ViewBox(GraphicsWidget): [[xmin, xmax], [ymin, ymax]] Values may be None if there are no specific bounds for an axis. """ - prof = debug.Profiler('updateAutoRange', disabled=True) + profiler = debug.Profiler() if items is None: items = self.addedItems @@ -1282,7 +1282,7 @@ class ViewBox(GraphicsWidget): range[0] = [min(bounds.left(), range[0][0]), max(bounds.right(), range[0][1])] else: range[0] = [bounds.left(), bounds.right()] - prof.mark('2') + profiler() #print "range", range @@ -1306,10 +1306,7 @@ class ViewBox(GraphicsWidget): continue range[1][0] = min(range[1][0], bounds.top() - px*pxSize) range[1][1] = max(range[1][1], bounds.bottom() + px*pxSize) - - #print "final range", range - - prof.finish() + return range def childrenBoundingRect(self, *args, **kwds): diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 77f34419..25700d93 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -190,7 +190,7 @@ class ImageView(QtGui.QWidget): image data. ================== ======================================================================= """ - prof = debug.Profiler('ImageView.setImage', disabled=True) + profiler = debug.Profiler() if hasattr(img, 'implements') and img.implements('MetaArray'): img = img.asarray() @@ -209,7 +209,7 @@ class ImageView(QtGui.QWidget): else: self.tVals = np.arange(img.shape[0]) - prof.mark('1') + profiler() if axes is None: if img.ndim == 2: @@ -234,13 +234,9 @@ class ImageView(QtGui.QWidget): for x in ['t', 'x', 'y', 'c']: self.axes[x] = self.axes.get(x, None) - prof.mark('2') - - self.imageDisp = None - - - prof.mark('3') - + + profiler() + self.currentIndex = 0 self.updateImage(autoHistogramRange=autoHistogramRange) if levels is None and autoLevels: @@ -250,9 +246,9 @@ class ImageView(QtGui.QWidget): if self.ui.roiBtn.isChecked(): self.roiChanged() - prof.mark('4') - - + + profiler() + if self.axes['t'] is not None: #self.ui.roiPlot.show() self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) @@ -271,8 +267,8 @@ class ImageView(QtGui.QWidget): s.setBounds([start, stop]) #else: #self.ui.roiPlot.hide() - prof.mark('5') - + profiler() + self.imageItem.resetTransform() if scale is not None: self.imageItem.scale(*scale) @@ -280,15 +276,15 @@ class ImageView(QtGui.QWidget): self.imageItem.setPos(*pos) if transform is not None: self.imageItem.setTransform(transform) - prof.mark('6') - + + profiler() + if autoRange: self.autoRange() self.roiClicked() - prof.mark('7') - prof.finish() - + profiler() + def play(self, rate): """Begin automatically stepping frames forward at the given rate (in fps). This can also be accessed by pressing the spacebar.""" From 6ae0892ea0d1c46b53f847bbadb38aeb5ffc94af Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 1 Dec 2013 10:23:45 -0500 Subject: [PATCH 023/268] Set version strings to 0.9.8 in source; these will be updated with major releases. Added tools/setVersion script setup.py now auto-generates version string based on pyqtgraph/__init__ and git info, if available --- doc/source/conf.py | 4 ++-- pyqtgraph/__init__.py | 2 +- setup.py | 49 +++++++++++++++++++++++++++++++++++++++++-- tools/setVersion.py | 26 +++++++++++++++++++++++ 4 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 tools/setVersion.py diff --git a/doc/source/conf.py b/doc/source/conf.py index 5475fc60..bf35651d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -50,9 +50,9 @@ copyright = '2011, Luke Campagnola' # built documents. # # The short X.Y version. -version = '' +version = '0.9.8' # The full version, including alpha/beta/rc tags. -release = '' +release = '0.9.8' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 11e281a4..f46184b4 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -4,7 +4,7 @@ PyQtGraph - Scientific Graphics and GUI Library for Python www.pyqtgraph.org """ -__version__ = None +__version__ = '0.9.8' ### import all the goodies and add some helper functions for easy CLI use diff --git a/setup.py b/setup.py index 055b74e8..8bda9eeb 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ from distutils.core import setup import distutils.dir_util -import os +import os, re +from subprocess import check_output ## generate list of all sub-packages path = os.path.abspath(os.path.dirname(__file__)) @@ -14,8 +15,52 @@ buildPath = os.path.join(path, 'build') if os.path.isdir(buildPath): distutils.dir_util.remove_tree(buildPath) + +## Determine current version string +init = open(os.path.join(path, 'pyqtgraph/__init__.py')).read() +m = re.search(r'__version__ = (\S+)\n', init) +if m is None: + raise Exception("Cannot determine version number!") +version = m.group(1).strip('\'\"') + +# If this is a git checkout, append the current commit +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'])).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 + '+' + +print("PyQtGraph version: " + version) + setup(name='pyqtgraph', - version='', + version=version, description='Scientific Graphics and GUI Library for Python', long_description="""\ PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and diff --git a/tools/setVersion.py b/tools/setVersion.py new file mode 100644 index 00000000..b62aca01 --- /dev/null +++ b/tools/setVersion.py @@ -0,0 +1,26 @@ +import re, os, sys + +version = sys.argv[1] + +replace = [ + ("pyqtgraph/__init__.py", r"__version__ = .*", "__version__ = '%s'" % version), + #("setup.py", r" version=.*,", " version='%s'," % version), # setup.py automatically detects version + ("doc/source/conf.py", r"version = .*", "version = '%s'" % version), + ("doc/source/conf.py", r"release = .*", "release = '%s'" % version), + #("tools/debian/control", r"^Version: .*", "Version: %s" % version) + ] + +path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') + +for filename, search, sub in replace: + filename = os.path.join(path, filename) + data = open(filename, 'r').read() + if re.search(search, data) is None: + print('Error: Search expression "%s" not found in file %s.' % (search, filename)) + os._exit(1) + open(filename, 'w').write(re.sub(search, sub, data)) + +print("Updated version strings to %s" % version) + + + From bc7bc29740ef5ec651bfe5c21224dc342f3c1041 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 8 Dec 2013 12:47:04 -0500 Subject: [PATCH 024/268] Added HDF5 file to demonstrate dynamically plotting a subset of a very large dataset * Loads only data that is currently visible * Downsamples to avoid plotting too many samples * Loads data in chunks to limit memory usage during downsampling --- examples/__main__.py | 1 + examples/hdf5.py | 139 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 examples/hdf5.py diff --git a/examples/__main__.py b/examples/__main__.py index a397cf05..7d75e36a 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -30,6 +30,7 @@ examples = OrderedDict([ ('Histograms', 'histogram.py'), ('Auto-range', 'PlotAutoRange.py'), ('Remote Plotting', 'RemoteSpeedTest.py'), + ('HDF5 big data', 'hdf5.py'), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), diff --git a/examples/hdf5.py b/examples/hdf5.py new file mode 100644 index 00000000..57b5672f --- /dev/null +++ b/examples/hdf5.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +""" +In this example we create a subclass of PlotCurveItem for displaying a very large +data set from an HDF5 file that does not fit in memory. + +The basic approach is to override PlotCurveItem.viewRangeChanged such that it +reads only the portion of the HDF5 data that is necessary to display the visible +portion of the data. This is further downsampled to reduce the number of samples +being displayed. + +A more clever implementation of this class would employ some kind of caching +to avoid re-reading the entire visible waveform at every update. +""" + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np +import h5py + +import sys, os +if len(sys.argv) > 1: + fileName = sys.argv[1] +else: + fileName = 'test.hdf5' + if not os.path.isfile(fileName): + print "No suitable HDF5 file found. Use createFile() to generate an example file." + os._exit(1) + +plt = pg.plot() +plt.setWindowTitle('pyqtgraph example: HDF5 big data') +plt.enableAutoRange(False, False) +plt.setXRange(0, 500) + +class HDF5Plot(pg.PlotCurveItem): + def __init__(self, *args, **kwds): + self.hdf5 = None + self.limit = 10000 # maximum number of samples to be plotted + pg.PlotCurveItem.__init__(self, *args, **kwds) + + def setHDF5(self, data): + self.hdf5 = data + self.updateHDF5Plot() + + def viewRangeChanged(self): + self.updateHDF5Plot() + + def updateHDF5Plot(self): + if self.hdf5 is None: + self.setData([]) + return + + vb = self.getViewBox() + if vb is None: + return # no ViewBox yet + + # Determine what data range must be read from HDF5 + xrange = vb.viewRange()[0] + start = max(0,int(xrange[0])-1) + stop = min(len(self.hdf5), int(xrange[1]+2)) + + # Decide by how much we should downsample + ds = int((stop-start) / self.limit) + 1 + + if ds == 1: + # Small enough to display with no intervention. + visible = self.hdf5[start:stop] + scale = 1 + else: + # Here convert data into a down-sampled array suitable for visualizing. + # Must do this piecewise to limit memory usage. + samples = 1 + ((stop-start) // ds) + visible = np.zeros(samples*2, dtype=self.hdf5.dtype) + sourcePtr = start + targetPtr = 0 + + # read data in chunks of ~1M samples + chunkSize = (1000000//ds) * ds + while sourcePtr < stop-1: + chunk = self.hdf5[sourcePtr:min(stop,sourcePtr+chunkSize)] + sourcePtr += len(chunk) + + # reshape chunk to be integral multiple of ds + chunk = chunk[:(len(chunk)//ds) * ds].reshape(len(chunk)//ds, ds) + + # compute max and min + chunkMax = chunk.max(axis=1) + chunkMin = chunk.min(axis=1) + + # interleave min and max into plot data to preserve envelope shape + visible[targetPtr:targetPtr+chunk.shape[0]*2:2] = chunkMin + visible[1+targetPtr:1+targetPtr+chunk.shape[0]*2:2] = chunkMax + targetPtr += chunk.shape[0]*2 + + visible = visible[:targetPtr] + scale = ds * 0.5 + + self.setData(visible) # update the plot + self.setPos(start, 0) # shift to match starting index + self.resetTransform() + self.scale(scale, 1) # scale to match downsampling + + +f = h5py.File(fileName, 'r') +curve = HDF5Plot() +curve.setHDF5(f['data']) +plt.addItem(curve) + + +def createFile(finalSize=2000000000): + """Create a large HDF5 data file for testing. + Data consists of 1M random samples tiled through the end of the array. + """ + + chunk = np.random.normal(size=1000000).astype(np.float32) + + f = h5py.File('test.hdf5', 'w') + f.create_dataset('data', data=chunk, chunks=True, maxshape=(None,)) + data = f['data'] + + for i in range(finalSize // (chunk.size * chunk.itemsize)): + newshape = [data.shape[0] + chunk.shape[0]] + data.resize(newshape) + data[-chunk.shape[0]:] = chunk + + f.close() + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() + + + + From e4ca62448b676d9cf90d5debe4ab834bd5cc1a2a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 11 Dec 2013 14:28:56 -0500 Subject: [PATCH 025/268] Added Dock.raiseDock() method --- pyqtgraph/dockarea/Container.py | 7 +++++++ pyqtgraph/dockarea/Dock.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/pyqtgraph/dockarea/Container.py b/pyqtgraph/dockarea/Container.py index 83610937..01ae51d3 100644 --- a/pyqtgraph/dockarea/Container.py +++ b/pyqtgraph/dockarea/Container.py @@ -241,6 +241,13 @@ class TContainer(Container, QtGui.QWidget): else: w.label.setDim(True) + def raiseDock(self, dock): + """Move *dock* to the top of the stack""" + self.stack.currentWidget().label.setDim(True) + self.stack.setCurrentWidget(dock) + dock.label.setDim(False) + + def type(self): return 'tab' diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 414980ac..09a97813 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -208,6 +208,11 @@ class Dock(QtGui.QWidget, DockDrop): self.moveLabel = False self.setOrientation(force=True) + + def raiseDock(self): + """If this Dock is stacked underneath others, raise it to the top.""" + self.container().raiseDock(self) + def close(self): """Remove this dock from the DockArea it lives inside.""" From 5b7f4124d973fca42412c43465a92ccfe3f69127 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 15 Dec 2013 09:07:09 -0500 Subject: [PATCH 026/268] * Made new profilers compatible with old API * Adjusted output formatting for clearer representation of nested profilers * Message string formatting deferred until finish to reduce overhead --- pyqtgraph/debug.py | 56 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 0ee65a33..685780d4 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -402,12 +402,27 @@ class Profiler(object): _profilers = os.environ.get("PYQTGRAPHPROFILE", "") _depth = 0 _msgs = [] + + class DisabledProfiler(object): + def __init__(self, *args, **kwds): + pass + def __call__(self, *args): + pass + def finish(self): + pass + def mark(self, msg=None): + pass + _disabledProfiler = DisabledProfiler() + if _profilers: _profilers = _profilers.split(",") - def __new__(cls, delayed=True): + def __new__(cls, msg=None, disabled='env', delayed=True): """Optionally create a new profiler based on caller's qualname. """ + if disabled is True: + return cls._disabledProfiler + # determine the qualified name of the caller function caller_frame = sys._getframe(1) try: @@ -418,15 +433,16 @@ class Profiler(object): qualifier = caller_object_type.__name__ func_qualname = qualifier + "." + caller_frame.f_code.co_name if func_qualname not in cls._profilers: # don't do anything - return lambda msg=None: None + return cls._disabledProfiler # create an actual profiling object cls._depth += 1 obj = super(Profiler, cls).__new__(cls) - obj._name = func_qualname + obj._name = msg or func_qualname obj._delayed = delayed obj._markCount = 0 + obj._finished = False obj._firstTime = obj._lastTime = ptime.time() - obj._newMsg("> Entering " + func_qualname) + obj._newMsg("> Entering " + obj._name) return obj else: def __new__(cls, delayed=True): @@ -439,26 +455,38 @@ class Profiler(object): msg = str(self._markCount) self._markCount += 1 newTime = ptime.time() - self._newMsg( - msg + ": " + str((newTime - self._lastTime) * 1000) + "ms") + self._newMsg(" %s: %0.4f ms", + msg, (newTime - self._lastTime) * 1000) self._lastTime = newTime + + def mark(self, msg=None): + self(msg) - def _newMsg(self, msg): - msg = " " * (self._depth - 1) + msg + def _newMsg(self, msg, *args): + msg = " " * (self._depth - 1) + msg if self._delayed: - self._msgs.append(msg) + self._msgs.append((msg, args)) else: - print(msg) + print(msg % args) def __del__(self): + self.finish() + + def finish(self, msg=None): """Add a final message; flush the message list if no parent profiler. """ - self._newMsg("< Exiting " + self._name + ", total time: " + - str((ptime.time() - self._firstTime) * 1000) + "ms") + if self._finished: + return + self._finished = True + if msg is not None: + self(msg) + self._newMsg("< Exiting %s, total time: %0.4f ms", + self._name, (ptime.time() - self._firstTime) * 1000) type(self)._depth -= 1 - if not self._depth and self._msgs: - print("\n".join(self._msgs)) + if self._depth < 1 and self._msgs: + print("\n".join([m[0]%m[1] for m in self._msgs])) type(self)._msgs = [] + def profile(code, name='profile_run', sort='cumulative', num=30): From 44ce6f56461f74d398e61acd5b0954447a9d2bcb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 15 Dec 2013 11:38:44 -0500 Subject: [PATCH 027/268] Updated contributors list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 23f47ea7..b5f83be2 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Contributors * Ulrich Leutner * Felix Schill * Guillaume Poulin + * Antony Lee Requirements ------------ From 09e0bf73c35aef6222338c4056fa88e708dff275 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 15 Dec 2013 13:01:37 -0500 Subject: [PATCH 028/268] setup.py now modifies __init__.py on build to include a more descriptive version string if .git is present. --- setup.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8bda9eeb..1bf206b2 100644 --- a/setup.py +++ b/setup.py @@ -17,11 +17,12 @@ if os.path.isdir(buildPath): ## Determine current version string -init = open(os.path.join(path, 'pyqtgraph/__init__.py')).read() +init = open(os.path.join(path, 'pyqtgraph', '__init__.py')).read() m = re.search(r'__version__ = (\S+)\n', init) if m is None: raise Exception("Cannot determine version number!") version = m.group(1).strip('\'\"') +initVersion = version # If this is a git checkout, append the current commit if os.path.isdir(os.path.join(path, '.git')): @@ -59,8 +60,27 @@ if os.path.isdir(os.path.join(path, '.git')): print("PyQtGraph version: " + version) +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') + 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="""\ PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and From 0f73e89ec68876c21c9dff11cfe952b9d0992631 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 15 Dec 2013 13:17:26 -0500 Subject: [PATCH 029/268] make setup.py more robust to possible errors during version string modification --- setup.py | 95 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/setup.py b/setup.py index 1bf206b2..1d3f0b25 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from distutils.core import setup import distutils.dir_util -import os, re +import os, sys, re from subprocess import check_output ## generate list of all sub-packages @@ -17,48 +17,57 @@ if os.path.isdir(buildPath): ## Determine current version string -init = open(os.path.join(path, 'pyqtgraph', '__init__.py')).read() +initfile = os.path.join(path, 'pyqtgraph', '__init__.py') +init = open(initfile).read() m = re.search(r'__version__ = (\S+)\n', init) -if m is None: - raise Exception("Cannot determine version number!") +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, append the current commit -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 +# 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:] - # 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'])).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 + '+' + # 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'])).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()) -print("PyQtGraph version: " + version) import distutils.command.build @@ -73,8 +82,16 @@ class Build(distutils.command.build.build): return ret initfile = os.path.join(path, self.build_lib, 'pyqtgraph', '__init__.py') - data = open(initfile, 'r').read() - open(initfile, 'w').write(re.sub(r"__version__ = .*", "__version__ = '%s'" % version, data)) + 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 From a9b1fd9079c4d3288b6247d626516f4c3349be16 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 16 Dec 2013 00:25:38 -0800 Subject: [PATCH 030/268] Some Python3 related fixes. --- examples/hdf5.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hdf5.py b/examples/hdf5.py index 57b5672f..0cbf667f 100644 --- a/examples/hdf5.py +++ b/examples/hdf5.py @@ -25,7 +25,7 @@ if len(sys.argv) > 1: else: fileName = 'test.hdf5' if not os.path.isfile(fileName): - print "No suitable HDF5 file found. Use createFile() to generate an example file." + print("No suitable HDF5 file found. Use createFile() to generate an example file.") os._exit(1) plt = pg.plot() diff --git a/setup.py b/setup.py index 1d3f0b25..826bb57c 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ try: lastTag = gitCommit(lastTagName) head = gitCommit('HEAD') if head != lastTag: - branch = re.search(r'\* (.*)', check_output(['git', 'branch'])).group(1) + branch = re.search(r'\* (.*)', check_output(['git', 'branch'], universal_newlines=True)).group(1) version = version + "-%s-%s" % (branch, head[:10]) # any uncommitted modifications? From 4e9e75817fb0d2b5f6880278857621af30d7d0f1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 17 Dec 2013 21:23:37 -0500 Subject: [PATCH 031/268] Added Qt.loadUiType function for PySide Added example of simple Designer usage. --- examples/designerExample.py | 46 ++++++++++++++++++++++++++++++++ examples/designerExample.ui | 38 +++++++++++++++++++++++++++ pyqtgraph/Qt.py | 52 ++++++++++++++++++++++++++++++++++--- 3 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 examples/designerExample.py create mode 100644 examples/designerExample.ui diff --git a/examples/designerExample.py b/examples/designerExample.py new file mode 100644 index 00000000..812eff6b --- /dev/null +++ b/examples/designerExample.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" +Simple example of loading UI template created with Qt Designer. + +This example uses uic.loadUiType to parse and load the ui at runtime. It is also +possible to pre-compile the .ui file using pyuic (see VideoSpeedTest and +ScatterPlotSpeedTest examples; these .ui files have been compiled with the +tools/rebuildUi.py script). +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np +import os + +pg.mkQApp() + +## Define main window class from template +path = os.path.dirname(os.path.abspath(__file__)) +uiFile = os.path.join(path, 'designerExample.ui') +WindowTemplate, TemplateBaseClass = pg.Qt.loadUiType(uiFile) + +class MainWindow(TemplateBaseClass): + def __init__(self): + TemplateBaseClass.__init__(self) + self.setWindowTitle('pyqtgraph example: Qt Designer') + + # Create the main window + self.ui = WindowTemplate() + self.ui.setupUi(self) + self.ui.plotBtn.clicked.connect(self.plot) + + self.show() + + def plot(self): + self.ui.plot.plot(np.random.normal(size=100), clear=True) + +win = MainWindow() + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/designerExample.ui b/examples/designerExample.ui new file mode 100644 index 00000000..41d06089 --- /dev/null +++ b/examples/designerExample.ui @@ -0,0 +1,38 @@ + + + Form + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + Plot! + + + + + + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index e584a381..410bfd83 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -1,4 +1,14 @@ -## Do all Qt imports from here to allow easier PyQt / PySide compatibility +""" +This module exists to smooth out some of the differences between PySide and PyQt4: + +* Automatically import either PyQt4 or PySide depending on availability +* Allow to import QtCore/QtGui pyqtgraph.Qt without specifying which Qt wrapper + you want to use. +* Declare QtCore.Signal, .Slot in PyQt4 +* Declare loadUiType function for Pyside + +""" + import sys, re ## Automatically determine whether to use PyQt or PySide. @@ -23,8 +33,41 @@ if USE_PYSIDE: from PySide import QtGui, QtCore, QtOpenGL, QtSvg import PySide VERSION_INFO = 'PySide ' + PySide.__version__ + + # Make a loadUiType function like PyQt has + + # Credit: + # http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313 + + def loadUiType(uiFile): + """ + Pyside "loadUiType" command like PyQt4 has one, so we have to convert the ui file to py code in-memory first and then execute it in a special frame to retrieve the form_class. + """ + import pysideuic + import xml.etree.ElementTree as xml + from io import StringIO + + parsed = xml.parse(uiFile) + widget_class = parsed.find('widget').get('class') + form_class = parsed.find('class').text + + with open(uiFile, 'r') as f: + o = StringIO() + frame = {} + + pysideuic.compileUi(f, o, indent=0) + pyc = compile(o.getvalue(), '', 'exec') + exec(pyc, frame) + + #Fetch the base_class and form class based on their type in the xml from designer + form_class = frame['Ui_%s'%form_class] + base_class = eval('QtGui.%s'%widget_class) + + return form_class, base_class + + else: - from PyQt4 import QtGui, QtCore + from PyQt4 import QtGui, QtCore, uic try: from PyQt4 import QtSvg except ImportError: @@ -34,6 +77,9 @@ else: except ImportError: pass + + loadUiType = uic.loadUiType + QtCore.Signal = QtCore.pyqtSignal VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR @@ -43,6 +89,6 @@ versionReq = [4, 7] QtVersion = PySide.QtCore.__version__ if USE_PYSIDE else QtCore.QT_VERSION_STR m = re.match(r'(\d+)\.(\d+).*', QtVersion) if m is not None and list(map(int, m.groups())) < versionReq: - print(map(int, m.groups())) + print(list(map(int, m.groups()))) raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) From a0b7e5a61c98e02a663ff19373f1f14c4cd5de4e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 19 Dec 2013 09:56:58 -0500 Subject: [PATCH 032/268] Corrected mouse clicking on PlotCurveItem - now uses stroke outline instead of path shape Added 'width' argument to PlotCurveItem.setClickable() --- examples/MouseSelection.py | 37 +++++++++++++ pyqtgraph/graphicsItems/PlotCurveItem.py | 67 +++++++++++++++++------- 2 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 examples/MouseSelection.py diff --git a/examples/MouseSelection.py b/examples/MouseSelection.py new file mode 100644 index 00000000..3a573751 --- /dev/null +++ b/examples/MouseSelection.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" +Demonstrates selecting plot curves by mouse click +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +win = pg.plot() +win.setWindowTitle('pyqtgraph example: Plot data selection') + +curves = [ + pg.PlotCurveItem(y=np.sin(np.linspace(0, 20, 1000)), pen='r', clickable=True), + pg.PlotCurveItem(y=np.sin(np.linspace(1, 21, 1000)), pen='g', clickable=True), + pg.PlotCurveItem(y=np.sin(np.linspace(2, 22, 1000)), pen='b', clickable=True), + ] + +def plotClicked(curve): + global curves + for i,c in enumerate(curves): + if c is curve: + c.setPen('rgb'[i], width=3) + else: + c.setPen('rgb'[i], width=1) + + +for c in curves: + win.addItem(c) + c.sigClicked.connect(plotClicked) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 7ee06338..d221bf74 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -53,9 +53,6 @@ class PlotCurveItem(GraphicsObject): """ GraphicsObject.__init__(self, kargs.get('parent', None)) self.clear() - self.path = None - self.fillPath = None - self._boundsCache = [None, None] ## this is disastrous for performance. #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) @@ -70,6 +67,7 @@ class PlotCurveItem(GraphicsObject): 'name': None, 'antialias': pg.getConfigOption('antialias'),\ 'connect': 'all', + 'mouseWidth': 8, # width of shape responding to mouse click } self.setClickable(kargs.get('clickable', False)) self.setData(*args, **kargs) @@ -80,9 +78,17 @@ class PlotCurveItem(GraphicsObject): return ints return interface in ints - def setClickable(self, s): - """Sets whether the item responds to mouse clicks.""" + def setClickable(self, s, width=None): + """Sets whether the item responds to mouse clicks. + + The *width* argument specifies the width in pixels orthogonal to the + curve that will respond to a mouse click. + """ self.clickable = s + if width is not None: + self.opts['mouseWidth'] = width + self._mouseShape = None + self._boundingRect = None def getData(self): @@ -148,6 +154,8 @@ class PlotCurveItem(GraphicsObject): w += pen.widthF()*0.7072 if spen is not None and spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen: w = max(w, spen.widthF()*0.7072) + if self.clickable: + w = max(w, self.opts['mouseWidth']//2 + 1) return w def boundingRect(self): @@ -171,6 +179,7 @@ class PlotCurveItem(GraphicsObject): #px += self._maxSpotWidth * 0.5 #py += self._maxSpotWidth * 0.5 self._boundingRect = QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn) + return self._boundingRect def viewTransformChanged(self): @@ -328,6 +337,7 @@ class PlotCurveItem(GraphicsObject): self.path = None self.fillPath = None + self._mouseShape = None #self.xDisp = self.yDisp = None if 'name' in kargs: @@ -376,12 +386,14 @@ class PlotCurveItem(GraphicsObject): return path - def shape(self): + def getPath(self): if self.path is None: - try: - self.path = self.generatePath(*self.getData()) - except: + 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.fillPath = None + self._mouseShape = None return self.path @pg.debug.warnOnException ## raising an exception here causes crash @@ -396,14 +408,8 @@ class PlotCurveItem(GraphicsObject): x = None y = None - 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 - self.path = self.generatePath(x,y) - self.fillPath = None - - path = self.path + path = self.getPath() + profiler('generate path') if self._exportOpts is not False: @@ -522,13 +528,36 @@ class PlotCurveItem(GraphicsObject): self.xDisp = None ## display values (after log / fft) self.yDisp = None self.path = None + self.fillPath = None + self._mouseShape = None + self._mouseBounds = None + self._boundsCache = [None, None] #del self.xData, self.yData, self.xDisp, self.yDisp, self.path + + def mouseShape(self): + """ + Return a QPainterPath representing the clickable shape of the curve + + """ + if self._mouseShape is None: + view = self.getViewBox() + if view is None: + return QtGui.QPainterPath() + stroker = QtGui.QPainterPathStroker() + path = self.getPath() + path = self.mapToItem(view, path) + stroker.setWidth(self.opts['mouseWidth']) + mousePath = stroker.createStroke(path) + self._mouseShape = self.mapFromItem(view, mousePath) + return self._mouseShape def mouseClickEvent(self, ev): if not self.clickable or ev.button() != QtCore.Qt.LeftButton: return - ev.accept() - self.sigClicked.emit(self) + if self.mouseShape().contains(ev.pos()): + ev.accept() + self.sigClicked.emit(self) + class ROIPlotItem(PlotCurveItem): From 03c01d3b32b420cd888f9acf7f2d36b7a4d47acf Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 19 Dec 2013 12:30:00 -0500 Subject: [PATCH 033/268] Fixes related to CSV exporter: - CSV headers include data names, if available - Exporter correctly handles items with no data - pg.plot() avoids creating empty data item - removed call to reduce() from exporter; not available in python 3 - Gave .name() methods to PlotDataItem, PlotCurveItem, and ScatterPlotItem --- examples/Plotting.py | 6 +++--- pyqtgraph/__init__.py | 3 ++- pyqtgraph/exporters/CSVExporter.py | 12 +++++++++--- pyqtgraph/graphicsItems/PlotCurveItem.py | 5 ++++- pyqtgraph/graphicsItems/PlotDataItem.py | 3 +++ pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 4 +++- pyqtgraph/graphicsItems/ScatterPlotItem.py | 6 ++++++ 7 files changed, 30 insertions(+), 9 deletions(-) diff --git a/examples/Plotting.py b/examples/Plotting.py index 6578fb2b..8476eae8 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -27,9 +27,9 @@ pg.setConfigOptions(antialias=True) p1 = win.addPlot(title="Basic array plotting", y=np.random.normal(size=100)) p2 = win.addPlot(title="Multiple curves") -p2.plot(np.random.normal(size=100), pen=(255,0,0)) -p2.plot(np.random.normal(size=100)+5, pen=(0,255,0)) -p2.plot(np.random.normal(size=100)+10, pen=(0,0,255)) +p2.plot(np.random.normal(size=100), pen=(255,0,0), name="Red curve") +p2.plot(np.random.normal(size=110)+5, pen=(0,255,0), name="Blue curve") +p2.plot(np.random.normal(size=120)+10, pen=(0,0,255), name="Green curve") p3 = win.addPlot(title="Drawing with points") p3.plot(np.random.normal(size=100), pen=(200,200,200), symbolBrush=(255,0,0), symbolPen='w') diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f46184b4..f25d1c3a 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -292,7 +292,8 @@ def plot(*args, **kargs): dataArgs[k] = kargs[k] w = PlotWindow(**pwArgs) - w.plot(*args, **dataArgs) + if len(args) > 0 or len(dataArgs) > 0: + w.plot(*args, **dataArgs) plots.append(w) w.show() return w diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index 0439fc35..3ff2af31 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -33,8 +33,14 @@ class CSVExporter(Exporter): data = [] header = [] for c in self.item.curves: - data.append(c.getData()) - header.extend(['x', 'y']) + cd = c.getData() + if cd[0] is None: + continue + data.append(cd) + name = '' + if hasattr(c, 'implements') and c.implements('plotData') and c.name() is not None: + name = c.name().replace('"', '""') + '_' + header.extend(['"'+name+'x"', '"'+name+'y"']) if self.params['separator'] == 'comma': sep = ',' @@ -44,7 +50,7 @@ class CSVExporter(Exporter): fd.write(sep.join(header) + '\n') i = 0 numFormat = '%%0.%dg' % self.params['precision'] - numRows = reduce(max, [len(d[0]) for d in data]) + numRows = max([len(d[0]) for d in data]) for i in range(numRows): for d in data: if i < len(d[0]): diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index d221bf74..93ef195b 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -65,7 +65,7 @@ class PlotCurveItem(GraphicsObject): 'brush': None, 'stepMode': False, 'name': None, - 'antialias': pg.getConfigOption('antialias'),\ + 'antialias': pg.getConfigOption('antialias'), 'connect': 'all', 'mouseWidth': 8, # width of shape responding to mouse click } @@ -78,6 +78,9 @@ class PlotCurveItem(GraphicsObject): return ints return interface in ints + def name(self): + return self.opts.get('name', None) + def setClickable(self, s, width=None): """Sets whether the item responds to mouse clicks. diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 2235711c..7d46d65c 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -170,6 +170,9 @@ class PlotDataItem(GraphicsObject): return ints return interface in ints + def name(self): + return self.opts.get('name', None) + def boundingRect(self): return QtCore.QRectF() ## let child items handle this diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 7f817f81..b99a3266 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -514,7 +514,9 @@ class PlotItem(GraphicsWidget): if 'ignoreBounds' in kargs: vbargs['ignoreBounds'] = kargs['ignoreBounds'] self.vb.addItem(item, *args, **vbargs) + name = None if hasattr(item, 'implements') and item.implements('plotData'): + name = item.name() self.dataItems.append(item) #self.plotChanged() @@ -547,7 +549,7 @@ class PlotItem(GraphicsWidget): #c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) #item.sigPlotChanged.connect(self.plotChanged) #self.plotChanged() - name = kargs.get('name', getattr(item, 'opts', {}).get('name', None)) + #name = kargs.get('name', getattr(item, 'opts', {}).get('name', None)) if name is not None and hasattr(self, 'legend') and self.legend is not None: self.legend.addItem(item, name=name) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 15be8be0..498df2cd 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -234,6 +234,7 @@ class ScatterPlotItem(GraphicsObject): 'pxMode': True, 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. 'antialias': pg.getConfigOption('antialias'), + 'name': None, } self.setPen(200,200,200, update=False) @@ -281,6 +282,8 @@ class ScatterPlotItem(GraphicsObject): *antialias* Whether to draw symbols with antialiasing. Note that if pxMode is True, symbols are always rendered with antialiasing (since the rendered symbols can be cached, this incurs very little performance cost) + *name* The name of this item. Names are used for automatically + generating LegendItem entries and by some exporters. ====================== =============================================================================================== """ oldData = self.data ## this causes cached pixmaps to be preserved while new data is registered. @@ -410,6 +413,9 @@ class ScatterPlotItem(GraphicsObject): return ints return interface in ints + def name(self): + return self.opts.get('name', None) + def setPen(self, *args, **kargs): """Set the pen(s) used to draw the outline around each spot. If a list or array is provided, then the pen for each spot will be set separately. From 63f3b0ab6e735093ff0548610f9b515d42a23e34 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 19 Dec 2013 12:44:03 -0500 Subject: [PATCH 034/268] Fix examples/hdf5.py to work properly with --test --- examples/hdf5.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/hdf5.py b/examples/hdf5.py index 0cbf667f..3e239d9f 100644 --- a/examples/hdf5.py +++ b/examples/hdf5.py @@ -25,8 +25,7 @@ if len(sys.argv) > 1: else: fileName = 'test.hdf5' if not os.path.isfile(fileName): - print("No suitable HDF5 file found. Use createFile() to generate an example file.") - os._exit(1) + raise Exception("No suitable HDF5 file found. Use createFile() to generate an example file.") plt = pg.plot() plt.setWindowTitle('pyqtgraph example: HDF5 big data') From b50b94908ff4212b228c79888bf206b2b7669844 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 22 Nov 2013 19:52:40 -0500 Subject: [PATCH 035/268] pyqtgraph, .exporters, and .opengl now use static imports --- pyqtgraph/__init__.py | 154 +++++++++++++++++++-------- pyqtgraph/exporters/CSVExporter.py | 2 +- pyqtgraph/exporters/Exporter.py | 52 ++------- pyqtgraph/exporters/ImageExporter.py | 1 + pyqtgraph/exporters/Matplotlib.py | 3 + pyqtgraph/exporters/PrintExporter.py | 3 + pyqtgraph/exporters/SVGExporter.py | 4 + pyqtgraph/exporters/__init__.py | 43 ++++---- pyqtgraph/opengl/__init__.py | 36 +++---- 9 files changed, 163 insertions(+), 135 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f25d1c3a..77b7c590 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -130,56 +130,119 @@ if __version__ is None and not hasattr(sys, 'frozen') and sys.version_info[0] == ## Import almost everything to make it available from a single namespace ## don't import the more complex systems--canvas, parametertree, flowchart, dockarea ## these must be imported separately. -from . import frozenSupport -def importModules(path, globals, locals, excludes=()): - """Import all modules residing within *path*, return a dict of name: module pairs. +#from . import frozenSupport +#def importModules(path, globals, locals, excludes=()): + #"""Import all modules residing within *path*, return a dict of name: module pairs. - Note that *path* MUST be relative to the module doing the import. - """ - d = os.path.join(os.path.split(globals['__file__'])[0], path) - files = set() - for f in frozenSupport.listdir(d): - if frozenSupport.isdir(os.path.join(d, f)) and f not in ['__pycache__', 'tests']: - files.add(f) - elif f[-3:] == '.py' and f != '__init__.py': - files.add(f[:-3]) - elif f[-4:] == '.pyc' and f != '__init__.pyc': - files.add(f[:-4]) + #Note that *path* MUST be relative to the module doing the import. + #""" + #d = os.path.join(os.path.split(globals['__file__'])[0], path) + #files = set() + #for f in frozenSupport.listdir(d): + #if frozenSupport.isdir(os.path.join(d, f)) and f not in ['__pycache__', 'tests']: + #files.add(f) + #elif f[-3:] == '.py' and f != '__init__.py': + #files.add(f[:-3]) + #elif f[-4:] == '.pyc' and f != '__init__.pyc': + #files.add(f[:-4]) - mods = {} - path = path.replace(os.sep, '.') - for modName in files: - if modName in excludes: - continue - try: - if len(path) > 0: - modName = path + '.' + modName - #mod = __import__(modName, globals, locals, fromlist=['*']) - mod = __import__(modName, globals, locals, ['*'], 1) - mods[modName] = mod - except: - import traceback - traceback.print_stack() - sys.excepthook(*sys.exc_info()) - print("[Error importing module: %s]" % modName) + #mods = {} + #path = path.replace(os.sep, '.') + #for modName in files: + #if modName in excludes: + #continue + #try: + #if len(path) > 0: + #modName = path + '.' + modName + #print( "from .%s import * " % modName) + #mod = __import__(modName, globals, locals, ['*'], 1) + #mods[modName] = mod + #except: + #import traceback + #traceback.print_stack() + #sys.excepthook(*sys.exc_info()) + #print("[Error importing module: %s]" % modName) - return mods + #return mods -def importAll(path, globals, locals, excludes=()): - """Given a list of modules, import all names from each module into the global namespace.""" - mods = importModules(path, globals, locals, excludes) - for mod in mods.values(): - if hasattr(mod, '__all__'): - names = mod.__all__ - else: - names = [n for n in dir(mod) if n[0] != '_'] - for k in names: - if hasattr(mod, k): - globals[k] = getattr(mod, k) +#def importAll(path, globals, locals, excludes=()): + #"""Given a list of modules, import all names from each module into the global namespace.""" + #mods = importModules(path, globals, locals, excludes) + #for mod in mods.values(): + #if hasattr(mod, '__all__'): + #names = mod.__all__ + #else: + #names = [n for n in dir(mod) if n[0] != '_'] + #for k in names: + #if hasattr(mod, k): + #globals[k] = getattr(mod, k) -importAll('graphicsItems', globals(), locals()) -importAll('widgets', globals(), locals(), - excludes=['MatplotlibWidget', 'RawImageWidget', 'RemoteGraphicsView']) +# Dynamic imports are disabled. This causes too many problems. +#importAll('graphicsItems', globals(), locals()) +#importAll('widgets', globals(), locals(), + #excludes=['MatplotlibWidget', 'RawImageWidget', 'RemoteGraphicsView']) + +from .graphicsItems.VTickGroup import * +from .graphicsItems.GraphicsWidget import * +from .graphicsItems.ScaleBar import * +from .graphicsItems.PlotDataItem import * +from .graphicsItems.GraphItem import * +from .graphicsItems.TextItem import * +from .graphicsItems.GraphicsLayout import * +from .graphicsItems.UIGraphicsItem import * +from .graphicsItems.GraphicsObject import * +from .graphicsItems.PlotItem import * +from .graphicsItems.ROI import * +from .graphicsItems.InfiniteLine import * +from .graphicsItems.HistogramLUTItem import * +from .graphicsItems.GridItem import * +from .graphicsItems.GradientLegend import * +from .graphicsItems.GraphicsItem import * +from .graphicsItems.BarGraphItem import * +from .graphicsItems.ViewBox import * +from .graphicsItems.ArrowItem import * +from .graphicsItems.ImageItem import * +from .graphicsItems.AxisItem import * +from .graphicsItems.LabelItem import * +from .graphicsItems.CurvePoint import * +from .graphicsItems.GraphicsWidgetAnchor import * +from .graphicsItems.PlotCurveItem import * +from .graphicsItems.ButtonItem import * +from .graphicsItems.GradientEditorItem import * +from .graphicsItems.MultiPlotItem import * +from .graphicsItems.ErrorBarItem import * +from .graphicsItems.IsocurveItem import * +from .graphicsItems.LinearRegionItem import * +from .graphicsItems.FillBetweenItem import * +from .graphicsItems.LegendItem import * +from .graphicsItems.ScatterPlotItem import * +from .graphicsItems.ItemGroup import * + +from .widgets.MultiPlotWidget import * +from .widgets.ScatterPlotWidget import * +from .widgets.ColorMapWidget import * +from .widgets.FileDialog import * +from .widgets.ValueLabel import * +from .widgets.HistogramLUTWidget import * +from .widgets.CheckTable import * +from .widgets.BusyCursor import * +from .widgets.PlotWidget import * +from .widgets.ComboBox import * +from .widgets.GradientWidget import * +from .widgets.DataFilterWidget import * +from .widgets.SpinBox import * +from .widgets.JoystickButton import * +from .widgets.GraphicsLayoutWidget import * +from .widgets.TreeWidget import * +from .widgets.PathButton import * +from .widgets.VerticalLabel import * +from .widgets.FeedbackButton import * +from .widgets.ColorButton import * +from .widgets.DataTreeWidget import * +from .widgets.GraphicsView import * +from .widgets.LayoutWidget import * +from .widgets.TableWidget import * +from .widgets.ProgressDialog import * from .imageview import * from .WidgetGroup import * @@ -194,6 +257,7 @@ from .SignalProxy import * from .colormap import * from .ptime import time + ############################################################## ## PyQt and PySide both are prone to crashing on exit. ## There are two general approaches to dealing with this: diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index 3ff2af31..c6386655 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -60,6 +60,6 @@ class CSVExporter(Exporter): fd.write('\n') fd.close() - +CSVExporter.register() diff --git a/pyqtgraph/exporters/Exporter.py b/pyqtgraph/exporters/Exporter.py index 6371a3b9..281fbb9a 100644 --- a/pyqtgraph/exporters/Exporter.py +++ b/pyqtgraph/exporters/Exporter.py @@ -11,6 +11,14 @@ class Exporter(object): Abstract class used for exporting graphics to file / printer / whatever. """ allowCopy = False # subclasses set this to True if they can use the copy buffer + Exporters = [] + + @classmethod + def register(cls): + """ + Used to register Exporter classes to appear in the export dialog. + """ + Exporter.Exporters.append(cls) def __init__(self, item): """ @@ -20,9 +28,6 @@ class Exporter(object): object.__init__(self) self.item = item - #def item(self): - #return self.item - def parameters(self): """Return the parameters used to configure this exporter.""" raise Exception("Abstract method must be overridden in subclass.") @@ -131,45 +136,4 @@ class Exporter(object): return preItems + rootItem + postItems def render(self, painter, targetRect, sourceRect, item=None): - - #if item is None: - #item = self.item - #preItems = [] - #postItems = [] - #if isinstance(item, QtGui.QGraphicsScene): - #childs = [i for i in item.items() if i.parentItem() is None] - #rootItem = [] - #else: - #childs = item.childItems() - #rootItem = [item] - #childs.sort(lambda a,b: cmp(a.zValue(), b.zValue())) - #while len(childs) > 0: - #ch = childs.pop(0) - #if int(ch.flags() & ch.ItemStacksBehindParent) > 0 or (ch.zValue() < 0 and int(ch.flags() & ch.ItemNegativeZStacksBehindParent) > 0): - #preItems.extend(tree) - #else: - #postItems.extend(tree) - - #for ch in preItems: - #self.render(painter, sourceRect, targetRect, item=ch) - ### paint root here - #for ch in postItems: - #self.render(painter, sourceRect, targetRect, item=ch) - - self.getScene().render(painter, QtCore.QRectF(targetRect), QtCore.QRectF(sourceRect)) - - #def writePs(self, fileName=None, item=None): - #if fileName is None: - #self.fileSaveDialog(self.writeSvg, filter="PostScript (*.ps)") - #return - #if item is None: - #item = self - #printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution) - #printer.setOutputFileName(fileName) - #painter = QtGui.QPainter(printer) - #self.render(painter) - #painter.end() - - #def writeToPrinter(self): - #pass diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index 9fb77e2a..40a76fbd 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -98,4 +98,5 @@ class ImageExporter(Exporter): else: self.png.save(fileName) +ImageExporter.register() \ No newline at end of file diff --git a/pyqtgraph/exporters/Matplotlib.py b/pyqtgraph/exporters/Matplotlib.py index 76f878d2..42008468 100644 --- a/pyqtgraph/exporters/Matplotlib.py +++ b/pyqtgraph/exporters/Matplotlib.py @@ -57,6 +57,7 @@ class MatplotlibExporter(Exporter): else: raise Exception("Matplotlib export currently only works with plot items") +MatplotlibExporter.register() class MatplotlibWindow(QtGui.QMainWindow): @@ -72,3 +73,5 @@ class MatplotlibWindow(QtGui.QMainWindow): def closeEvent(self, ev): MatplotlibExporter.windows.remove(self) + + diff --git a/pyqtgraph/exporters/PrintExporter.py b/pyqtgraph/exporters/PrintExporter.py index 5b31b45d..ef35c2f8 100644 --- a/pyqtgraph/exporters/PrintExporter.py +++ b/pyqtgraph/exporters/PrintExporter.py @@ -63,3 +63,6 @@ class PrintExporter(Exporter): finally: self.setExportMode(False) painter.end() + + +#PrintExporter.register() diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 19a7a6a7..425f48e9 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -404,6 +404,10 @@ def correctCoordinates(node, item): if removeTransform: grp.removeAttribute('transform') + +SVGExporter.register() + + def itemTransform(item, root): ## Return the transformation mapping item to root ## (actually to parent coordinate system of root) diff --git a/pyqtgraph/exporters/__init__.py b/pyqtgraph/exporters/__init__.py index 3f3c1f1d..99bea53a 100644 --- a/pyqtgraph/exporters/__init__.py +++ b/pyqtgraph/exporters/__init__.py @@ -1,27 +1,24 @@ -Exporters = [] -from pyqtgraph import importModules -#from .. import frozenSupport -import os -d = os.path.split(__file__)[0] -#files = [] -#for f in frozenSupport.listdir(d): - #if frozenSupport.isdir(os.path.join(d, f)) and f != '__pycache__': - #files.append(f) - #elif f[-3:] == '.py' and f not in ['__init__.py', 'Exporter.py']: - #files.append(f[:-3]) - -#for modName in files: - #mod = __import__(modName, globals(), locals(), fromlist=['*']) -for mod in importModules('', globals(), locals(), excludes=['Exporter']).values(): - if hasattr(mod, '__all__'): - names = mod.__all__ - else: - names = [n for n in dir(mod) if n[0] != '_'] - for k in names: - if hasattr(mod, k): - Exporters.append(getattr(mod, k)) +#Exporters = [] +#from pyqtgraph import importModules +#import os +#d = os.path.split(__file__)[0] +#for mod in importModules('', globals(), locals(), excludes=['Exporter']).values(): + #if hasattr(mod, '__all__'): + #names = mod.__all__ + #else: + #names = [n for n in dir(mod) if n[0] != '_'] + #for k in names: + #if hasattr(mod, k): + #Exporters.append(getattr(mod, k)) + +import Exporter +from .ImageExporter import * +from .SVGExporter import * +from .Matplotlib import * +from .CSVExporter import * +from .PrintExporter import * def listExporters(): - return Exporters[:] + return Exporter.Exporter.Exporters[:] diff --git a/pyqtgraph/opengl/__init__.py b/pyqtgraph/opengl/__init__.py index 5345e187..d10932a5 100644 --- a/pyqtgraph/opengl/__init__.py +++ b/pyqtgraph/opengl/__init__.py @@ -1,28 +1,20 @@ from .GLViewWidget import GLViewWidget -from pyqtgraph import importAll -#import os -#def importAll(path): - #d = os.path.join(os.path.split(__file__)[0], path) - #files = [] - #for f in os.listdir(d): - #if os.path.isdir(os.path.join(d, f)) and f != '__pycache__': - #files.append(f) - #elif f[-3:] == '.py' and f != '__init__.py': - #files.append(f[:-3]) - - #for modName in files: - #mod = __import__(path+"."+modName, globals(), locals(), fromlist=['*']) - #if hasattr(mod, '__all__'): - #names = mod.__all__ - #else: - #names = [n for n in dir(mod) if n[0] != '_'] - #for k in names: - #if hasattr(mod, k): - #globals()[k] = getattr(mod, k) +## dynamic imports cause too many problems. +#from pyqtgraph import importAll +#importAll('items', globals(), locals()) + +from .items.GLGridItem import * +from .items.GLBarGraphItem import * +from .items.GLScatterPlotItem import * +from .items.GLMeshItem import * +from .items.GLLinePlotItem import * +from .items.GLAxisItem import * +from .items.GLImageItem import * +from .items.GLSurfacePlotItem import * +from .items.GLBoxItem import * +from .items.GLVolumeItem import * -importAll('items', globals(), locals()) -\ from .MeshData import MeshData ## for backward compatibility: #MeshData.MeshData = MeshData ## breaks autodoc. From 59f07a03eef0839b1d7ddc71700dd996044f7371 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 22 Nov 2013 20:09:59 -0500 Subject: [PATCH 036/268] python3 fix --- pyqtgraph/exporters/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/__init__.py b/pyqtgraph/exporters/__init__.py index 99bea53a..e2a81bc2 100644 --- a/pyqtgraph/exporters/__init__.py +++ b/pyqtgraph/exporters/__init__.py @@ -11,7 +11,7 @@ #if hasattr(mod, k): #Exporters.append(getattr(mod, k)) -import Exporter +from .Exporter import Exporter from .ImageExporter import * from .SVGExporter import * from .Matplotlib import * @@ -20,5 +20,5 @@ from .PrintExporter import * def listExporters(): - return Exporter.Exporter.Exporters[:] + return Exporter.Exporters[:] From 19be6959f3c14362f4f899b7bab7efa58584fe54 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 15 Dec 2013 23:50:11 -0500 Subject: [PATCH 037/268] Flowchart: * Replaced dynamic imports with static * Added NodeLibrary allowing multiple customized collections of Node types --- examples/FlowchartCustomNode.py | 28 ++-- pyqtgraph/flowchart/Flowchart.py | 15 ++- pyqtgraph/flowchart/NodeLibrary.py | 84 ++++++++++++ pyqtgraph/flowchart/library/__init__.py | 169 ++++++++++++------------ 4 files changed, 197 insertions(+), 99 deletions(-) create mode 100644 pyqtgraph/flowchart/NodeLibrary.py diff --git a/examples/FlowchartCustomNode.py b/examples/FlowchartCustomNode.py index bce37982..25ea5c77 100644 --- a/examples/FlowchartCustomNode.py +++ b/examples/FlowchartCustomNode.py @@ -83,9 +83,8 @@ class ImageViewNode(Node): else: self.view.setImage(data) -## register the class so it will appear in the menu of node types. -## It will appear in the 'display' sub-menu. -fclib.registerNodeType(ImageViewNode, [('Display',)]) + + ## We will define an unsharp masking filter node as a subclass of CtrlNode. ## CtrlNode is just a convenience class that automatically creates its @@ -113,12 +112,25 @@ class UnsharpMaskNode(CtrlNode): strength = self.ctrls['strength'].value() output = dataIn - (strength * scipy.ndimage.gaussian_filter(dataIn, (sigma,sigma))) return {'dataOut': output} + + +## To make our custom node classes available in the flowchart context menu, +## we can either register them with the default node library or make a +## new library. + -## register the class so it will appear in the menu of node types. -## It will appear in a new 'image' sub-menu. -fclib.registerNodeType(UnsharpMaskNode, [('Image',)]) - - +## Method 1: Register to global default library: +#fclib.registerNodeType(ImageViewNode, [('Display',)]) +#fclib.registerNodeType(UnsharpMaskNode, [('Image',)]) + +## Method 2: If we want to make our custom node available only to this flowchart, +## then instead of registering the node type globally, we can create a new +## NodeLibrary: +library = fclib.LIBRARY.copy() # start with the default node set +library.addNodeType(ImageViewNode, [('Display',)]) +library.addNodeType(UnsharpMaskNode, [('Image',)]) +fc.setLibrary(library) + ## Now we will programmatically add nodes to define the function of the flowchart. ## Normally, the user will do this manually or by loading a pre-generated diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 81f9e163..f566e97c 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -14,7 +14,7 @@ else: from .Terminal import Terminal from numpy import ndarray -from . import library +from .library import LIBRARY from pyqtgraph.debug import printExc import pyqtgraph.configfile as configfile import pyqtgraph.dockarea as dockarea @@ -67,7 +67,8 @@ class Flowchart(Node): sigChartLoaded = QtCore.Signal() sigStateChanged = QtCore.Signal() - def __init__(self, terminals=None, name=None, filePath=None): + def __init__(self, terminals=None, name=None, filePath=None, library=None): + self.library = library or LIBRARY if name is None: name = "Flowchart" if terminals is None: @@ -105,6 +106,10 @@ class Flowchart(Node): for name, opts in terminals.items(): self.addTerminal(name, **opts) + def setLibrary(self, lib): + self.library = lib + self.widget().chartWidget.buildMenu() + def setInput(self, **args): """Set the input values of the flowchart. This will automatically propagate the new values throughout the flowchart, (possibly) causing the output to change. @@ -194,7 +199,7 @@ class Flowchart(Node): break n += 1 - node = library.getNodeType(nodeType)(name) + node = self.library.getNodeType(nodeType)(name) self.addNode(node, name, pos) return node @@ -846,13 +851,13 @@ class FlowchartWidget(dockarea.DockArea): self.nodeMenu.triggered.disconnect(self.nodeMenuTriggered) self.nodeMenu = None self.subMenus = [] - library.loadLibrary(reloadLibs=True) + self.chart.library.reload() self.buildMenu() def buildMenu(self, pos=None): self.nodeMenu = QtGui.QMenu() self.subMenus = [] - for section, nodes in library.getNodeTree().items(): + for section, nodes in self.chart.library.getNodeTree().items(): menu = QtGui.QMenu(section) self.nodeMenu.addMenu(menu) for name in nodes: diff --git a/pyqtgraph/flowchart/NodeLibrary.py b/pyqtgraph/flowchart/NodeLibrary.py new file mode 100644 index 00000000..356848f9 --- /dev/null +++ b/pyqtgraph/flowchart/NodeLibrary.py @@ -0,0 +1,84 @@ +from pyqtgraph.pgcollections import OrderedDict +from Node import Node + +def isNodeClass(cls): + try: + if not issubclass(cls, Node): + return False + except: + return False + return hasattr(cls, 'nodeName') + + + +class NodeLibrary: + """ + A library of flowchart Node types. Custom libraries may be built to provide + each flowchart with a specific set of allowed Node types. + """ + + def __init__(self): + self.nodeList = OrderedDict() + self.nodeTree = OrderedDict() + + def addNodeType(self, nodeClass, paths, override=False): + """ + Register a new node type. If the type's name is already in use, + an exception will be raised (unless override=True). + + Arguments: + + nodeClass - a subclass of Node (must have typ.nodeName) + paths - list of tuples specifying the location(s) this + type will appear in the library tree. + override - if True, overwrite any class having the same name + """ + if not isNodeClass(nodeClass): + raise Exception("Object %s is not a Node subclass" % str(nodeClass)) + + name = nodeClass.nodeName + if not override and name in self.nodeList: + raise Exception("Node type name '%s' is already registered." % name) + + self.nodeList[name] = nodeClass + for path in paths: + root = self.nodeTree + for n in path: + if n not in root: + root[n] = OrderedDict() + root = root[n] + root[name] = nodeClass + + def getNodeType(self, name): + try: + return self.nodeList[name] + except KeyError: + raise Exception("No node type called '%s'" % name) + + def getNodeTree(self): + return self.nodeTree + + def copy(self): + """ + Return a copy of this library. + """ + lib = NodeLibrary() + lib.nodeList = self.nodeList.copy() + lib.nodeTree = self.treeCopy(self.nodeTree) + return lib + + @staticmethod + def treeCopy(tree): + copy = OrderedDict() + for k,v in tree.items(): + if isNodeClass(v): + copy[k] = v + else: + copy[k] = NodeLibrary.treeCopy(v) + return copy + + def reload(self): + """ + Reload Node classes in this library. + """ + raise NotImplementedError() diff --git a/pyqtgraph/flowchart/library/__init__.py b/pyqtgraph/flowchart/library/__init__.py index 1e44edff..3ab4767e 100644 --- a/pyqtgraph/flowchart/library/__init__.py +++ b/pyqtgraph/flowchart/library/__init__.py @@ -1,103 +1,100 @@ # -*- coding: utf-8 -*- from pyqtgraph.pgcollections import OrderedDict -from pyqtgraph import importModules +#from pyqtgraph import importModules import os, types from pyqtgraph.debug import printExc -from ..Node import Node +#from ..Node import Node +from ..NodeLibrary import NodeLibrary, isNodeClass import pyqtgraph.reload as reload -NODE_LIST = OrderedDict() ## maps name:class for all registered Node subclasses -NODE_TREE = OrderedDict() ## categorized tree of Node subclasses +# Build default library +LIBRARY = NodeLibrary() -def getNodeType(name): - try: - return NODE_LIST[name] - except KeyError: - raise Exception("No node type called '%s'" % name) +# For backward compatibility, expose the default library's properties here: +NODE_LIST = LIBRARY.nodeList +NODE_TREE = LIBRARY.nodeTree +registerNodeType = LIBRARY.addNodeType +getNodeTree = LIBRARY.getNodeTree +getNodeType = LIBRARY.getNodeType -def getNodeTree(): - return NODE_TREE - -def registerNodeType(cls, paths, override=False): - """ - Register a new node type. If the type's name is already in use, - an exception will be raised (unless override=True). +# Add all nodes to the default library +for modName in ['Data', 'Display', 'Filters', 'Operators']: + mod = __import__(modName, globals(), locals(), [], -1) + nodes = [getattr(mod, name) for name in dir(mod) if isNodeClass(getattr(mod, name))] + for node in nodes: + LIBRARY.addNodeType(node, [(modName,)]) - Arguments: - cls - a subclass of Node (must have typ.nodeName) - paths - list of tuples specifying the location(s) this - type will appear in the library tree. - override - if True, overwrite any class having the same name - """ - if not isNodeClass(cls): - raise Exception("Object %s is not a Node subclass" % str(cls)) +#NODE_LIST = OrderedDict() ## maps name:class for all registered Node subclasses +#NODE_TREE = OrderedDict() ## categorized tree of Node subclasses + +#def getNodeType(name): + #try: + #return NODE_LIST[name] + #except KeyError: + #raise Exception("No node type called '%s'" % name) + +#def getNodeTree(): + #return NODE_TREE + +#def registerNodeType(cls, paths, override=False): + #""" + #Register a new node type. If the type's name is already in use, + #an exception will be raised (unless override=True). - name = cls.nodeName - if not override and name in NODE_LIST: - raise Exception("Node type name '%s' is already registered." % name) + #Arguments: + #cls - a subclass of Node (must have typ.nodeName) + #paths - list of tuples specifying the location(s) this + #type will appear in the library tree. + #override - if True, overwrite any class having the same name + #""" + #if not isNodeClass(cls): + #raise Exception("Object %s is not a Node subclass" % str(cls)) - NODE_LIST[name] = cls - for path in paths: - root = NODE_TREE - for n in path: - if n not in root: - root[n] = OrderedDict() - root = root[n] - root[name] = cls - - - -def isNodeClass(cls): - try: - if not issubclass(cls, Node): - return False - except: - return False - return hasattr(cls, 'nodeName') - -def loadLibrary(reloadLibs=False, libPath=None): - """Import all Node subclasses found within files in the library module.""" - - global NODE_LIST, NODE_TREE - #if libPath is None: - #libPath = os.path.dirname(os.path.abspath(__file__)) + #name = cls.nodeName + #if not override and name in NODE_LIST: + #raise Exception("Node type name '%s' is already registered." % name) - if reloadLibs: - reload.reloadAll(libPath) + #NODE_LIST[name] = cls + #for path in paths: + #root = NODE_TREE + #for n in path: + #if n not in root: + #root[n] = OrderedDict() + #root = root[n] + #root[name] = cls + + + +#def isNodeClass(cls): + #try: + #if not issubclass(cls, Node): + #return False + #except: + #return False + #return hasattr(cls, 'nodeName') + +#def loadLibrary(reloadLibs=False, libPath=None): + #"""Import all Node subclasses found within files in the library module.""" + + #global NODE_LIST, NODE_TREE + + #if reloadLibs: + #reload.reloadAll(libPath) - mods = importModules('', globals(), locals()) - #for f in frozenSupport.listdir(libPath): - #pathName, ext = os.path.splitext(f) - #if ext not in ('.py', '.pyc') or '__init__' in pathName or '__pycache__' in pathName: - #continue - #try: - ##print "importing from", f - #mod = __import__(pathName, globals(), locals()) - #except: - #printExc("Error loading flowchart library %s:" % pathName) - #continue + #mods = importModules('', globals(), locals()) - for name, mod in mods.items(): - nodes = [] - for n in dir(mod): - o = getattr(mod, n) - if isNodeClass(o): - #print " ", str(o) - registerNodeType(o, [(name,)], override=reloadLibs) - #nodes.append((o.nodeName, o)) - #if len(nodes) > 0: - #NODE_TREE[name] = OrderedDict(nodes) - #NODE_LIST.extend(nodes) - #NODE_LIST = OrderedDict(NODE_LIST) + #for name, mod in mods.items(): + #nodes = [] + #for n in dir(mod): + #o = getattr(mod, n) + #if isNodeClass(o): + #registerNodeType(o, [(name,)], override=reloadLibs) -def reloadLibrary(): - loadLibrary(reloadLibs=True) +#def reloadLibrary(): + #loadLibrary(reloadLibs=True) -loadLibrary() -#NODE_LIST = [] -#for o in locals().values(): - #if type(o) is type(AddNode) and issubclass(o, Node) and o is not Node and hasattr(o, 'nodeName'): - #NODE_LIST.append((o.nodeName, o)) -#NODE_LIST.sort(lambda a,b: cmp(a[0], b[0])) -#NODE_LIST = OrderedDict(NODE_LIST) \ No newline at end of file +#loadLibrary() + + + From 90b6b5b50125b90417d31d9a8d655ad3ffe578ad Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 21 Dec 2013 23:41:37 -0500 Subject: [PATCH 038/268] python 3 fixes --- pyqtgraph/flowchart/NodeLibrary.py | 2 +- pyqtgraph/flowchart/library/__init__.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/flowchart/NodeLibrary.py b/pyqtgraph/flowchart/NodeLibrary.py index 356848f9..20d0085e 100644 --- a/pyqtgraph/flowchart/NodeLibrary.py +++ b/pyqtgraph/flowchart/NodeLibrary.py @@ -1,5 +1,5 @@ from pyqtgraph.pgcollections import OrderedDict -from Node import Node +from .Node import Node def isNodeClass(cls): try: diff --git a/pyqtgraph/flowchart/library/__init__.py b/pyqtgraph/flowchart/library/__init__.py index 3ab4767e..32a17b58 100644 --- a/pyqtgraph/flowchart/library/__init__.py +++ b/pyqtgraph/flowchart/library/__init__.py @@ -19,11 +19,13 @@ getNodeTree = LIBRARY.getNodeTree getNodeType = LIBRARY.getNodeType # Add all nodes to the default library -for modName in ['Data', 'Display', 'Filters', 'Operators']: - mod = __import__(modName, globals(), locals(), [], -1) +from . import Data, Display, Filters, Operators +for mod in [Data, Display, Filters, Operators]: + #mod = getattr(__import__('', fromlist=[modName], level=1), modName) + #mod = __import__(modName, level=1) nodes = [getattr(mod, name) for name in dir(mod) if isNodeClass(getattr(mod, name))] for node in nodes: - LIBRARY.addNodeType(node, [(modName,)]) + LIBRARY.addNodeType(node, [(mod.__name__.split('.')[-1],)]) #NODE_LIST = OrderedDict() ## maps name:class for all registered Node subclasses #NODE_TREE = OrderedDict() ## categorized tree of Node subclasses From f6307344534c309b44c285398e0d34053af67a99 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 22 Dec 2013 02:08:39 -0500 Subject: [PATCH 039/268] Switching to relative imports to allow pyqtgraph to be imported under other names. finished top-level files and graphicsItems --- pyqtgraph/SRTTransform.py | 5 ++-- pyqtgraph/SRTTransform3D.py | 26 +++++++++---------- pyqtgraph/ThreadsafeTimer.py | 2 +- pyqtgraph/Transform3D.py | 4 +-- pyqtgraph/colormap.py | 2 +- pyqtgraph/functions.py | 14 +++++----- pyqtgraph/graphicsItems/ArrowItem.py | 4 +-- pyqtgraph/graphicsItems/AxisItem.py | 20 +++++++------- pyqtgraph/graphicsItems/BarGraphItem.py | 16 +++++++----- pyqtgraph/graphicsItems/ButtonItem.py | 2 +- pyqtgraph/graphicsItems/CurvePoint.py | 4 +-- pyqtgraph/graphicsItems/ErrorBarItem.py | 9 ++++--- pyqtgraph/graphicsItems/FillBetweenItem.py | 11 ++++---- pyqtgraph/graphicsItems/GradientEditorItem.py | 10 +++---- pyqtgraph/graphicsItems/GradientLegend.py | 4 +-- pyqtgraph/graphicsItems/GraphItem.py | 18 ++++++------- pyqtgraph/graphicsItems/GraphicsItem.py | 10 +++---- pyqtgraph/graphicsItems/GraphicsLayout.py | 4 +-- pyqtgraph/graphicsItems/GraphicsObject.py | 2 +- pyqtgraph/graphicsItems/GraphicsWidget.py | 4 +-- pyqtgraph/graphicsItems/GridItem.py | 6 ++--- pyqtgraph/graphicsItems/HistogramLUTItem.py | 10 +++---- pyqtgraph/graphicsItems/ImageItem.py | 6 ++--- pyqtgraph/graphicsItems/InfiniteLine.py | 6 ++--- pyqtgraph/graphicsItems/IsocurveItem.py | 4 +-- pyqtgraph/graphicsItems/ItemGroup.py | 2 +- pyqtgraph/graphicsItems/LabelItem.py | 8 +++--- pyqtgraph/graphicsItems/LegendItem.py | 13 +++++----- pyqtgraph/graphicsItems/LinearRegionItem.py | 6 ++--- pyqtgraph/graphicsItems/PlotCurveItem.py | 18 ++++++------- pyqtgraph/graphicsItems/PlotDataItem.py | 12 ++++----- pyqtgraph/graphicsItems/ROI.py | 8 +++--- pyqtgraph/graphicsItems/ScaleBar.py | 8 +++--- pyqtgraph/graphicsItems/ScatterPlotItem.py | 18 ++++++------- pyqtgraph/graphicsItems/TextItem.py | 16 ++++++------ pyqtgraph/graphicsItems/UIGraphicsItem.py | 2 +- pyqtgraph/graphicsItems/VTickGroup.py | 18 ++----------- 37 files changed, 161 insertions(+), 171 deletions(-) diff --git a/pyqtgraph/SRTTransform.py b/pyqtgraph/SRTTransform.py index efb24f60..23281343 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -2,7 +2,6 @@ from .Qt import QtCore, QtGui from .Point import Point import numpy as np -import pyqtgraph as pg class SRTTransform(QtGui.QTransform): """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate @@ -77,7 +76,7 @@ class SRTTransform(QtGui.QTransform): self.update() def setFromMatrix4x4(self, m): - m = pg.SRTTransform3D(m) + m = SRTTransform3D(m) angle, axis = m.getRotation() if angle != 0 and (axis[0] != 0 or axis[1] != 0 or axis[2] != 1): print("angle: %s axis: %s" % (str(angle), str(axis))) @@ -256,4 +255,4 @@ if __name__ == '__main__': w1.sigRegionChanged.connect(update) #w2.sigRegionChanged.connect(update2) - \ No newline at end of file +from .SRTTransform3D import SRTTransform3D diff --git a/pyqtgraph/SRTTransform3D.py b/pyqtgraph/SRTTransform3D.py index 7d87dcb8..417190e1 100644 --- a/pyqtgraph/SRTTransform3D.py +++ b/pyqtgraph/SRTTransform3D.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- from .Qt import QtCore, QtGui from .Vector import Vector -from .SRTTransform import SRTTransform -import pyqtgraph as pg +from .Transform3D import Transform3D +from .Vector import Vector import numpy as np import scipy.linalg -class SRTTransform3D(pg.Transform3D): +class SRTTransform3D(Transform3D): """4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate This transform has no shear; angles are always preserved. """ def __init__(self, init=None): - pg.Transform3D.__init__(self) + Transform3D.__init__(self) self.reset() if init is None: return @@ -44,14 +44,14 @@ class SRTTransform3D(pg.Transform3D): def getScale(self): - return pg.Vector(self._state['scale']) + return Vector(self._state['scale']) def getRotation(self): """Return (angle, axis) of rotation""" - return self._state['angle'], pg.Vector(self._state['axis']) + return self._state['angle'], Vector(self._state['axis']) def getTranslation(self): - return pg.Vector(self._state['pos']) + return Vector(self._state['pos']) def reset(self): self._state = { @@ -169,7 +169,7 @@ class SRTTransform3D(pg.Transform3D): def as2D(self): """Return a QTransform representing the x,y portion of this transform (if possible)""" - return pg.SRTTransform(self) + return SRTTransform(self) #def __div__(self, t): #"""A / B == B^-1 * A""" @@ -202,11 +202,11 @@ class SRTTransform3D(pg.Transform3D): self.update() def update(self): - pg.Transform3D.setToIdentity(self) + Transform3D.setToIdentity(self) ## modifications to the transform are multiplied on the right, so we need to reverse order here. - pg.Transform3D.translate(self, *self._state['pos']) - pg.Transform3D.rotate(self, self._state['angle'], *self._state['axis']) - pg.Transform3D.scale(self, *self._state['scale']) + Transform3D.translate(self, *self._state['pos']) + Transform3D.rotate(self, self._state['angle'], *self._state['axis']) + Transform3D.scale(self, *self._state['scale']) def __repr__(self): return str(self.saveState()) @@ -311,4 +311,4 @@ if __name__ == '__main__': w1.sigRegionChanged.connect(update) #w2.sigRegionChanged.connect(update2) - \ No newline at end of file +from .SRTTransform import SRTTransform diff --git a/pyqtgraph/ThreadsafeTimer.py b/pyqtgraph/ThreadsafeTimer.py index f2de9791..201469de 100644 --- a/pyqtgraph/ThreadsafeTimer.py +++ b/pyqtgraph/ThreadsafeTimer.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtCore, QtGui +from .Qt import QtCore, QtGui class ThreadsafeTimer(QtCore.QObject): """ diff --git a/pyqtgraph/Transform3D.py b/pyqtgraph/Transform3D.py index aa948e28..43b12de3 100644 --- a/pyqtgraph/Transform3D.py +++ b/pyqtgraph/Transform3D.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from .Qt import QtCore, QtGui -import pyqtgraph as pg +from . import functions as fn import numpy as np class Transform3D(QtGui.QMatrix4x4): @@ -26,7 +26,7 @@ class Transform3D(QtGui.QMatrix4x4): Extends QMatrix4x4.map() to allow mapping (3, ...) arrays of coordinates """ if isinstance(obj, np.ndarray) and obj.ndim >= 2 and obj.shape[0] in (2,3): - return pg.transformCoordinates(self, obj) + return fn.transformCoordinates(self, obj) else: return QtGui.QMatrix4x4.map(self, obj) diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index d6169209..cb1e882e 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -1,6 +1,6 @@ import numpy as np import scipy.interpolate -from pyqtgraph.Qt import QtGui, QtCore +from .Qt import QtGui, QtCore class ColorMap(object): """ diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 80e61404..12588cf1 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -24,7 +24,7 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' from .Qt import QtGui, QtCore, USE_PYSIDE -import pyqtgraph as pg +from . import getConfigOption, setConfigOptions import numpy as np import decimal, re import ctypes @@ -33,11 +33,11 @@ import sys, struct try: import scipy.ndimage HAVE_SCIPY = True - if pg.getConfigOption('useWeave'): + if getConfigOption('useWeave'): try: import scipy.weave except ImportError: - pg.setConfigOptions(useWeave=False) + setConfigOptions(useWeave=False) except ImportError: HAVE_SCIPY = False @@ -620,7 +620,7 @@ def rescaleData(data, scale, offset, dtype=None): dtype = np.dtype(dtype) try: - if not pg.getConfigOption('useWeave'): + if not getConfigOption('useWeave'): raise Exception('Weave is disabled; falling back to slower version.') ## require native dtype when using weave @@ -647,10 +647,10 @@ def rescaleData(data, scale, offset, dtype=None): newData = newData.astype(dtype) data = newData.reshape(data.shape) except: - if pg.getConfigOption('useWeave'): - if pg.getConfigOption('weaveDebug'): + if getConfigOption('useWeave'): + if getConfigOption('weaveDebug'): debug.printExc("Error; disabling weave.") - pg.setConfigOption('useWeave', False) + setConfigOptions(useWeave=False) #p = np.poly1d([scale, -offset*scale]) #data = p(data).astype(dtype) diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index dcede02a..b15fc664 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -1,5 +1,5 @@ -from pyqtgraph.Qt import QtGui, QtCore -import pyqtgraph.functions as fn +from ..Qt import QtGui, QtCore +from .. import functions as fn import numpy as np __all__ = ['ArrowItem'] diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 3dd98cef..1d0b36b6 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -1,11 +1,11 @@ -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph.python2_3 import asUnicode +from ..Qt import QtGui, QtCore +from ..python2_3 import asUnicode import numpy as np -from pyqtgraph.Point import Point -import pyqtgraph.debug as debug +from ..Point import Point +from .. import debug as debug import weakref -import pyqtgraph.functions as fn -import pyqtgraph as pg +from .. import functions as fn +from .. import getConfigOption from .GraphicsWidget import GraphicsWidget __all__ = ['AxisItem'] @@ -268,8 +268,8 @@ class AxisItem(GraphicsWidget): def pen(self): if self._pen is None: - return fn.mkPen(pg.getConfigOption('foreground')) - return pg.mkPen(self._pen) + return fn.mkPen(getConfigOption('foreground')) + return fn.mkPen(self._pen) def setPen(self, pen): """ @@ -280,8 +280,8 @@ class AxisItem(GraphicsWidget): self._pen = pen self.picture = None if pen is None: - pen = pg.getConfigOption('foreground') - self.labelStyle['color'] = '#' + pg.colorStr(pg.mkPen(pen).color())[:6] + pen = getConfigOption('foreground') + self.labelStyle['color'] = '#' + fn.colorStr(fn.mkPen(pen).color())[:6] self.setLabel() self.update() diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index 0527e9f1..9f9dbcde 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -1,8 +1,10 @@ -import pyqtgraph as pg -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .GraphicsObject import GraphicsObject +from .. import getConfigOption +from .. import functions as fn import numpy as np + __all__ = ['BarGraphItem'] class BarGraphItem(GraphicsObject): @@ -61,7 +63,7 @@ class BarGraphItem(GraphicsObject): pens = self.opts['pens'] if pen is None and pens is None: - pen = pg.getConfigOption('foreground') + pen = getConfigOption('foreground') brush = self.opts['brush'] brushes = self.opts['brushes'] @@ -112,13 +114,13 @@ class BarGraphItem(GraphicsObject): raise Exception('must specify either y1 or height') height = y1 - y0 - p.setPen(pg.mkPen(pen)) - p.setBrush(pg.mkBrush(brush)) + p.setPen(fn.mkPen(pen)) + p.setBrush(fn.mkBrush(brush)) for i in range(len(x0)): if pens is not None: - p.setPen(pg.mkPen(pens[i])) + p.setPen(fn.mkPen(pens[i])) if brushes is not None: - p.setBrush(pg.mkBrush(brushes[i])) + p.setBrush(fn.mkBrush(brushes[i])) if np.isscalar(y0): y = y0 diff --git a/pyqtgraph/graphicsItems/ButtonItem.py b/pyqtgraph/graphicsItems/ButtonItem.py index 741f2666..1c796823 100644 --- a/pyqtgraph/graphicsItems/ButtonItem.py +++ b/pyqtgraph/graphicsItems/ButtonItem.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .GraphicsObject import GraphicsObject __all__ = ['ButtonItem'] diff --git a/pyqtgraph/graphicsItems/CurvePoint.py b/pyqtgraph/graphicsItems/CurvePoint.py index 668830f7..f981bdf8 100644 --- a/pyqtgraph/graphicsItems/CurvePoint.py +++ b/pyqtgraph/graphicsItems/CurvePoint.py @@ -1,7 +1,7 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from . import ArrowItem import numpy as np -from pyqtgraph.Point import Point +from ..Point import Point import weakref from .GraphicsObject import GraphicsObject diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 656b9e2e..7b681389 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -1,6 +1,7 @@ -import pyqtgraph as pg -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .GraphicsObject import GraphicsObject +from .. import getConfigOption +from .. import functions as fn __all__ = ['ErrorBarItem'] @@ -121,8 +122,8 @@ class ErrorBarItem(GraphicsObject): self.drawPath() pen = self.opts['pen'] if pen is None: - pen = pg.getConfigOption('foreground') - p.setPen(pg.mkPen(pen)) + pen = getConfigOption('foreground') + p.setPen(fn.mkPen(pen)) p.drawPath(self.path) def boundingRect(self): diff --git a/pyqtgraph/graphicsItems/FillBetweenItem.py b/pyqtgraph/graphicsItems/FillBetweenItem.py index e0011177..13e5fa6b 100644 --- a/pyqtgraph/graphicsItems/FillBetweenItem.py +++ b/pyqtgraph/graphicsItems/FillBetweenItem.py @@ -1,23 +1,24 @@ -import pyqtgraph as pg +from ..Qt import QtGui +from .. import functions as fn -class FillBetweenItem(pg.QtGui.QGraphicsPathItem): +class FillBetweenItem(QtGui.QGraphicsPathItem): """ GraphicsItem filling the space between two PlotDataItems. """ def __init__(self, p1, p2, brush=None): - pg.QtGui.QGraphicsPathItem.__init__(self) + QtGui.QGraphicsPathItem.__init__(self) self.p1 = p1 self.p2 = p2 p1.sigPlotChanged.connect(self.updatePath) p2.sigPlotChanged.connect(self.updatePath) if brush is not None: - self.setBrush(pg.mkBrush(brush)) + self.setBrush(fn.mkBrush(brush)) self.setZValue(min(p1.zValue(), p2.zValue())-1) self.updatePath() def updatePath(self): p1 = self.p1.curve.path p2 = self.p2.curve.path - path = pg.QtGui.QPainterPath() + path = QtGui.QPainterPath() path.addPolygon(p1.toSubpathPolygons()[0] + p2.toReversed().toSubpathPolygons()[0]) self.setPath(path) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index 955106d8..f5158a74 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -1,11 +1,11 @@ -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph.python2_3 import sortList -import pyqtgraph.functions as fn +from ..Qt import QtGui, QtCore +from ..python2_3 import sortList +from .. import functions as fn from .GraphicsObject import GraphicsObject from .GraphicsWidget import GraphicsWidget import weakref -from pyqtgraph.pgcollections import OrderedDict -from pyqtgraph.colormap import ColorMap +from ..pgcollections import OrderedDict +from ..colormap import ColorMap import numpy as np diff --git a/pyqtgraph/graphicsItems/GradientLegend.py b/pyqtgraph/graphicsItems/GradientLegend.py index 4528b7ed..28c2cd63 100644 --- a/pyqtgraph/graphicsItems/GradientLegend.py +++ b/pyqtgraph/graphicsItems/GradientLegend.py @@ -1,6 +1,6 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .UIGraphicsItem import * -import pyqtgraph.functions as fn +from .. import functions as fn __all__ = ['GradientLegend'] diff --git a/pyqtgraph/graphicsItems/GraphItem.py b/pyqtgraph/graphicsItems/GraphItem.py index b1f34baa..63b5afbd 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -1,7 +1,7 @@ from .. import functions as fn from .GraphicsObject import GraphicsObject from .ScatterPlotItem import ScatterPlotItem -import pyqtgraph as pg +from ..Qt import QtGui, QtCore import numpy as np __all__ = ['GraphItem'] @@ -71,11 +71,11 @@ class GraphItem(GraphicsObject): self.picture = None def generatePicture(self): - self.picture = pg.QtGui.QPicture() + self.picture = QtGui.QPicture() if self.pen is None or self.pos is None or self.adjacency is None: return - p = pg.QtGui.QPainter(self.picture) + p = QtGui.QPainter(self.picture) try: pts = self.pos[self.adjacency] pen = self.pen @@ -86,14 +86,14 @@ class GraphItem(GraphicsObject): if np.any(pen != lastPen): lastPen = pen if pen.dtype.fields is None: - p.setPen(pg.mkPen(color=(pen[0], pen[1], pen[2], pen[3]), width=1)) + p.setPen(fn.mkPen(color=(pen[0], pen[1], pen[2], pen[3]), width=1)) else: - p.setPen(pg.mkPen(color=(pen['red'], pen['green'], pen['blue'], pen['alpha']), width=pen['width'])) - p.drawLine(pg.QtCore.QPointF(*pts[i][0]), pg.QtCore.QPointF(*pts[i][1])) + p.setPen(fn.mkPen(color=(pen['red'], pen['green'], pen['blue'], pen['alpha']), width=pen['width'])) + p.drawLine(QtCore.QPointF(*pts[i][0]), QtCore.QPointF(*pts[i][1])) else: if pen == 'default': - pen = pg.getConfigOption('foreground') - p.setPen(pg.mkPen(pen)) + pen = getConfigOption('foreground') + p.setPen(fn.mkPen(pen)) pts = pts.reshape((pts.shape[0]*pts.shape[1], pts.shape[2])) path = fn.arrayToQPath(x=pts[:,0], y=pts[:,1], connect='pairs') p.drawPath(path) @@ -103,7 +103,7 @@ class GraphItem(GraphicsObject): def paint(self, p, *args): if self.picture == None: self.generatePicture() - if pg.getConfigOption('antialias') is True: + if getConfigOption('antialias') is True: p.setRenderHint(p.Antialiasing) self.picture.play(p) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 19cddd8a..8d2238b8 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -1,9 +1,9 @@ -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph.GraphicsScene import GraphicsScene -from pyqtgraph.Point import Point -import pyqtgraph.functions as fn +from ..Qt import QtGui, QtCore +from ..GraphicsScene import GraphicsScene +from ..Point import Point +from .. import functions as fn import weakref -from pyqtgraph.pgcollections import OrderedDict +from ..pgcollections import OrderedDict import operator, sys class FiniteCache(OrderedDict): diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index 9d48e627..a4016522 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -1,5 +1,5 @@ -from pyqtgraph.Qt import QtGui, QtCore -import pyqtgraph.functions as fn +from ..Qt import QtGui, QtCore +from .. import functions as fn from .GraphicsWidget import GraphicsWidget ## Must be imported at the end to avoid cyclic-dependency hell: from .ViewBox import ViewBox diff --git a/pyqtgraph/graphicsItems/GraphicsObject.py b/pyqtgraph/graphicsItems/GraphicsObject.py index d8f55d27..1ea9a08b 100644 --- a/pyqtgraph/graphicsItems/GraphicsObject.py +++ b/pyqtgraph/graphicsItems/GraphicsObject.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +from ..Qt import QtGui, QtCore, USE_PYSIDE if not USE_PYSIDE: import sip from .GraphicsItem import GraphicsItem diff --git a/pyqtgraph/graphicsItems/GraphicsWidget.py b/pyqtgraph/graphicsItems/GraphicsWidget.py index 7650b125..c379ce8e 100644 --- a/pyqtgraph/graphicsItems/GraphicsWidget.py +++ b/pyqtgraph/graphicsItems/GraphicsWidget.py @@ -1,5 +1,5 @@ -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph.GraphicsScene import GraphicsScene +from ..Qt import QtGui, QtCore +from ..GraphicsScene import GraphicsScene from .GraphicsItem import GraphicsItem __all__ = ['GraphicsWidget'] diff --git a/pyqtgraph/graphicsItems/GridItem.py b/pyqtgraph/graphicsItems/GridItem.py index 29b0aa2c..87f90a62 100644 --- a/pyqtgraph/graphicsItems/GridItem.py +++ b/pyqtgraph/graphicsItems/GridItem.py @@ -1,8 +1,8 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .UIGraphicsItem import * import numpy as np -from pyqtgraph.Point import Point -import pyqtgraph.functions as fn +from ..Point import Point +from .. import functions as fn __all__ = ['GridItem'] class GridItem(UIGraphicsItem): diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 70d8662f..8474202c 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -3,8 +3,8 @@ GraphicsWidget displaying an image histogram along with gradient editor. Can be """ -from pyqtgraph.Qt import QtGui, QtCore -import pyqtgraph.functions as fn +from ..Qt import QtGui, QtCore +from .. import functions as fn from .GraphicsWidget import GraphicsWidget from .ViewBox import * from .GradientEditorItem import * @@ -12,10 +12,10 @@ from .LinearRegionItem import * from .PlotDataItem import * from .AxisItem import * from .GridItem import * -from pyqtgraph.Point import Point -import pyqtgraph.functions as fn +from ..Point import Point +from .. import functions as fn import numpy as np -import pyqtgraph.debug as debug +from .. import debug as debug __all__ = ['HistogramLUTItem'] diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index f7a211d9..120312ad 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -1,8 +1,8 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore import numpy as np import collections -import pyqtgraph.functions as fn -import pyqtgraph.debug as debug +from .. import functions as fn +from .. import debug as debug from .GraphicsObject import GraphicsObject __all__ = ['ImageItem'] diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 4f0df863..edf6b19e 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -1,7 +1,7 @@ -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph.Point import Point +from ..Qt import QtGui, QtCore +from ..Point import Point from .GraphicsObject import GraphicsObject -import pyqtgraph.functions as fn +from .. import functions as fn import numpy as np import weakref diff --git a/pyqtgraph/graphicsItems/IsocurveItem.py b/pyqtgraph/graphicsItems/IsocurveItem.py index 01ef57b6..71113ba8 100644 --- a/pyqtgraph/graphicsItems/IsocurveItem.py +++ b/pyqtgraph/graphicsItems/IsocurveItem.py @@ -1,8 +1,8 @@ from .GraphicsObject import * -import pyqtgraph.functions as fn -from pyqtgraph.Qt import QtGui, QtCore +from .. import functions as fn +from ..Qt import QtGui, QtCore class IsocurveItem(GraphicsObject): diff --git a/pyqtgraph/graphicsItems/ItemGroup.py b/pyqtgraph/graphicsItems/ItemGroup.py index 930fdf80..4eb0ee0d 100644 --- a/pyqtgraph/graphicsItems/ItemGroup.py +++ b/pyqtgraph/graphicsItems/ItemGroup.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .GraphicsObject import GraphicsObject __all__ = ['ItemGroup'] diff --git a/pyqtgraph/graphicsItems/LabelItem.py b/pyqtgraph/graphicsItems/LabelItem.py index 6101c4bc..37980ee3 100644 --- a/pyqtgraph/graphicsItems/LabelItem.py +++ b/pyqtgraph/graphicsItems/LabelItem.py @@ -1,8 +1,8 @@ -from pyqtgraph.Qt import QtGui, QtCore -import pyqtgraph.functions as fn -import pyqtgraph as pg +from ..Qt import QtGui, QtCore +from .. import functions as fn from .GraphicsWidget import GraphicsWidget from .GraphicsWidgetAnchor import GraphicsWidgetAnchor +from .. import getConfigOption __all__ = ['LabelItem'] @@ -54,7 +54,7 @@ class LabelItem(GraphicsWidget, GraphicsWidgetAnchor): color = self.opts['color'] if color is None: - color = pg.getConfigOption('foreground') + color = getConfigOption('foreground') color = fn.mkColor(color) optlist.append('color: #' + fn.colorStr(color)[:6]) if 'size' in opts: diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 69ddffea..a1228789 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -3,8 +3,9 @@ from .LabelItem import LabelItem from ..Qt import QtGui, QtCore from .. import functions as fn from ..Point import Point +from .ScatterPlotItem import ScatterPlotItem +from .PlotDataItem import PlotDataItem from .GraphicsWidgetAnchor import GraphicsWidgetAnchor -import pyqtgraph as pg __all__ = ['LegendItem'] class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): @@ -152,21 +153,21 @@ class ItemSample(GraphicsWidget): p.setPen(fn.mkPen(None)) p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)])) - if not isinstance(self.item, pg.ScatterPlotItem): + if not isinstance(self.item, ScatterPlotItem): p.setPen(fn.mkPen(opts['pen'])) p.drawLine(2, 18, 18, 2) symbol = opts.get('symbol', None) if symbol is not None: - if isinstance(self.item, pg.PlotDataItem): + if isinstance(self.item, PlotDataItem): opts = self.item.scatter.opts - pen = pg.mkPen(opts['pen']) - brush = pg.mkBrush(opts['brush']) + pen = fn.mkPen(opts['pen']) + brush = fn.mkBrush(opts['brush']) size = opts['size'] p.translate(10,10) - path = pg.graphicsItems.ScatterPlotItem.drawSymbol(p, symbol, size, pen, brush) + path = ScatterPlotItem.drawSymbol(p, symbol, size, pen, brush) diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py index 08f7e198..4f9d28dc 100644 --- a/pyqtgraph/graphicsItems/LinearRegionItem.py +++ b/pyqtgraph/graphicsItems/LinearRegionItem.py @@ -1,8 +1,8 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .UIGraphicsItem import UIGraphicsItem from .InfiniteLine import InfiniteLine -import pyqtgraph.functions as fn -import pyqtgraph.debug as debug +from .. import functions as fn +from .. import debug as debug __all__ = ['LinearRegionItem'] diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 93ef195b..b2beaa99 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -1,17 +1,17 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore try: - from pyqtgraph.Qt import QtOpenGL + from ..Qt import QtOpenGL HAVE_OPENGL = True except: HAVE_OPENGL = False import numpy as np from .GraphicsObject import GraphicsObject -import pyqtgraph.functions as fn -from pyqtgraph import debug -from pyqtgraph.Point import Point -import pyqtgraph as pg +from .. import functions as fn +from ..Point import Point import struct, sys +from .. import getConfigOption +from .. import debug __all__ = ['PlotCurveItem'] class PlotCurveItem(GraphicsObject): @@ -65,7 +65,7 @@ class PlotCurveItem(GraphicsObject): 'brush': None, 'stepMode': False, 'name': None, - 'antialias': pg.getConfigOption('antialias'), + 'antialias': getConfigOption('antialias'), 'connect': 'all', 'mouseWidth': 8, # width of shape responding to mouse click } @@ -399,13 +399,13 @@ class PlotCurveItem(GraphicsObject): self._mouseShape = None return self.path - @pg.debug.warnOnException ## raising an exception here causes crash + @debug.warnOnException ## raising an exception here causes crash def paint(self, p, opt, widget): profiler = debug.Profiler() if self.xData is None: return - if HAVE_OPENGL and pg.getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget): + if HAVE_OPENGL and getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget): self.paintGL(p, opt, widget) return diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 7d46d65c..e8c4145c 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -1,12 +1,12 @@ -import pyqtgraph.metaarray as metaarray -from pyqtgraph.Qt import QtCore +from .. import metaarray as metaarray +from ..Qt import QtCore from .GraphicsObject import GraphicsObject from .PlotCurveItem import PlotCurveItem from .ScatterPlotItem import ScatterPlotItem import numpy as np -import pyqtgraph.functions as fn -import pyqtgraph.debug as debug -import pyqtgraph as pg +from .. import functions as fn +from .. import debug as debug +from .. import getConfigOption class PlotDataItem(GraphicsObject): """ @@ -152,7 +152,7 @@ class PlotDataItem(GraphicsObject): 'symbolBrush': (50, 50, 150), 'pxMode': True, - 'antialias': pg.getConfigOption('antialias'), + 'antialias': getConfigOption('antialias'), 'pointMode': None, 'downsample': 1, diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index cb5f4f30..0dee2fd4 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -12,16 +12,16 @@ The ROI class is meant to serve as the base for more specific types; see several of how to build an ROI at the bottom of the file. """ -from pyqtgraph.Qt import QtCore, QtGui +from ..Qt import QtCore, QtGui #if not hasattr(QtCore, 'Signal'): #QtCore.Signal = QtCore.pyqtSignal import numpy as np from numpy.linalg import norm import scipy.ndimage as ndimage -from pyqtgraph.Point import * -from pyqtgraph.SRTTransform import SRTTransform +from ..Point import * +from ..SRTTransform import SRTTransform from math import cos, sin -import pyqtgraph.functions as fn +from .. import functions as fn from .GraphicsObject import GraphicsObject from .UIGraphicsItem import UIGraphicsItem diff --git a/pyqtgraph/graphicsItems/ScaleBar.py b/pyqtgraph/graphicsItems/ScaleBar.py index 768f6978..66258678 100644 --- a/pyqtgraph/graphicsItems/ScaleBar.py +++ b/pyqtgraph/graphicsItems/ScaleBar.py @@ -1,10 +1,10 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .GraphicsObject import * from .GraphicsWidgetAnchor import * from .TextItem import TextItem import numpy as np -import pyqtgraph.functions as fn -import pyqtgraph as pg +from .. import functions as fn +from .. import getConfigOption __all__ = ['ScaleBar'] @@ -19,7 +19,7 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor): self.setAcceptedMouseButtons(QtCore.Qt.NoButton) if brush is None: - brush = pg.getConfigOption('foreground') + brush = getConfigOption('foreground') self.brush = fn.mkBrush(brush) self.pen = fn.mkPen(pen) self._width = width diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 498df2cd..926c9045 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -1,14 +1,14 @@ -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE -from pyqtgraph.Point import Point -import pyqtgraph.functions as fn +from ..Qt import QtGui, QtCore, USE_PYSIDE +from ..Point import Point +from .. import functions as fn from .GraphicsItem import GraphicsItem from .GraphicsObject import GraphicsObject import numpy as np import weakref -import pyqtgraph.debug as debug -from pyqtgraph.pgcollections import OrderedDict -import pyqtgraph as pg -#import pyqtgraph as pg +from .. import getConfigOption +from .. import debug as debug +from ..pgcollections import OrderedDict +from .. import debug __all__ = ['ScatterPlotItem', 'SpotItem'] @@ -233,7 +233,7 @@ class ScatterPlotItem(GraphicsObject): self.opts = { 'pxMode': True, 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. - 'antialias': pg.getConfigOption('antialias'), + 'antialias': getConfigOption('antialias'), 'name': None, } @@ -693,7 +693,7 @@ class ScatterPlotItem(GraphicsObject): GraphicsObject.setExportMode(self, *args, **kwds) self.invalidate() - @pg.debug.warnOnException ## raising an exception here causes crash + @debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): #p.setPen(fn.mkPen('r')) diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index 911057f4..2b5ea51c 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -1,7 +1,7 @@ -from pyqtgraph.Qt import QtCore, QtGui -import pyqtgraph as pg +from ..Qt import QtCore, QtGui +from ..Point import Point from .UIGraphicsItem import * -import pyqtgraph.functions as fn +from .. import functions as fn class TextItem(UIGraphicsItem): """ @@ -27,7 +27,7 @@ class TextItem(UIGraphicsItem): #*angle* Angle in degrees to rotate text (note that the rotation assigned in this item's #transformation will be ignored) - self.anchor = pg.Point(anchor) + self.anchor = Point(anchor) #self.angle = 0 UIGraphicsItem.__init__(self) self.textItem = QtGui.QGraphicsTextItem() @@ -38,13 +38,13 @@ class TextItem(UIGraphicsItem): self.setText(text, color) else: self.setHtml(html) - self.fill = pg.mkBrush(fill) - self.border = pg.mkPen(border) + self.fill = fn.mkBrush(fill) + self.border = fn.mkPen(border) self.rotate(angle) self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport def setText(self, text, color=(200,200,200)): - color = pg.mkColor(color) + color = fn.mkColor(color) self.textItem.setDefaultTextColor(color) self.textItem.setPlainText(text) self.updateText() @@ -89,7 +89,7 @@ class TextItem(UIGraphicsItem): #br = self.textItem.mapRectToParent(self.textItem.boundingRect()) self.textItem.setPos(0,0) br = self.textItem.boundingRect() - apos = self.textItem.mapToParent(pg.Point(br.width()*self.anchor.x(), br.height()*self.anchor.y())) + apos = self.textItem.mapToParent(Point(br.width()*self.anchor.x(), br.height()*self.anchor.y())) #print br, apos self.textItem.setPos(-apos.x(), -apos.y()) diff --git a/pyqtgraph/graphicsItems/UIGraphicsItem.py b/pyqtgraph/graphicsItems/UIGraphicsItem.py index 19fda424..6f756334 100644 --- a/pyqtgraph/graphicsItems/UIGraphicsItem.py +++ b/pyqtgraph/graphicsItems/UIGraphicsItem.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +from ..Qt import QtGui, QtCore, USE_PYSIDE import weakref from .GraphicsObject import GraphicsObject if not USE_PYSIDE: diff --git a/pyqtgraph/graphicsItems/VTickGroup.py b/pyqtgraph/graphicsItems/VTickGroup.py index c6880f91..4b315678 100644 --- a/pyqtgraph/graphicsItems/VTickGroup.py +++ b/pyqtgraph/graphicsItems/VTickGroup.py @@ -3,8 +3,8 @@ if __name__ == '__main__': path = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, os.path.join(path, '..', '..')) -from pyqtgraph.Qt import QtGui, QtCore -import pyqtgraph.functions as fn +from ..Qt import QtGui, QtCore +from .. import functions as fn import weakref from .UIGraphicsItem import UIGraphicsItem @@ -96,18 +96,4 @@ class VTickGroup(UIGraphicsItem): p.setPen(self.pen) p.drawPath(self.path) - -if __name__ == '__main__': - app = QtGui.QApplication([]) - import pyqtgraph as pg - vt = VTickGroup([1,3,4,7,9], [0.8, 1.0]) - p = pg.plot() - p.addItem(vt) - - if sys.flags.interactive == 0: - app.exec_() - - - - \ No newline at end of file From cf312e7bac60a7bf85377f747fc54bb11ad65904 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 22 Dec 2013 02:18:37 -0500 Subject: [PATCH 040/268] updated widgets --- pyqtgraph/widgets/BusyCursor.py | 2 +- pyqtgraph/widgets/CheckTable.py | 2 +- pyqtgraph/widgets/ColorButton.py | 4 ++-- pyqtgraph/widgets/ColorMapWidget.py | 8 ++++---- pyqtgraph/widgets/ComboBox.py | 4 ++-- pyqtgraph/widgets/DataFilterWidget.py | 10 +++++----- pyqtgraph/widgets/DataTreeWidget.py | 4 ++-- pyqtgraph/widgets/FeedbackButton.py | 2 +- pyqtgraph/widgets/FileDialog.py | 2 +- pyqtgraph/widgets/GradientWidget.py | 4 ++-- pyqtgraph/widgets/GraphicsLayoutWidget.py | 4 ++-- pyqtgraph/widgets/GraphicsView.py | 21 ++++++++++----------- pyqtgraph/widgets/HistogramLUTWidget.py | 4 ++-- pyqtgraph/widgets/JoystickButton.py | 2 +- pyqtgraph/widgets/LayoutWidget.py | 2 +- pyqtgraph/widgets/MatplotlibWidget.py | 2 +- pyqtgraph/widgets/MultiPlotWidget.py | 4 ++-- pyqtgraph/widgets/PathButton.py | 9 +++++---- pyqtgraph/widgets/PlotWidget.py | 4 ++-- pyqtgraph/widgets/ProgressDialog.py | 2 +- pyqtgraph/widgets/RawImageWidget.py | 6 +++--- pyqtgraph/widgets/RemoteGraphicsView.py | 5 ++--- pyqtgraph/widgets/ScatterPlotWidget.py | 17 +++++++++-------- pyqtgraph/widgets/SpinBox.py | 8 ++++---- pyqtgraph/widgets/TableWidget.py | 4 ++-- pyqtgraph/widgets/TreeWidget.py | 2 +- pyqtgraph/widgets/ValueLabel.py | 8 ++++---- pyqtgraph/widgets/VerticalLabel.py | 2 +- 28 files changed, 74 insertions(+), 74 deletions(-) diff --git a/pyqtgraph/widgets/BusyCursor.py b/pyqtgraph/widgets/BusyCursor.py index b013dda0..d99fe589 100644 --- a/pyqtgraph/widgets/BusyCursor.py +++ b/pyqtgraph/widgets/BusyCursor.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore __all__ = ['BusyCursor'] diff --git a/pyqtgraph/widgets/CheckTable.py b/pyqtgraph/widgets/CheckTable.py index dd33fd75..22015126 100644 --- a/pyqtgraph/widgets/CheckTable.py +++ b/pyqtgraph/widgets/CheckTable.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from . import VerticalLabel __all__ = ['CheckTable'] diff --git a/pyqtgraph/widgets/ColorButton.py b/pyqtgraph/widgets/ColorButton.py index ee91801a..40f6740f 100644 --- a/pyqtgraph/widgets/ColorButton.py +++ b/pyqtgraph/widgets/ColorButton.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtGui, QtCore -import pyqtgraph.functions as functions +from ..Qt import QtGui, QtCore +from .. import functions as functions __all__ = ['ColorButton'] diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index 26539d7e..1874f5d1 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -1,8 +1,8 @@ -from pyqtgraph.Qt import QtGui, QtCore -import pyqtgraph.parametertree as ptree +from ..Qt import QtGui, QtCore +from .. import parametertree as ptree import numpy as np -from pyqtgraph.pgcollections import OrderedDict -import pyqtgraph.functions as fn +from ..pgcollections import OrderedDict +from .. import functions as fn __all__ = ['ColorMapWidget'] diff --git a/pyqtgraph/widgets/ComboBox.py b/pyqtgraph/widgets/ComboBox.py index 1884648c..72ac384f 100644 --- a/pyqtgraph/widgets/ComboBox.py +++ b/pyqtgraph/widgets/ComboBox.py @@ -1,5 +1,5 @@ -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph.SignalProxy import SignalProxy +from ..Qt import QtGui, QtCore +from ..SignalProxy import SignalProxy class ComboBox(QtGui.QComboBox): diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index c94f6c68..cae8be86 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -1,8 +1,8 @@ -from pyqtgraph.Qt import QtGui, QtCore -import pyqtgraph.parametertree as ptree +from ..Qt import QtGui, QtCore +from .. import parametertree as ptree import numpy as np -from pyqtgraph.pgcollections import OrderedDict -import pyqtgraph as pg +from ..pgcollections import OrderedDict +from .. import functions as fn __all__ = ['DataFilterWidget'] @@ -108,7 +108,7 @@ class RangeFilterItem(ptree.types.SimpleParameter): return mask def describe(self): - return "%s < %s < %s" % (pg.siFormat(self['Min'], suffix=self.units), self.fieldName, pg.siFormat(self['Max'], suffix=self.units)) + return "%s < %s < %s" % (fn.siFormat(self['Min'], suffix=self.units), self.fieldName, fn.siFormat(self['Max'], suffix=self.units)) class EnumFilterItem(ptree.types.SimpleParameter): def __init__(self, name, opts): diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index a6b5cac8..b99121bf 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph.pgcollections import OrderedDict +from ..Qt import QtGui, QtCore +from ..pgcollections import OrderedDict import types, traceback import numpy as np diff --git a/pyqtgraph/widgets/FeedbackButton.py b/pyqtgraph/widgets/FeedbackButton.py index f788f4b6..30114d4e 100644 --- a/pyqtgraph/widgets/FeedbackButton.py +++ b/pyqtgraph/widgets/FeedbackButton.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtCore, QtGui +from ..Qt import QtCore, QtGui __all__ = ['FeedbackButton'] diff --git a/pyqtgraph/widgets/FileDialog.py b/pyqtgraph/widgets/FileDialog.py index 33b838a2..faa0994c 100644 --- a/pyqtgraph/widgets/FileDialog.py +++ b/pyqtgraph/widgets/FileDialog.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore import sys __all__ = ['FileDialog'] diff --git a/pyqtgraph/widgets/GradientWidget.py b/pyqtgraph/widgets/GradientWidget.py index 7cbc032e..ce0cbeb9 100644 --- a/pyqtgraph/widgets/GradientWidget.py +++ b/pyqtgraph/widgets/GradientWidget.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .GraphicsView import GraphicsView -from pyqtgraph.graphicsItems.GradientEditorItem import GradientEditorItem +from ..graphicsItems.GradientEditorItem import GradientEditorItem import weakref import numpy as np diff --git a/pyqtgraph/widgets/GraphicsLayoutWidget.py b/pyqtgraph/widgets/GraphicsLayoutWidget.py index 1e667278..3c34ca58 100644 --- a/pyqtgraph/widgets/GraphicsLayoutWidget.py +++ b/pyqtgraph/widgets/GraphicsLayoutWidget.py @@ -1,5 +1,5 @@ -from pyqtgraph.Qt import QtGui -from pyqtgraph.graphicsItems.GraphicsLayout import GraphicsLayout +from ..Qt import QtGui +from ..graphicsItems.GraphicsLayout import GraphicsLayout from .GraphicsView import GraphicsView __all__ = ['GraphicsLayoutWidget'] diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index fb535929..f3f4856a 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -5,23 +5,22 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from pyqtgraph.Qt import QtCore, QtGui -import pyqtgraph as pg +from ..Qt import QtCore, QtGui, USE_PYSIDE try: - from pyqtgraph.Qt import QtOpenGL + from ..Qt import QtOpenGL HAVE_OPENGL = True except ImportError: HAVE_OPENGL = False -from pyqtgraph.Point import Point +from ..Point import Point import sys, os from .FileDialog import FileDialog -from pyqtgraph.GraphicsScene import GraphicsScene +from ..GraphicsScene import GraphicsScene import numpy as np -import pyqtgraph.functions as fn -import pyqtgraph.debug as debug -import pyqtgraph +from .. import functions as fn +from .. import debug as debug +from .. import getConfigOption __all__ = ['GraphicsView'] @@ -73,7 +72,7 @@ class GraphicsView(QtGui.QGraphicsView): QtGui.QGraphicsView.__init__(self, parent) if useOpenGL is None: - useOpenGL = pyqtgraph.getConfigOption('useOpenGL') + useOpenGL = getConfigOption('useOpenGL') self.useOpenGL(useOpenGL) @@ -108,7 +107,7 @@ class GraphicsView(QtGui.QGraphicsView): ## Workaround for PySide crash ## This ensures that the scene will outlive the view. - if pyqtgraph.Qt.USE_PYSIDE: + if USE_PYSIDE: self.sceneObj._view_ref_workaround = self ## by default we set up a central widget with a grid layout. @@ -138,7 +137,7 @@ class GraphicsView(QtGui.QGraphicsView): """ self._background = background if background == 'default': - background = pyqtgraph.getConfigOption('background') + background = getConfigOption('background') brush = fn.mkBrush(background) self.setBackgroundBrush(brush) diff --git a/pyqtgraph/widgets/HistogramLUTWidget.py b/pyqtgraph/widgets/HistogramLUTWidget.py index cbe8eb61..9aec837c 100644 --- a/pyqtgraph/widgets/HistogramLUTWidget.py +++ b/pyqtgraph/widgets/HistogramLUTWidget.py @@ -3,9 +3,9 @@ Widget displaying an image histogram along with gradient editor. Can be used to This is a wrapper around HistogramLUTItem """ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .GraphicsView import GraphicsView -from pyqtgraph.graphicsItems.HistogramLUTItem import HistogramLUTItem +from ..graphicsItems.HistogramLUTItem import HistogramLUTItem __all__ = ['HistogramLUTWidget'] diff --git a/pyqtgraph/widgets/JoystickButton.py b/pyqtgraph/widgets/JoystickButton.py index 201a957a..6f73c8dc 100644 --- a/pyqtgraph/widgets/JoystickButton.py +++ b/pyqtgraph/widgets/JoystickButton.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore __all__ = ['JoystickButton'] diff --git a/pyqtgraph/widgets/LayoutWidget.py b/pyqtgraph/widgets/LayoutWidget.py index f567ad74..65d04d3f 100644 --- a/pyqtgraph/widgets/LayoutWidget.py +++ b/pyqtgraph/widgets/LayoutWidget.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore __all__ = ['LayoutWidget'] class LayoutWidget(QtGui.QWidget): diff --git a/pyqtgraph/widgets/MatplotlibWidget.py b/pyqtgraph/widgets/MatplotlibWidget.py index 6a22c973..959e188a 100644 --- a/pyqtgraph/widgets/MatplotlibWidget.py +++ b/pyqtgraph/widgets/MatplotlibWidget.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +from ..Qt import QtGui, QtCore, USE_PYSIDE import matplotlib if USE_PYSIDE: diff --git a/pyqtgraph/widgets/MultiPlotWidget.py b/pyqtgraph/widgets/MultiPlotWidget.py index 400bee92..58b71296 100644 --- a/pyqtgraph/widgets/MultiPlotWidget.py +++ b/pyqtgraph/widgets/MultiPlotWidget.py @@ -6,7 +6,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation. """ from .GraphicsView import GraphicsView -import pyqtgraph.graphicsItems.MultiPlotItem as MultiPlotItem +from ..graphicsItems import MultiPlotItem as MultiPlotItem __all__ = ['MultiPlotWidget'] class MultiPlotWidget(GraphicsView): @@ -42,4 +42,4 @@ class MultiPlotWidget(GraphicsView): self.mPlotItem.close() self.mPlotItem = None self.setParent(None) - GraphicsView.close(self) \ No newline at end of file + GraphicsView.close(self) diff --git a/pyqtgraph/widgets/PathButton.py b/pyqtgraph/widgets/PathButton.py index 7950a53d..0c62bb1b 100644 --- a/pyqtgraph/widgets/PathButton.py +++ b/pyqtgraph/widgets/PathButton.py @@ -1,5 +1,6 @@ -from pyqtgraph.Qt import QtGui, QtCore -import pyqtgraph as pg +from ..Qt import QtGui, QtCore +from .. import functions as fn + __all__ = ['PathButton'] @@ -20,10 +21,10 @@ class PathButton(QtGui.QPushButton): def setBrush(self, brush): - self.brush = pg.mkBrush(brush) + self.brush = fn.mkBrush(brush) def setPen(self, pen): - self.pen = pg.mkPen(pen) + self.pen = fn.mkPen(pen) def setPath(self, path): self.path = path diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index 7b3c685c..f9b544f5 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -5,9 +5,9 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from pyqtgraph.Qt import QtCore, QtGui +from ..Qt import QtCore, QtGui from .GraphicsView import * -from pyqtgraph.graphicsItems.PlotItem import * +from ..graphicsItems.PlotItem import * __all__ = ['PlotWidget'] class PlotWidget(GraphicsView): diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 0f55e227..8c669be4 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore __all__ = ['ProgressDialog'] class ProgressDialog(QtGui.QProgressDialog): diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py index 517f4f99..970b570b 100644 --- a/pyqtgraph/widgets/RawImageWidget.py +++ b/pyqtgraph/widgets/RawImageWidget.py @@ -1,12 +1,12 @@ -from pyqtgraph.Qt import QtCore, QtGui +from ..Qt import QtCore, QtGui try: - from pyqtgraph.Qt import QtOpenGL + from ..Qt import QtOpenGL from OpenGL.GL import * HAVE_OPENGL = True except ImportError: HAVE_OPENGL = False -import pyqtgraph.functions as fn +from .. import functions as fn import numpy as np class RawImageWidget(QtGui.QWidget): diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index d44fd1c3..54712f43 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -1,8 +1,7 @@ -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +from ..Qt import QtGui, QtCore, USE_PYSIDE if not USE_PYSIDE: import sip -import pyqtgraph.multiprocess as mp -import pyqtgraph as pg +from .. import multiprocess as mp from .GraphicsView import GraphicsView import numpy as np import mmap, tempfile, ctypes, atexit, sys, random diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index e9e24dd7..02f260ca 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -1,12 +1,13 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .PlotWidget import PlotWidget from .DataFilterWidget import DataFilterParameter from .ColorMapWidget import ColorMapParameter -import pyqtgraph.parametertree as ptree -import pyqtgraph.functions as fn +from .. import parametertree as ptree +from .. import functions as fn +from .. import getConfigOption +from ..graphicsItems.TextItem import TextItem import numpy as np -from pyqtgraph.pgcollections import OrderedDict -import pyqtgraph as pg +from ..pgcollections import OrderedDict __all__ = ['ScatterPlotWidget'] @@ -48,9 +49,9 @@ class ScatterPlotWidget(QtGui.QSplitter): self.ctrlPanel.addWidget(self.ptree) self.addWidget(self.plot) - bg = pg.mkColor(pg.getConfigOption('background')) + bg = fn.mkColor(getConfigOption('background')) bg.setAlpha(150) - self.filterText = pg.TextItem(border=pg.getConfigOption('foreground'), color=bg) + self.filterText = TextItem(border=getConfigOption('foreground'), color=bg) self.filterText.setPos(60,20) self.filterText.setParentItem(self.plot.plotItem) @@ -193,7 +194,7 @@ class ScatterPlotWidget(QtGui.QSplitter): imax = int(xy[ax].max()) if len(xy[ax]) > 0 else 0 for i in range(imax+1): keymask = xy[ax] == i - scatter = pg.pseudoScatter(xy[1-ax][keymask], bidir=True) + scatter = fn.pseudoScatter(xy[1-ax][keymask], bidir=True) if len(scatter) == 0: continue smax = np.abs(scatter).max() diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 57e4f1ed..422522de 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph.python2_3 import asUnicode -from pyqtgraph.SignalProxy import SignalProxy +from ..Qt import QtGui, QtCore +from ..python2_3 import asUnicode +from ..SignalProxy import SignalProxy -import pyqtgraph.functions as fn +from .. import functions as fn from math import log from decimal import Decimal as D ## Use decimal to avoid accumulating floating-point errors from decimal import * diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 8ffe7291..03392648 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph.python2_3 import asUnicode +from ..Qt import QtGui, QtCore +from ..python2_3 import asUnicode import numpy as np try: diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index 97fbe953..ec2c35cf 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from weakref import * __all__ = ['TreeWidget', 'TreeWidgetItem'] diff --git a/pyqtgraph/widgets/ValueLabel.py b/pyqtgraph/widgets/ValueLabel.py index 7f6fa84b..d395cd43 100644 --- a/pyqtgraph/widgets/ValueLabel.py +++ b/pyqtgraph/widgets/ValueLabel.py @@ -1,6 +1,6 @@ -from pyqtgraph.Qt import QtCore, QtGui -from pyqtgraph.ptime import time -import pyqtgraph as pg +from ..Qt import QtCore, QtGui +from ..ptime import time +from .. import functions as fn from functools import reduce __all__ = ['ValueLabel'] @@ -67,7 +67,7 @@ class ValueLabel(QtGui.QLabel): avg = self.averageValue() val = self.values[-1][1] if self.siPrefix: - return pg.siFormat(avg, suffix=self.suffix) + return fn.siFormat(avg, suffix=self.suffix) else: return self.formatStr.format(value=val, avgValue=avg, suffix=self.suffix) diff --git a/pyqtgraph/widgets/VerticalLabel.py b/pyqtgraph/widgets/VerticalLabel.py index fa45ae5d..c8cc80bd 100644 --- a/pyqtgraph/widgets/VerticalLabel.py +++ b/pyqtgraph/widgets/VerticalLabel.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore __all__ = ['VerticalLabel'] #class VerticalLabel(QtGui.QLabel): From a2e8290d8e223a630e77708e7f5c3ba42ddd13ee Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 23 Dec 2013 07:51:33 -0500 Subject: [PATCH 041/268] console, graphicsscene, and 2nd-level graphicsitems --- pyqtgraph/GraphicsScene/GraphicsScene.py | 12 +++++----- pyqtgraph/GraphicsScene/exportDialog.py | 18 ++++++++------- .../GraphicsScene/exportDialogTemplate.ui | 2 +- .../exportDialogTemplate_pyqt.py | 2 +- .../exportDialogTemplate_pyside.py | 2 +- pyqtgraph/GraphicsScene/mouseEvents.py | 6 ++--- pyqtgraph/console/CmdInput.py | 4 ++-- pyqtgraph/console/Console.py | 8 +++---- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 12 +++++----- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 16 +++++++------- .../graphicsItems/ViewBox/ViewBoxMenu.py | 6 ++--- pyqtgraph/parametertree/Parameter.py | 4 ++-- pyqtgraph/parametertree/ParameterItem.py | 2 +- pyqtgraph/parametertree/ParameterTree.py | 4 ++-- pyqtgraph/parametertree/parameterTypes.py | 22 +++++++++---------- 15 files changed, 61 insertions(+), 59 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 3fdd5924..cce7ac4a 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -1,6 +1,6 @@ -from pyqtgraph.Qt import QtCore, QtGui +from ..Qt import QtCore, QtGui -from pyqtgraph.python2_3 import sortList +from ..python2_3 import sortList #try: #from PyQt4 import QtOpenGL #HAVE_OPENGL = True @@ -8,11 +8,11 @@ from pyqtgraph.python2_3 import sortList #HAVE_OPENGL = False import weakref -from pyqtgraph.Point import Point -import pyqtgraph.functions as fn -import pyqtgraph.ptime as ptime +from ..Point import Point +from .. import functions as fn +from .. import ptime as ptime from .mouseEvents import * -import pyqtgraph.debug as debug +from .. import debug as debug from . import exportDialog if hasattr(QtCore, 'PYQT_VERSION'): diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py index 436d5e42..5efb7c44 100644 --- a/pyqtgraph/GraphicsScene/exportDialog.py +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -1,6 +1,8 @@ -from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE -import pyqtgraph as pg -import pyqtgraph.exporters as exporters +from ..Qt import QtCore, QtGui, USE_PYSIDE +from .. import exporters as exporters +from .. import functions as fn +from ..graphicsItems.ViewBox import ViewBox +from ..graphicsItems.PlotItem import PlotItem if USE_PYSIDE: from . import exportDialogTemplate_pyside as exportDialogTemplate @@ -18,7 +20,7 @@ class ExportDialog(QtGui.QWidget): self.scene = scene self.selectBox = QtGui.QGraphicsRectItem() - self.selectBox.setPen(pg.mkPen('y', width=3, style=QtCore.Qt.DashLine)) + self.selectBox.setPen(fn.mkPen('y', width=3, style=QtCore.Qt.DashLine)) self.selectBox.hide() self.scene.addItem(self.selectBox) @@ -35,10 +37,10 @@ class ExportDialog(QtGui.QWidget): def show(self, item=None): if item is not None: ## Select next exportable parent of the item originally clicked on - while not isinstance(item, pg.ViewBox) and not isinstance(item, pg.PlotItem) and item is not None: + while not isinstance(item, ViewBox) and not isinstance(item, PlotItem) and item is not None: item = item.parentItem() ## if this is a ViewBox inside a PlotItem, select the parent instead. - if isinstance(item, pg.ViewBox) and isinstance(item.parentItem(), pg.PlotItem): + if isinstance(item, ViewBox) and isinstance(item.parentItem(), PlotItem): item = item.parentItem() self.updateItemList(select=item) self.setVisible(True) @@ -64,9 +66,9 @@ class ExportDialog(QtGui.QWidget): def updateItemTree(self, item, treeItem, select=None): si = None - if isinstance(item, pg.ViewBox): + if isinstance(item, ViewBox): si = QtGui.QTreeWidgetItem(['ViewBox']) - elif isinstance(item, pg.PlotItem): + elif isinstance(item, PlotItem): si = QtGui.QTreeWidgetItem(['Plot']) if si is not None: diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate.ui b/pyqtgraph/GraphicsScene/exportDialogTemplate.ui index c91fbc3f..eacacd88 100644 --- a/pyqtgraph/GraphicsScene/exportDialogTemplate.ui +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate.ui @@ -92,7 +92,7 @@ ParameterTree QTreeWidget -
pyqtgraph.parametertree
+
..parametertree
diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py index c3056d1c..3bbab155 100644 --- a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py @@ -65,4 +65,4 @@ class Ui_Form(object): self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8)) self.copyBtn.setText(QtGui.QApplication.translate("Form", "Copy", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.parametertree import ParameterTree +from ..parametertree import ParameterTree diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py index cf27f60a..8c95e717 100644 --- a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py @@ -60,4 +60,4 @@ class Ui_Form(object): self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8)) self.copyBtn.setText(QtGui.QApplication.translate("Form", "Copy", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.parametertree import ParameterTree +from ..parametertree import ParameterTree diff --git a/pyqtgraph/GraphicsScene/mouseEvents.py b/pyqtgraph/GraphicsScene/mouseEvents.py index 0b71ac6f..f337a657 100644 --- a/pyqtgraph/GraphicsScene/mouseEvents.py +++ b/pyqtgraph/GraphicsScene/mouseEvents.py @@ -1,7 +1,7 @@ -from pyqtgraph.Point import Point -from pyqtgraph.Qt import QtCore, QtGui +from ..Point import Point +from ..Qt import QtCore, QtGui import weakref -import pyqtgraph.ptime as ptime +from .. import ptime as ptime class MouseDragEvent(object): """ diff --git a/pyqtgraph/console/CmdInput.py b/pyqtgraph/console/CmdInput.py index 3e9730d6..24a01e89 100644 --- a/pyqtgraph/console/CmdInput.py +++ b/pyqtgraph/console/CmdInput.py @@ -1,5 +1,5 @@ -from pyqtgraph.Qt import QtCore, QtGui -from pyqtgraph.python2_3 import asUnicode +from ..Qt import QtCore, QtGui +from ..python2_3 import asUnicode class CmdInput(QtGui.QLineEdit): diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 982c2424..0cbd2c3e 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -1,14 +1,14 @@ -from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE +from ..Qt import QtCore, QtGui, USE_PYSIDE import sys, re, os, time, traceback, subprocess -import pyqtgraph as pg if USE_PYSIDE: from . import template_pyside as template else: from . import template_pyqt as template -import pyqtgraph.exceptionHandling as exceptionHandling +from .. import exceptionHandling as exceptionHandling import pickle +from .. import getConfigOption class ConsoleWidget(QtGui.QWidget): """ @@ -281,7 +281,7 @@ class ConsoleWidget(QtGui.QWidget): def stackItemDblClicked(self, item): editor = self.editor if editor is None: - editor = pg.getConfigOption('editorCommand') + editor = getConfigOption('editorCommand') if editor is None: return tb = self.currentFrame() diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index b99a3266..baff1aa9 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -16,16 +16,16 @@ This class is very heavily featured: - Control panel with a huge feature set including averaging, decimation, display, power spectrum, svg/png export, plot linking, and more. """ -from pyqtgraph.Qt import QtGui, QtCore, QtSvg, USE_PYSIDE -import pyqtgraph.pixmaps +from ...Qt import QtGui, QtCore, QtSvg, USE_PYSIDE +from ... import pixmaps if USE_PYSIDE: from .plotConfigTemplate_pyside import * else: from .plotConfigTemplate_pyqt import * -import pyqtgraph.functions as fn -from pyqtgraph.widgets.FileDialog import FileDialog +from ... import functions as fn +from ...widgets.FileDialog import FileDialog import weakref import numpy as np import os @@ -37,7 +37,7 @@ from .. LegendItem import LegendItem from .. GraphicsWidget import GraphicsWidget from .. ButtonItem import ButtonItem from .. InfiniteLine import InfiniteLine -from pyqtgraph.WidgetGroup import WidgetGroup +from ...WidgetGroup import WidgetGroup __all__ = ['PlotItem'] @@ -129,7 +129,7 @@ class PlotItem(GraphicsWidget): path = os.path.dirname(__file__) #self.autoImageFile = os.path.join(path, 'auto.png') #self.lockImageFile = os.path.join(path, 'lock.png') - self.autoBtn = ButtonItem(pyqtgraph.pixmaps.getPixmap('auto'), 14, self) + self.autoBtn = ButtonItem(pixmaps.getPixmap('auto'), 14, self) self.autoBtn.mode = 'auto' self.autoBtn.clicked.connect(self.autoBtnClicked) #self.autoBtn.hide() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 17bd2207..70012ec4 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1,15 +1,15 @@ -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph.python2_3 import sortList +from ...Qt import QtGui, QtCore +from ...python2_3 import sortList import numpy as np -from pyqtgraph.Point import Point -import pyqtgraph.functions as fn +from ...Point import Point +from ... import functions as fn from .. ItemGroup import ItemGroup from .. GraphicsWidget import GraphicsWidget -from pyqtgraph.GraphicsScene import GraphicsScene -import pyqtgraph +from ...GraphicsScene import GraphicsScene import weakref from copy import deepcopy -import pyqtgraph.debug as debug +from ... import debug as debug +from ... import getConfigOption __all__ = ['ViewBox'] @@ -113,7 +113,7 @@ class ViewBox(GraphicsWidget): ## a name string indicates that the view *should* link to another, but no view with that name exists yet. 'mouseEnabled': [enableMouse, enableMouse], - 'mouseMode': ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else ViewBox.RectMode, + 'mouseMode': ViewBox.PanMode if getConfigOption('leftButtonPan') else ViewBox.RectMode, 'enableMenu': enableMenu, 'wheelScaleFactor': -1.0 / 8.0, diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 15d0be06..af142771 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -1,6 +1,6 @@ -from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE -from pyqtgraph.python2_3 import asUnicode -from pyqtgraph.WidgetGroup import WidgetGroup +from ...Qt import QtCore, QtGui, USE_PYSIDE +from ...python2_3 import asUnicode +from ...WidgetGroup import WidgetGroup if USE_PYSIDE: from .axisCtrlTemplate_pyside import Ui_Form as AxisCtrlTemplate diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 9a7ece25..45e46e55 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -1,6 +1,6 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore import os, weakref, re -from pyqtgraph.pgcollections import OrderedDict +from ..pgcollections import OrderedDict from .ParameterItem import ParameterItem PARAM_TYPES = {} diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index 46499fd3..5a90becf 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore import os, weakref, re class ParameterItem(QtGui.QTreeWidgetItem): diff --git a/pyqtgraph/parametertree/ParameterTree.py b/pyqtgraph/parametertree/ParameterTree.py index 866875e5..953f3bb7 100644 --- a/pyqtgraph/parametertree/ParameterTree.py +++ b/pyqtgraph/parametertree/ParameterTree.py @@ -1,5 +1,5 @@ -from pyqtgraph.Qt import QtCore, QtGui -from pyqtgraph.widgets.TreeWidget import TreeWidget +from ..Qt import QtCore, QtGui +from ..widgets.TreeWidget import TreeWidget import os, weakref, re from .ParameterItem import ParameterItem #import functions as fn diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 3300171f..f58145dd 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -1,14 +1,14 @@ -from pyqtgraph.Qt import QtCore, QtGui -from pyqtgraph.python2_3 import asUnicode +from ..Qt import QtCore, QtGui +from ..python2_3 import asUnicode from .Parameter import Parameter, registerParameterType from .ParameterItem import ParameterItem -from pyqtgraph.widgets.SpinBox import SpinBox -from pyqtgraph.widgets.ColorButton import ColorButton -#from pyqtgraph.widgets.GradientWidget import GradientWidget ## creates import loop -import pyqtgraph as pg -import pyqtgraph.pixmaps as pixmaps +from ..widgets.SpinBox import SpinBox +from ..widgets.ColorButton import ColorButton +#from ..widgets.GradientWidget import GradientWidget ## creates import loop +from .. import pixmaps as pixmaps +from .. import functions as fn import os -from pyqtgraph.pgcollections import OrderedDict +from ..pgcollections import OrderedDict class WidgetParameterItem(ParameterItem): """ @@ -141,7 +141,7 @@ class WidgetParameterItem(ParameterItem): self.hideWidget = False w.setFlat(True) elif t == 'colormap': - from pyqtgraph.widgets.GradientWidget import GradientWidget ## need this here to avoid import loop + from ..widgets.GradientWidget import GradientWidget ## need this here to avoid import loop w = GradientWidget(orientation='bottom') w.sigChanged = w.sigGradientChangeFinished w.sigChanging = w.sigGradientChanged @@ -304,11 +304,11 @@ class SimpleParameter(Parameter): self.saveState = self.saveColorState def colorValue(self): - return pg.mkColor(Parameter.value(self)) + return fn.mkColor(Parameter.value(self)) def saveColorState(self): state = Parameter.saveState(self) - state['value'] = pg.colorTuple(self.value()) + state['value'] = fn.colorTuple(self.value()) return state From 7777240d899efbe85945fce5f9da200ec0f794c2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 23 Dec 2013 09:46:54 -0500 Subject: [PATCH 042/268] exporters, multiprocess, opengl --- pyqtgraph/exporters/CSVExporter.py | 9 ++-- pyqtgraph/exporters/Exporter.py | 14 +++--- pyqtgraph/exporters/ImageExporter.py | 8 ++-- pyqtgraph/exporters/Matplotlib.py | 26 +++++------ pyqtgraph/exporters/PrintExporter.py | 4 +- pyqtgraph/exporters/SVGExporter.py | 50 +++++---------------- pyqtgraph/exporters/__init__.py | 13 ------ pyqtgraph/multiprocess/parallelizer.py | 4 +- pyqtgraph/multiprocess/processes.py | 13 +++--- pyqtgraph/opengl/GLGraphicsItem.py | 4 +- pyqtgraph/opengl/GLViewWidget.py | 10 ++--- pyqtgraph/opengl/MeshData.py | 4 +- pyqtgraph/opengl/__init__.py | 2 +- pyqtgraph/opengl/glInfo.py | 2 +- pyqtgraph/opengl/items/GLAxisItem.py | 2 +- pyqtgraph/opengl/items/GLBoxItem.py | 6 +-- pyqtgraph/opengl/items/GLGridItem.py | 2 +- pyqtgraph/opengl/items/GLImageItem.py | 2 +- pyqtgraph/opengl/items/GLLinePlotItem.py | 2 +- pyqtgraph/opengl/items/GLMeshItem.py | 8 ++-- pyqtgraph/opengl/items/GLScatterPlotItem.py | 2 +- pyqtgraph/opengl/items/GLSurfacePlotItem.py | 3 +- pyqtgraph/opengl/items/GLVolumeItem.py | 2 +- 23 files changed, 74 insertions(+), 118 deletions(-) diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index c6386655..b0cf5af5 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -1,8 +1,7 @@ -import pyqtgraph as pg -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .Exporter import Exporter -from pyqtgraph.parametertree import Parameter - +from ..parametertree import Parameter +from .. import PlotItem __all__ = ['CSVExporter'] @@ -22,7 +21,7 @@ class CSVExporter(Exporter): def export(self, fileName=None): - if not isinstance(self.item, pg.PlotItem): + if not isinstance(self.item, PlotItem): raise Exception("Must have a PlotItem selected for CSV export.") if fileName is None: diff --git a/pyqtgraph/exporters/Exporter.py b/pyqtgraph/exporters/Exporter.py index 281fbb9a..64a25294 100644 --- a/pyqtgraph/exporters/Exporter.py +++ b/pyqtgraph/exporters/Exporter.py @@ -1,7 +1,7 @@ -from pyqtgraph.widgets.FileDialog import FileDialog -import pyqtgraph as pg -from pyqtgraph.Qt import QtGui, QtCore, QtSvg -from pyqtgraph.python2_3 import asUnicode +from ..widgets.FileDialog import FileDialog +from ..Qt import QtGui, QtCore, QtSvg +from ..python2_3 import asUnicode +from ..GraphicsScene import GraphicsScene import os, re LastExportDirectory = None @@ -77,20 +77,20 @@ class Exporter(object): self.export(fileName=fileName, **self.fileDialog.opts) def getScene(self): - if isinstance(self.item, pg.GraphicsScene): + if isinstance(self.item, GraphicsScene): return self.item else: return self.item.scene() def getSourceRect(self): - if isinstance(self.item, pg.GraphicsScene): + if isinstance(self.item, GraphicsScene): w = self.item.getViewWidget() return w.viewportTransform().inverted()[0].mapRect(w.rect()) else: return self.item.sceneBoundingRect() def getTargetRect(self): - if isinstance(self.item, pg.GraphicsScene): + if isinstance(self.item, GraphicsScene): return self.item.getViewWidget().rect() else: return self.item.mapRectToDevice(self.item.boundingRect()) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index 40a76fbd..c8abb02b 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -1,7 +1,7 @@ from .Exporter import Exporter -from pyqtgraph.parametertree import Parameter -from pyqtgraph.Qt import QtGui, QtCore, QtSvg, USE_PYSIDE -import pyqtgraph as pg +from ..parametertree import Parameter +from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE\ +from .. import functions as fn import numpy as np __all__ = ['ImageExporter'] @@ -73,7 +73,7 @@ class ImageExporter(Exporter): bg[:,:,1] = color.green() bg[:,:,2] = color.red() bg[:,:,3] = color.alpha() - self.png = pg.makeQImage(bg, alpha=True) + self.png = fn.makeQImage(bg, alpha=True) ## set resolution of image: origTargetRect = self.getTargetRect() diff --git a/pyqtgraph/exporters/Matplotlib.py b/pyqtgraph/exporters/Matplotlib.py index 42008468..c2980b49 100644 --- a/pyqtgraph/exporters/Matplotlib.py +++ b/pyqtgraph/exporters/Matplotlib.py @@ -1,7 +1,7 @@ -import pyqtgraph as pg -from pyqtgraph.Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore from .Exporter import Exporter - +from .. import PlotItem +from .. import functions as fn __all__ = ['MatplotlibExporter'] @@ -17,7 +17,7 @@ class MatplotlibExporter(Exporter): def export(self, fileName=None): - if isinstance(self.item, pg.PlotItem): + if isinstance(self.item, PlotItem): mpw = MatplotlibWindow() MatplotlibExporter.windows.append(mpw) fig = mpw.getFigure() @@ -29,23 +29,23 @@ class MatplotlibExporter(Exporter): for item in self.item.curves: x, y = item.getData() opts = item.opts - pen = pg.mkPen(opts['pen']) + pen = fn.mkPen(opts['pen']) if pen.style() == QtCore.Qt.NoPen: linestyle = '' else: linestyle = '-' - color = tuple([c/255. for c in pg.colorTuple(pen.color())]) + color = tuple([c/255. for c in fn.colorTuple(pen.color())]) symbol = opts['symbol'] if symbol == 't': symbol = '^' - symbolPen = pg.mkPen(opts['symbolPen']) - symbolBrush = pg.mkBrush(opts['symbolBrush']) - markeredgecolor = tuple([c/255. for c in pg.colorTuple(symbolPen.color())]) - markerfacecolor = tuple([c/255. for c in pg.colorTuple(symbolBrush.color())]) + symbolPen = fn.mkPen(opts['symbolPen']) + symbolBrush = fn.mkBrush(opts['symbolBrush']) + markeredgecolor = tuple([c/255. for c in fn.colorTuple(symbolPen.color())]) + markerfacecolor = tuple([c/255. for c in fn.colorTuple(symbolBrush.color())]) if opts['fillLevel'] is not None and opts['fillBrush'] is not None: - fillBrush = pg.mkBrush(opts['fillBrush']) - fillcolor = tuple([c/255. for c in pg.colorTuple(fillBrush.color())]) + fillBrush = fn.mkBrush(opts['fillBrush']) + fillcolor = tuple([c/255. for c in fn.colorTuple(fillBrush.color())]) ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor) ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(), linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor) @@ -62,7 +62,7 @@ MatplotlibExporter.register() class MatplotlibWindow(QtGui.QMainWindow): def __init__(self): - import pyqtgraph.widgets.MatplotlibWidget + from .. import widgets.MatplotlibWidget QtGui.QMainWindow.__init__(self) self.mpl = pyqtgraph.widgets.MatplotlibWidget.MatplotlibWidget() self.setCentralWidget(self.mpl) diff --git a/pyqtgraph/exporters/PrintExporter.py b/pyqtgraph/exporters/PrintExporter.py index ef35c2f8..3e2d45fa 100644 --- a/pyqtgraph/exporters/PrintExporter.py +++ b/pyqtgraph/exporters/PrintExporter.py @@ -1,6 +1,6 @@ from .Exporter import Exporter -from pyqtgraph.parametertree import Parameter -from pyqtgraph.Qt import QtGui, QtCore, QtSvg +from ..parametertree import Parameter +from ..Qt import QtGui, QtCore, QtSvg import re __all__ = ['PrintExporter'] diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 425f48e9..4a02965b 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -1,8 +1,9 @@ from .Exporter import Exporter -from pyqtgraph.python2_3 import asUnicode -from pyqtgraph.parametertree import Parameter -from pyqtgraph.Qt import QtGui, QtCore, QtSvg -import pyqtgraph as pg +from ..python2_3 import asUnicode +from ..parametertree import Parameter +from ..Qt import QtGui, QtCore, QtSvg +from .. import debug +from .. import functions as fn import re import xml.dom.minidom as xml import numpy as np @@ -156,7 +157,7 @@ def _generateItemSvg(item, nodes=None, root=None): ## ## Both 2 and 3 can be addressed by drawing all items in world coordinates. - profiler = pg.debug.Profiler() + profiler = debug.Profiler() if nodes is None: ## nodes maps all node IDs to their XML element. ## this allows us to ensure all elements receive unique names. @@ -196,17 +197,12 @@ def _generateItemSvg(item, nodes=None, root=None): tr2 = QtGui.QTransform() tr2.translate(-rootPos.x(), -rootPos.y()) tr = tr * tr2 - #print item, pg.SRTTransform(tr) - #tr.translate(item.pos().x(), item.pos().y()) - #tr = tr * item.transform() arr = QtCore.QByteArray() buf = QtCore.QBuffer(arr) svg = QtSvg.QSvgGenerator() svg.setOutputDevice(buf) dpi = QtGui.QDesktopWidget().physicalDpiX() - ### not really sure why this works, but it seems to be important: - #self.svg.setSize(QtCore.QSize(self.params['width']*dpi/90., self.params['height']*dpi/90.)) svg.setResolution(dpi) p = QtGui.QPainter() @@ -350,7 +346,7 @@ def correctCoordinates(node, item): if ch.tagName == 'polyline': removeTransform = True coords = np.array([[float(a) for a in c.split(',')] for c in ch.getAttribute('points').strip().split(' ')]) - coords = pg.transformCoordinates(tr, coords, transpose=True) + coords = fn.transformCoordinates(tr, coords, transpose=True) ch.setAttribute('points', ' '.join([','.join([str(a) for a in c]) for c in coords])) elif ch.tagName == 'path': removeTransform = True @@ -365,7 +361,7 @@ def correctCoordinates(node, item): x = x[1:] else: t = '' - nc = pg.transformCoordinates(tr, np.array([[float(x),float(y)]]), transpose=True) + nc = fn.transformCoordinates(tr, np.array([[float(x),float(y)]]), transpose=True) newCoords += t+str(nc[0,0])+','+str(nc[0,1])+' ' ch.setAttribute('d', newCoords) elif ch.tagName == 'text': @@ -375,7 +371,7 @@ def correctCoordinates(node, item): #[float(ch.getAttribute('x')), float(ch.getAttribute('y'))], #[float(ch.getAttribute('font-size')), 0], #[0,0]]) - #c = pg.transformCoordinates(tr, c, transpose=True) + #c = fn.transformCoordinates(tr, c, transpose=True) #ch.setAttribute('x', str(c[0,0])) #ch.setAttribute('y', str(c[0,1])) #fs = c[1]-c[2] @@ -397,7 +393,7 @@ def correctCoordinates(node, item): ## correct line widths if needed if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke': w = float(grp.getAttribute('stroke-width')) - s = pg.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True) + s = fn.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True) w = ((s[0]-s[1])**2).sum()**0.5 ch.setAttribute('stroke-width', str(w)) @@ -443,35 +439,9 @@ def itemTransform(item, root): tr = item.sceneTransform() else: tr = itemTransform(nextRoot, root) * item.itemTransform(nextRoot)[0] - #pos = QtGui.QTransform() - #pos.translate(root.pos().x(), root.pos().y()) - #tr = pos * root.transform() * item.itemTransform(root)[0] - return tr - -#def correctStroke(node, item, root, width=1): - ##print "==============", item, node - #if node.hasAttribute('stroke-width'): - #width = float(node.getAttribute('stroke-width')) - #if node.getAttribute('vector-effect') == 'non-scaling-stroke': - #node.removeAttribute('vector-effect') - #if isinstance(root, QtGui.QGraphicsScene): - #w = item.mapFromScene(pg.Point(width,0)) - #o = item.mapFromScene(pg.Point(0,0)) - #else: - #w = item.mapFromItem(root, pg.Point(width,0)) - #o = item.mapFromItem(root, pg.Point(0,0)) - #w = w-o - ##print " ", w, o, w-o - #w = (w.x()**2 + w.y()**2) ** 0.5 - ##print " ", w - #node.setAttribute('stroke-width', str(w)) - - #for ch in node.childNodes: - #if isinstance(ch, xml.Element): - #correctStroke(ch, item, root, width) def cleanXml(node): ## remove extraneous text; let the xml library do the formatting. diff --git a/pyqtgraph/exporters/__init__.py b/pyqtgraph/exporters/__init__.py index e2a81bc2..8be1c3b6 100644 --- a/pyqtgraph/exporters/__init__.py +++ b/pyqtgraph/exporters/__init__.py @@ -1,16 +1,3 @@ -#Exporters = [] -#from pyqtgraph import importModules -#import os -#d = os.path.split(__file__)[0] -#for mod in importModules('', globals(), locals(), excludes=['Exporter']).values(): - #if hasattr(mod, '__all__'): - #names = mod.__all__ - #else: - #names = [n for n in dir(mod) if n[0] != '_'] - #for k in names: - #if hasattr(mod, k): - #Exporters.append(getattr(mod, k)) - from .Exporter import Exporter from .ImageExporter import * from .SVGExporter import * diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index e96692e2..4ad30b6e 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -63,8 +63,8 @@ class Parallelize(object): self.showProgress = True if isinstance(progressDialog, basestring): progressDialog = {'labelText': progressDialog} - import pyqtgraph as pg - self.progressDlg = pg.ProgressDialog(**progressDialog) + from ..widgets.ProgressDialog import ProgressDialog + self.progressDlg = ProgressDialog(**progressDialog) if workers is None: workers = self.suggestedWorkerCount() diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 4d32c999..5a4ccb99 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -1,7 +1,8 @@ from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy import subprocess, atexit, os, sys, time, random, socket, signal import multiprocessing.connection -import pyqtgraph as pg +from ..Qt import USE_PYSIDE + try: import cPickle as pickle except ImportError: @@ -118,7 +119,7 @@ class Process(RemoteEventHandler): ppid=pid, targetStr=targetStr, path=sysPath, - pyside=pg.Qt.USE_PYSIDE, + pyside=USE_PYSIDE, debug=debug ) pickle.dump(data, self.proc.stdin) @@ -337,7 +338,7 @@ class RemoteQtEventHandler(RemoteEventHandler): RemoteEventHandler.__init__(self, *args, **kwds) def startEventTimer(self): - from pyqtgraph.Qt import QtGui, QtCore + from ..Qt import QtGui, QtCore self.timer = QtCore.QTimer() self.timer.timeout.connect(self.processRequests) self.timer.start(10) @@ -346,7 +347,7 @@ class RemoteQtEventHandler(RemoteEventHandler): try: RemoteEventHandler.processRequests(self) except ClosedError: - from pyqtgraph.Qt import QtGui, QtCore + from ..Qt import QtGui, QtCore QtGui.QApplication.instance().quit() self.timer.stop() #raise SystemExit @@ -384,7 +385,7 @@ class QtProcess(Process): self.startEventTimer() def startEventTimer(self): - from pyqtgraph.Qt import QtGui, QtCore ## avoid module-level import to keep bootstrap snappy. + from ..Qt import QtGui, QtCore ## avoid module-level import to keep bootstrap snappy. self.timer = QtCore.QTimer() if self._processRequests: app = QtGui.QApplication.instance() @@ -415,7 +416,7 @@ def startQtEventLoop(name, port, authkey, ppid, debug=False): conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) if debug: print('[%d] connected; starting remote proxy.' % os.getpid()) - from pyqtgraph.Qt import QtGui, QtCore + from ..Qt import QtGui, QtCore #from PyQt4 import QtGui, QtCore app = QtGui.QApplication.instance() #print app diff --git a/pyqtgraph/opengl/GLGraphicsItem.py b/pyqtgraph/opengl/GLGraphicsItem.py index 9680fba7..cdfaa683 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -1,5 +1,5 @@ -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph import Transform3D +from ..Qt import QtGui, QtCore +from .. import Transform3D from OpenGL.GL import * from OpenGL import GL diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 89fef92e..d74a13ce 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -1,9 +1,9 @@ -from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL +from ..Qt import QtCore, QtGui, QtOpenGL from OpenGL.GL import * import OpenGL.GL.framebufferobjects as glfbo import numpy as np -from pyqtgraph import Vector -import pyqtgraph.functions as fn +from .. import Vector +from .. import functions as fn ##Vector = QtGui.QVector3D @@ -179,7 +179,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): self._itemNames[id(i)] = i i.paint() except: - import pyqtgraph.debug + from .. import debug pyqtgraph.debug.printExc() msg = "Error while drawing item %s." % str(item) ver = glGetString(GL_VERSION) @@ -345,7 +345,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): ## Only to be called from within exception handler. ver = glGetString(GL_VERSION).split()[0] if int(ver.split('.')[0]) < 2: - import pyqtgraph.debug + from .. import debug pyqtgraph.debug.printExc() raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver) else: diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index 71e566c9..3046459d 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -1,5 +1,5 @@ -from pyqtgraph.Qt import QtGui -import pyqtgraph.functions as fn +from ..Qt import QtGui +from .. import functions as fn import numpy as np class MeshData(object): diff --git a/pyqtgraph/opengl/__init__.py b/pyqtgraph/opengl/__init__.py index d10932a5..931003e4 100644 --- a/pyqtgraph/opengl/__init__.py +++ b/pyqtgraph/opengl/__init__.py @@ -1,7 +1,7 @@ from .GLViewWidget import GLViewWidget ## dynamic imports cause too many problems. -#from pyqtgraph import importAll +#from .. import importAll #importAll('items', globals(), locals()) from .items.GLGridItem import * diff --git a/pyqtgraph/opengl/glInfo.py b/pyqtgraph/opengl/glInfo.py index 28da1f69..84346d81 100644 --- a/pyqtgraph/opengl/glInfo.py +++ b/pyqtgraph/opengl/glInfo.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL +from ..Qt import QtCore, QtGui, QtOpenGL from OpenGL.GL import * app = QtGui.QApplication([]) diff --git a/pyqtgraph/opengl/items/GLAxisItem.py b/pyqtgraph/opengl/items/GLAxisItem.py index 860ac497..c6c206e4 100644 --- a/pyqtgraph/opengl/items/GLAxisItem.py +++ b/pyqtgraph/opengl/items/GLAxisItem.py @@ -1,6 +1,6 @@ from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem -from pyqtgraph import QtGui +from ... import QtGui __all__ = ['GLAxisItem'] diff --git a/pyqtgraph/opengl/items/GLBoxItem.py b/pyqtgraph/opengl/items/GLBoxItem.py index bc25afd1..f0a6ae6c 100644 --- a/pyqtgraph/opengl/items/GLBoxItem.py +++ b/pyqtgraph/opengl/items/GLBoxItem.py @@ -1,7 +1,7 @@ from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem -from pyqtgraph.Qt import QtGui -import pyqtgraph as pg +from ...Qt import QtGui +from ... import functions as fn __all__ = ['GLBoxItem'] @@ -38,7 +38,7 @@ class GLBoxItem(GLGraphicsItem): def setColor(self, *args): """Set the color of the box. Arguments are the same as those accepted by functions.mkColor()""" - self.__color = pg.Color(*args) + self.__color = fn.Color(*args) def color(self): return self.__color diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index 01a2b178..2c4642c8 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -1,6 +1,6 @@ from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem -from pyqtgraph import QtGui +from ... import QtGui __all__ = ['GLGridItem'] diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py index aca68f3d..2cab23a3 100644 --- a/pyqtgraph/opengl/items/GLImageItem.py +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -1,6 +1,6 @@ from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem -from pyqtgraph.Qt import QtGui +from ...Qt import QtGui import numpy as np __all__ = ['GLImageItem'] diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py index 23d227c9..a578dd1d 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -2,7 +2,7 @@ from OpenGL.GL import * from OpenGL.arrays import vbo from .. GLGraphicsItem import GLGraphicsItem from .. import shaders -from pyqtgraph import QtGui +from ... import QtGui import numpy as np __all__ = ['GLLinePlotItem'] diff --git a/pyqtgraph/opengl/items/GLMeshItem.py b/pyqtgraph/opengl/items/GLMeshItem.py index 5b245e64..14d178f8 100644 --- a/pyqtgraph/opengl/items/GLMeshItem.py +++ b/pyqtgraph/opengl/items/GLMeshItem.py @@ -1,9 +1,9 @@ from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem from .. MeshData import MeshData -from pyqtgraph.Qt import QtGui -import pyqtgraph as pg +from ...Qt import QtGui from .. import shaders +from ... import functions as fn import numpy as np @@ -177,7 +177,7 @@ class GLMeshItem(GLGraphicsItem): if self.colors is None: color = self.opts['color'] if isinstance(color, QtGui.QColor): - glColor4f(*pg.glColor(color)) + glColor4f(*fn.glColor(color)) else: glColor4f(*color) else: @@ -209,7 +209,7 @@ class GLMeshItem(GLGraphicsItem): if self.edgeColors is None: color = self.opts['edgeColor'] if isinstance(color, QtGui.QColor): - glColor4f(*pg.glColor(color)) + glColor4f(*fn.glColor(color)) else: glColor4f(*color) else: diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index b02a9dda..9ddd3b34 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -2,7 +2,7 @@ from OpenGL.GL import * from OpenGL.arrays import vbo from .. GLGraphicsItem import GLGraphicsItem from .. import shaders -from pyqtgraph import QtGui +from ... import QtGui import numpy as np __all__ = ['GLScatterPlotItem'] diff --git a/pyqtgraph/opengl/items/GLSurfacePlotItem.py b/pyqtgraph/opengl/items/GLSurfacePlotItem.py index 88d50fac..9c41a878 100644 --- a/pyqtgraph/opengl/items/GLSurfacePlotItem.py +++ b/pyqtgraph/opengl/items/GLSurfacePlotItem.py @@ -1,8 +1,7 @@ from OpenGL.GL import * from .GLMeshItem import GLMeshItem from .. MeshData import MeshData -from pyqtgraph.Qt import QtGui -import pyqtgraph as pg +from ...Qt import QtGui import numpy as np diff --git a/pyqtgraph/opengl/items/GLVolumeItem.py b/pyqtgraph/opengl/items/GLVolumeItem.py index 4980239d..84f23e12 100644 --- a/pyqtgraph/opengl/items/GLVolumeItem.py +++ b/pyqtgraph/opengl/items/GLVolumeItem.py @@ -1,6 +1,6 @@ from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem -from pyqtgraph.Qt import QtGui +from ...Qt import QtGui import numpy as np __all__ = ['GLVolumeItem'] From eaf29b5f07e31f89ad3463a252bb540d3eb320fe Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 23 Dec 2013 10:01:20 -0500 Subject: [PATCH 043/268] flowchart, canvas --- pyqtgraph/canvas/Canvas.py | 20 ++--- pyqtgraph/canvas/CanvasItem.py | 26 +++--- pyqtgraph/canvas/CanvasManager.py | 2 +- pyqtgraph/canvas/CanvasTemplate_pyqt.py | 4 +- pyqtgraph/canvas/CanvasTemplate_pyside.py | 4 +- pyqtgraph/flowchart/Flowchart.py | 20 ++--- .../flowchart/FlowchartCtrlTemplate_pyqt.py | 4 +- .../flowchart/FlowchartCtrlTemplate_pyside.py | 4 +- pyqtgraph/flowchart/FlowchartGraphicsView.py | 8 +- pyqtgraph/flowchart/FlowchartTemplate_pyqt.py | 4 +- .../flowchart/FlowchartTemplate_pyside.py | 4 +- pyqtgraph/flowchart/Node.py | 10 +-- pyqtgraph/flowchart/NodeLibrary.py | 2 +- pyqtgraph/flowchart/Terminal.py | 8 +- pyqtgraph/flowchart/eq.py | 2 +- pyqtgraph/flowchart/library/Data.py | 10 +-- pyqtgraph/flowchart/library/Display.py | 11 ++- pyqtgraph/flowchart/library/Filters.py | 6 +- pyqtgraph/flowchart/library/__init__.py | 80 +------------------ pyqtgraph/flowchart/library/common.py | 10 +-- pyqtgraph/flowchart/library/functions.py | 2 +- 21 files changed, 79 insertions(+), 162 deletions(-) diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index 17a39c2b..d07b3428 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -3,29 +3,21 @@ if __name__ == '__main__': import sys, os md = os.path.dirname(os.path.abspath(__file__)) sys.path = [os.path.dirname(md), os.path.join(md, '..', '..', '..')] + sys.path - #print md - -#from pyqtgraph.GraphicsView import GraphicsView -#import pyqtgraph.graphicsItems as graphicsItems -#from pyqtgraph.PlotWidget import PlotWidget -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE -from pyqtgraph.graphicsItems.ROI import ROI -from pyqtgraph.graphicsItems.ViewBox import ViewBox -from pyqtgraph.graphicsItems.GridItem import GridItem +from ..Qt import QtGui, QtCore, USE_PYSIDE +from ..graphicsItems.ROI import ROI +from ..graphicsItems.ViewBox import ViewBox +from ..graphicsItems.GridItem import GridItem if USE_PYSIDE: from .CanvasTemplate_pyside import * else: from .CanvasTemplate_pyqt import * -#import DataManager import numpy as np -from pyqtgraph import debug -#import pyqtgraph as pg +from .. import debug import weakref from .CanvasManager import CanvasManager -#import items from .CanvasItem import CanvasItem, GroupCanvasItem class Canvas(QtGui.QWidget): @@ -605,4 +597,4 @@ class SelectBox(ROI): - \ No newline at end of file + diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index 81388cb6..a808765c 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtGui, QtCore, QtSvg, USE_PYSIDE -from pyqtgraph.graphicsItems.ROI import ROI -import pyqtgraph as pg +from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE +from ..graphicsItems.ROI import ROI +from .. import SRTTransform, ItemGroup if USE_PYSIDE: from . import TransformGuiTemplate_pyside as TransformGuiTemplate else: from . import TransformGuiTemplate_pyqt as TransformGuiTemplate -from pyqtgraph import debug +from .. import debug class SelectBox(ROI): def __init__(self, scalable=False, rotatable=True): @@ -96,7 +96,7 @@ class CanvasItem(QtCore.QObject): if 'transform' in self.opts: self.baseTransform = self.opts['transform'] else: - self.baseTransform = pg.SRTTransform() + self.baseTransform = SRTTransform() if 'pos' in self.opts and self.opts['pos'] is not None: self.baseTransform.translate(self.opts['pos']) if 'angle' in self.opts and self.opts['angle'] is not None: @@ -124,8 +124,8 @@ class CanvasItem(QtCore.QObject): self.itemScale = QtGui.QGraphicsScale() self._graphicsItem.setTransformations([self.itemRotation, self.itemScale]) - self.tempTransform = pg.SRTTransform() ## holds the additional transform that happens during a move - gets added to the userTransform when move is done. - self.userTransform = pg.SRTTransform() ## stores the total transform of the object + self.tempTransform = SRTTransform() ## holds the additional transform that happens during a move - gets added to the userTransform when move is done. + self.userTransform = SRTTransform() ## stores the total transform of the object self.resetUserTransform() ## now happens inside resetUserTransform -> selectBoxToItem @@ -200,7 +200,7 @@ class CanvasItem(QtCore.QObject): #flip = self.transformGui.mirrorImageCheck.isChecked() #tr = self.userTransform.saveState() - inv = pg.SRTTransform() + inv = SRTTransform() inv.scale(-1, 1) self.userTransform = self.userTransform * inv self.updateTransform() @@ -231,7 +231,7 @@ class CanvasItem(QtCore.QObject): if not self.isMovable(): return self.rotate(180.) - # inv = pg.SRTTransform() + # inv = SRTTransform() # inv.scale(-1, -1) # self.userTransform = self.userTransform * inv #flip lr/ud # s=self.updateTransform() @@ -316,7 +316,7 @@ class CanvasItem(QtCore.QObject): def resetTemporaryTransform(self): - self.tempTransform = pg.SRTTransform() ## don't use Transform.reset()--this transform might be used elsewhere. + self.tempTransform = SRTTransform() ## don't use Transform.reset()--this transform might be used elsewhere. self.updateTransform() def transform(self): @@ -368,7 +368,7 @@ class CanvasItem(QtCore.QObject): try: #self.userTranslate = pg.Point(tr['trans']) #self.userRotate = tr['rot'] - self.userTransform = pg.SRTTransform(tr) + self.userTransform = SRTTransform(tr) self.updateTransform() self.selectBoxFromUser() ## move select box to match @@ -377,7 +377,7 @@ class CanvasItem(QtCore.QObject): except: #self.userTranslate = pg.Point([0,0]) #self.userRotate = 0 - self.userTransform = pg.SRTTransform() + self.userTransform = SRTTransform() debug.printExc("Failed to load transform:") #print "set transform", self, self.userTranslate @@ -504,6 +504,6 @@ class GroupCanvasItem(CanvasItem): def __init__(self, **opts): defOpts = {'movable': False, 'scalable': False} defOpts.update(opts) - item = pg.ItemGroup() + item = ItemGroup() CanvasItem.__init__(self, item, **defOpts) diff --git a/pyqtgraph/canvas/CanvasManager.py b/pyqtgraph/canvas/CanvasManager.py index e89ec00f..28188039 100644 --- a/pyqtgraph/canvas/CanvasManager.py +++ b/pyqtgraph/canvas/CanvasManager.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtCore, QtGui +from ..Qt import QtCore, QtGui if not hasattr(QtCore, 'Signal'): QtCore.Signal = QtCore.pyqtSignal import weakref diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/pyqtgraph/canvas/CanvasTemplate_pyqt.py index 4d1d8208..e465640d 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt.py @@ -95,6 +95,6 @@ class Ui_Form(object): self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.widgets.GraphicsView import GraphicsView +from ..widgets.GraphicsView import GraphicsView from CanvasManager import CanvasCombo -from pyqtgraph.widgets.TreeWidget import TreeWidget +from ..widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside.py b/pyqtgraph/canvas/CanvasTemplate_pyside.py index 12afdf25..8350ed33 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyside.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyside.py @@ -90,6 +90,6 @@ class Ui_Form(object): self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.widgets.GraphicsView import GraphicsView +from ..widgets.GraphicsView import GraphicsView from CanvasManager import CanvasCombo -from pyqtgraph.widgets.TreeWidget import TreeWidget +from ..widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index f566e97c..8d1ea4ce 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE +from ..Qt import QtCore, QtGui, USE_PYSIDE from .Node import * -from pyqtgraph.pgcollections import OrderedDict -from pyqtgraph.widgets.TreeWidget import * +from ..pgcollections import OrderedDict +from ..widgets.TreeWidget import * +from .. import FileDialog, DataTreeWidget ## pyside and pyqt use incompatible ui files. if USE_PYSIDE: @@ -15,10 +16,9 @@ else: from .Terminal import Terminal from numpy import ndarray from .library import LIBRARY -from pyqtgraph.debug import printExc -import pyqtgraph.configfile as configfile -import pyqtgraph.dockarea as dockarea -import pyqtgraph as pg +from ..debug import printExc +from .. import configfile as configfile +from .. import dockarea as dockarea from . import FlowchartGraphicsView def strDict(d): @@ -537,7 +537,7 @@ class Flowchart(Node): startDir = self.filePath if startDir is None: startDir = '.' - self.fileDialog = pg.FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") + self.fileDialog = FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.show() @@ -558,7 +558,7 @@ class Flowchart(Node): startDir = self.filePath if startDir is None: startDir = '.' - self.fileDialog = pg.FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") + self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) #self.fileDialog.setDirectory(startDir) @@ -821,7 +821,7 @@ class FlowchartWidget(dockarea.DockArea): self.selDescLabel = QtGui.QLabel() self.selNameLabel = QtGui.QLabel() self.selDescLabel.setWordWrap(True) - self.selectedTree = pg.DataTreeWidget() + self.selectedTree = DataTreeWidget() #self.selectedTree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) #self.selInfoLayout.addWidget(self.selNameLabel) self.selInfoLayout.addWidget(self.selDescLabel) diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py index 0410cdf3..41b7647d 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py @@ -67,5 +67,5 @@ class Ui_Form(object): self.reloadBtn.setText(QtGui.QApplication.translate("Form", "Reload Libs", None, QtGui.QApplication.UnicodeUTF8)) self.showChartBtn.setText(QtGui.QApplication.translate("Form", "Flowchart", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.widgets.FeedbackButton import FeedbackButton -from pyqtgraph.widgets.TreeWidget import TreeWidget +from ..widgets.FeedbackButton import FeedbackButton +from ..widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py index f579c957..5695f8e6 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py @@ -62,5 +62,5 @@ class Ui_Form(object): self.reloadBtn.setText(QtGui.QApplication.translate("Form", "Reload Libs", None, QtGui.QApplication.UnicodeUTF8)) self.showChartBtn.setText(QtGui.QApplication.translate("Form", "Flowchart", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.widgets.FeedbackButton import FeedbackButton -from pyqtgraph.widgets.TreeWidget import TreeWidget +from ..widgets.FeedbackButton import FeedbackButton +from ..widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/flowchart/FlowchartGraphicsView.py b/pyqtgraph/flowchart/FlowchartGraphicsView.py index 0ec4d5c8..ab4b2914 100644 --- a/pyqtgraph/flowchart/FlowchartGraphicsView.py +++ b/pyqtgraph/flowchart/FlowchartGraphicsView.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtGui, QtCore -from pyqtgraph.widgets.GraphicsView import GraphicsView -from pyqtgraph.GraphicsScene import GraphicsScene -from pyqtgraph.graphicsItems.ViewBox import ViewBox +from ..Qt import QtGui, QtCore +from ..widgets.GraphicsView import GraphicsView +from ..GraphicsScene import GraphicsScene +from ..graphicsItems.ViewBox import ViewBox #class FlowchartGraphicsView(QtGui.QGraphicsView): class FlowchartGraphicsView(GraphicsView): diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py b/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py index c07dd734..dabcdc32 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py @@ -55,5 +55,5 @@ class Ui_Form(object): def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.widgets.DataTreeWidget import DataTreeWidget -from pyqtgraph.flowchart.FlowchartGraphicsView import FlowchartGraphicsView +from ..widgets.DataTreeWidget import DataTreeWidget +from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyside.py b/pyqtgraph/flowchart/FlowchartTemplate_pyside.py index c73f3c00..1d47ed05 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyside.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyside.py @@ -50,5 +50,5 @@ class Ui_Form(object): def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.widgets.DataTreeWidget import DataTreeWidget -from pyqtgraph.flowchart.FlowchartGraphicsView import FlowchartGraphicsView +from ..widgets.DataTreeWidget import DataTreeWidget +from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py index f1de40d6..b6ed1e0f 100644 --- a/pyqtgraph/flowchart/Node.py +++ b/pyqtgraph/flowchart/Node.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtCore, QtGui -from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject -import pyqtgraph.functions as fn +from ..Qt import QtCore, QtGui +from ..graphicsItems.GraphicsObject import GraphicsObject +from .. import functions as fn from .Terminal import * -from pyqtgraph.pgcollections import OrderedDict -from pyqtgraph.debug import * +from ..pgcollections import OrderedDict +from ..debug import * import numpy as np from .eq import * diff --git a/pyqtgraph/flowchart/NodeLibrary.py b/pyqtgraph/flowchart/NodeLibrary.py index 20d0085e..a30ffb2a 100644 --- a/pyqtgraph/flowchart/NodeLibrary.py +++ b/pyqtgraph/flowchart/NodeLibrary.py @@ -1,4 +1,4 @@ -from pyqtgraph.pgcollections import OrderedDict +from ..pgcollections import OrderedDict from .Node import Node def isNodeClass(cls): diff --git a/pyqtgraph/flowchart/Terminal.py b/pyqtgraph/flowchart/Terminal.py index fea60dee..6a6db62e 100644 --- a/pyqtgraph/flowchart/Terminal.py +++ b/pyqtgraph/flowchart/Terminal.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtCore, QtGui +from ..Qt import QtCore, QtGui import weakref -from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject -import pyqtgraph.functions as fn -from pyqtgraph.Point import Point +from ..graphicsItems.GraphicsObject import GraphicsObject +from .. import functions as fn +from ..Point import Point #from PySide import QtCore, QtGui from .eq import * diff --git a/pyqtgraph/flowchart/eq.py b/pyqtgraph/flowchart/eq.py index 031ebce8..89ebe09f 100644 --- a/pyqtgraph/flowchart/eq.py +++ b/pyqtgraph/flowchart/eq.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from numpy import ndarray, bool_ -from pyqtgraph.metaarray import MetaArray +from ..metaarray import MetaArray def eq(a, b): """The great missing equivalence function: Guaranteed evaluation to a single bool value.""" diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py index cbef848a..52458bd9 100644 --- a/pyqtgraph/flowchart/library/Data.py +++ b/pyqtgraph/flowchart/library/Data.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- from ..Node import Node -from pyqtgraph.Qt import QtGui, QtCore +from ...Qt import QtGui, QtCore import numpy as np from .common import * -from pyqtgraph.SRTTransform import SRTTransform -from pyqtgraph.Point import Point -from pyqtgraph.widgets.TreeWidget import TreeWidget -from pyqtgraph.graphicsItems.LinearRegionItem import LinearRegionItem +from ...SRTTransform import SRTTransform +from ...Point import Point +from ...widgets.TreeWidget import TreeWidget +from ...graphicsItems.LinearRegionItem import LinearRegionItem from . import functions diff --git a/pyqtgraph/flowchart/library/Display.py b/pyqtgraph/flowchart/library/Display.py index 9068c0ec..2c352fb2 100644 --- a/pyqtgraph/flowchart/library/Display.py +++ b/pyqtgraph/flowchart/library/Display.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from ..Node import Node import weakref -#from pyqtgraph import graphicsItems -from pyqtgraph.Qt import QtCore, QtGui -from pyqtgraph.graphicsItems.ScatterPlotItem import ScatterPlotItem -from pyqtgraph.graphicsItems.PlotCurveItem import PlotCurveItem -from pyqtgraph import PlotDataItem +from ...Qt import QtCore, QtGui +from ...graphicsItems.ScatterPlotItem import ScatterPlotItem +from ...graphicsItems.PlotCurveItem import PlotCurveItem +from ... import PlotDataItem from .common import * import numpy as np @@ -272,4 +271,4 @@ class ScatterPlot(CtrlNode): #pos = file. - \ No newline at end of file + diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py index 090c261c..518c8c18 100644 --- a/pyqtgraph/flowchart/library/Filters.py +++ b/pyqtgraph/flowchart/library/Filters.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtCore, QtGui +from ...Qt import QtCore, QtGui from ..Node import Node from scipy.signal import detrend from scipy.ndimage import median_filter, gaussian_filter -#from pyqtgraph.SignalProxy import SignalProxy +#from ...SignalProxy import SignalProxy from . import functions from .common import * import numpy as np -import pyqtgraph.metaarray as metaarray +from ... import metaarray as metaarray class Downsample(CtrlNode): diff --git a/pyqtgraph/flowchart/library/__init__.py b/pyqtgraph/flowchart/library/__init__.py index 32a17b58..d8038aa4 100644 --- a/pyqtgraph/flowchart/library/__init__.py +++ b/pyqtgraph/flowchart/library/__init__.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- -from pyqtgraph.pgcollections import OrderedDict -#from pyqtgraph import importModules +from ...pgcollections import OrderedDict import os, types -from pyqtgraph.debug import printExc -#from ..Node import Node +from ...debug import printExc from ..NodeLibrary import NodeLibrary, isNodeClass -import pyqtgraph.reload as reload +from ... import reload as reload # Build default library @@ -21,82 +19,10 @@ getNodeType = LIBRARY.getNodeType # Add all nodes to the default library from . import Data, Display, Filters, Operators for mod in [Data, Display, Filters, Operators]: - #mod = getattr(__import__('', fromlist=[modName], level=1), modName) - #mod = __import__(modName, level=1) nodes = [getattr(mod, name) for name in dir(mod) if isNodeClass(getattr(mod, name))] for node in nodes: LIBRARY.addNodeType(node, [(mod.__name__.split('.')[-1],)]) -#NODE_LIST = OrderedDict() ## maps name:class for all registered Node subclasses -#NODE_TREE = OrderedDict() ## categorized tree of Node subclasses - -#def getNodeType(name): - #try: - #return NODE_LIST[name] - #except KeyError: - #raise Exception("No node type called '%s'" % name) - -#def getNodeTree(): - #return NODE_TREE - -#def registerNodeType(cls, paths, override=False): - #""" - #Register a new node type. If the type's name is already in use, - #an exception will be raised (unless override=True). - - #Arguments: - #cls - a subclass of Node (must have typ.nodeName) - #paths - list of tuples specifying the location(s) this - #type will appear in the library tree. - #override - if True, overwrite any class having the same name - #""" - #if not isNodeClass(cls): - #raise Exception("Object %s is not a Node subclass" % str(cls)) - - #name = cls.nodeName - #if not override and name in NODE_LIST: - #raise Exception("Node type name '%s' is already registered." % name) - - #NODE_LIST[name] = cls - #for path in paths: - #root = NODE_TREE - #for n in path: - #if n not in root: - #root[n] = OrderedDict() - #root = root[n] - #root[name] = cls - - - -#def isNodeClass(cls): - #try: - #if not issubclass(cls, Node): - #return False - #except: - #return False - #return hasattr(cls, 'nodeName') - -#def loadLibrary(reloadLibs=False, libPath=None): - #"""Import all Node subclasses found within files in the library module.""" - - #global NODE_LIST, NODE_TREE - - #if reloadLibs: - #reload.reloadAll(libPath) - - #mods = importModules('', globals(), locals()) - - #for name, mod in mods.items(): - #nodes = [] - #for n in dir(mod): - #o = getattr(mod, n) - #if isNodeClass(o): - #registerNodeType(o, [(name,)], override=reloadLibs) - -#def reloadLibrary(): - #loadLibrary(reloadLibs=True) - -#loadLibrary() diff --git a/pyqtgraph/flowchart/library/common.py b/pyqtgraph/flowchart/library/common.py index 65f8c1fd..548dc440 100644 --- a/pyqtgraph/flowchart/library/common.py +++ b/pyqtgraph/flowchart/library/common.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtCore, QtGui -from pyqtgraph.widgets.SpinBox import SpinBox -#from pyqtgraph.SignalProxy import SignalProxy -from pyqtgraph.WidgetGroup import WidgetGroup +from ...Qt import QtCore, QtGui +from ...widgets.SpinBox import SpinBox +#from ...SignalProxy import SignalProxy +from ...WidgetGroup import WidgetGroup #from ColorMapper import ColorMapper from ..Node import Node import numpy as np -from pyqtgraph.widgets.ColorButton import ColorButton +from ...widgets.ColorButton import ColorButton try: import metaarray HAVE_METAARRAY = True diff --git a/pyqtgraph/flowchart/library/functions.py b/pyqtgraph/flowchart/library/functions.py index 0476e02f..9efb8f36 100644 --- a/pyqtgraph/flowchart/library/functions.py +++ b/pyqtgraph/flowchart/library/functions.py @@ -1,6 +1,6 @@ import scipy import numpy as np -from pyqtgraph.metaarray import MetaArray +from ...metaarray import MetaArray def downsample(data, n, axis=0, xvals='subsample'): """Downsample by averaging points together across axis. From 50df2b2def326257151863aacdcd912b2cf7f3a9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 23 Dec 2013 10:06:26 -0500 Subject: [PATCH 044/268] dockarea, imageview, metaarray --- pyqtgraph/dockarea/Container.py | 2 +- pyqtgraph/dockarea/Dock.py | 4 ++-- pyqtgraph/dockarea/DockArea.py | 4 ++-- pyqtgraph/dockarea/DockDrop.py | 2 +- pyqtgraph/imageview/ImageView.py | 20 +++++++++---------- pyqtgraph/imageview/ImageViewTemplate_pyqt.py | 6 +++--- .../imageview/ImageViewTemplate_pyside.py | 6 +++--- pyqtgraph/metaarray/MetaArray.py | 4 ++-- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pyqtgraph/dockarea/Container.py b/pyqtgraph/dockarea/Container.py index 01ae51d3..277375f3 100644 --- a/pyqtgraph/dockarea/Container.py +++ b/pyqtgraph/dockarea/Container.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtCore, QtGui +from ..Qt import QtCore, QtGui import weakref class Container(object): diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 09a97813..f83397c7 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -1,7 +1,7 @@ -from pyqtgraph.Qt import QtCore, QtGui +from ..Qt import QtCore, QtGui from .DockDrop import * -from pyqtgraph.widgets.VerticalLabel import VerticalLabel +from ..widgets.VerticalLabel import VerticalLabel class Dock(QtGui.QWidget, DockDrop): diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index 882b29a3..5c367f0b 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtCore, QtGui +from ..Qt import QtCore, QtGui from .Container import * from .DockDrop import * from .Dock import Dock -import pyqtgraph.debug as debug +from .. import debug as debug import weakref ## TODO: diff --git a/pyqtgraph/dockarea/DockDrop.py b/pyqtgraph/dockarea/DockDrop.py index acab28cd..bd364f50 100644 --- a/pyqtgraph/dockarea/DockDrop.py +++ b/pyqtgraph/dockarea/DockDrop.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pyqtgraph.Qt import QtCore, QtGui +from ..Qt import QtCore, QtGui class DockDrop(object): """Provides dock-dropping methods""" diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 25700d93..d4458a0e 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -12,29 +12,29 @@ Widget used for displaying 2D or 3D data. Features: - ROI plotting - Image normalization through a variety of methods """ -from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE +from ..Qt import QtCore, QtGui, USE_PYSIDE if USE_PYSIDE: from .ImageViewTemplate_pyside import * else: from .ImageViewTemplate_pyqt import * -from pyqtgraph.graphicsItems.ImageItem import * -from pyqtgraph.graphicsItems.ROI import * -from pyqtgraph.graphicsItems.LinearRegionItem import * -from pyqtgraph.graphicsItems.InfiniteLine import * -from pyqtgraph.graphicsItems.ViewBox import * +from ..graphicsItems.ImageItem import * +from ..graphicsItems.ROI import * +from ..graphicsItems.LinearRegionItem import * +from ..graphicsItems.InfiniteLine import * +from ..graphicsItems.ViewBox import * #from widgets import ROI import sys #from numpy import ndarray -import pyqtgraph.ptime as ptime +from .. import ptime as ptime import numpy as np -import pyqtgraph.debug as debug +from .. import debug as debug -from pyqtgraph.SignalProxy import SignalProxy +from ..SignalProxy import SignalProxy #try: - #import pyqtgraph.metaarray as metaarray + #from .. import metaarray as metaarray #HAVE_METAARRAY = True #except: #HAVE_METAARRAY = False diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py index e6423276..8bdf1081 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py @@ -155,6 +155,6 @@ class Ui_Form(object): self.normTimeRangeCheck.setText(QtGui.QApplication.translate("Form", "Time range", None, QtGui.QApplication.UnicodeUTF8)) self.normFrameCheck.setText(QtGui.QApplication.translate("Form", "Frame", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.widgets.GraphicsView import GraphicsView -from pyqtgraph.widgets.PlotWidget import PlotWidget -from pyqtgraph.widgets.HistogramLUTWidget import HistogramLUTWidget +from ..widgets.GraphicsView import GraphicsView +from ..widgets.PlotWidget import PlotWidget +from ..widgets.HistogramLUTWidget import HistogramLUTWidget diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyside.py b/pyqtgraph/imageview/ImageViewTemplate_pyside.py index c17bbfe1..1732d60e 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyside.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyside.py @@ -150,6 +150,6 @@ class Ui_Form(object): self.normTimeRangeCheck.setText(QtGui.QApplication.translate("Form", "Time range", None, QtGui.QApplication.UnicodeUTF8)) self.normFrameCheck.setText(QtGui.QApplication.translate("Form", "Frame", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.widgets.GraphicsView import GraphicsView -from pyqtgraph.widgets.PlotWidget import PlotWidget -from pyqtgraph.widgets.HistogramLUTWidget import HistogramLUTWidget +from ..widgets.GraphicsView import GraphicsView +from ..widgets.PlotWidget import PlotWidget +from ..widgets.HistogramLUTWidget import HistogramLUTWidget diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index f55c60dc..d24a7d05 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -929,7 +929,7 @@ class MetaArray(object): if proc == False: raise Exception('remote read failed') if proc == None: - import pyqtgraph.multiprocess as mp + from .. import multiprocess as mp #print "new process" proc = mp.Process(executable='/usr/bin/python') proc.setProxyOptions(deferGetattr=True) @@ -1471,4 +1471,4 @@ if __name__ == '__main__': ma2 = MetaArray(file=tf, mmap=True) print("\nArrays are equivalent:", (ma == ma2).all()) os.remove(tf) - \ No newline at end of file + From 7e907db5577c4fe55b3085f88f08915476c14120 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 23 Dec 2013 10:08:26 -0500 Subject: [PATCH 045/268] .ui files --- pyqtgraph/canvas/CanvasTemplate.ui | 4 ++-- pyqtgraph/flowchart/FlowchartCtrlTemplate.ui | 4 ++-- pyqtgraph/flowchart/FlowchartTemplate.ui | 4 ++-- pyqtgraph/imageview/ImageViewTemplate.ui | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/canvas/CanvasTemplate.ui b/pyqtgraph/canvas/CanvasTemplate.ui index da032906..218cf48d 100644 --- a/pyqtgraph/canvas/CanvasTemplate.ui +++ b/pyqtgraph/canvas/CanvasTemplate.ui @@ -131,12 +131,12 @@ TreeWidget QTreeWidget -
pyqtgraph.widgets.TreeWidget
+
..widgets.TreeWidget
GraphicsView QGraphicsView -
pyqtgraph.widgets.GraphicsView
+
..widgets.GraphicsView
CanvasCombo diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui b/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui index 610846b6..0361ad3e 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui @@ -107,12 +107,12 @@ TreeWidget QTreeWidget -
pyqtgraph.widgets.TreeWidget
+
..widgets.TreeWidget
FeedbackButton QPushButton -
pyqtgraph.widgets.FeedbackButton
+
..widgets.FeedbackButton
diff --git a/pyqtgraph/flowchart/FlowchartTemplate.ui b/pyqtgraph/flowchart/FlowchartTemplate.ui index 31b1359c..8b0c19da 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate.ui +++ b/pyqtgraph/flowchart/FlowchartTemplate.ui @@ -85,12 +85,12 @@ DataTreeWidget QTreeWidget -
pyqtgraph.widgets.DataTreeWidget
+
..widgets.DataTreeWidget
FlowchartGraphicsView QGraphicsView -
pyqtgraph.flowchart.FlowchartGraphicsView
+
..flowchart.FlowchartGraphicsView
diff --git a/pyqtgraph/imageview/ImageViewTemplate.ui b/pyqtgraph/imageview/ImageViewTemplate.ui index 497c0c59..9a3dab03 100644 --- a/pyqtgraph/imageview/ImageViewTemplate.ui +++ b/pyqtgraph/imageview/ImageViewTemplate.ui @@ -233,18 +233,18 @@ PlotWidget QWidget -
pyqtgraph.widgets.PlotWidget
+
..widgets.PlotWidget
1
GraphicsView QGraphicsView -
pyqtgraph.widgets.GraphicsView
+
..widgets.GraphicsView
HistogramLUTWidget QGraphicsView -
pyqtgraph.widgets.HistogramLUTWidget
+
..widgets.HistogramLUTWidget
From a069104c6b45eb29f4a397c93f7464f58d510e0c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 23 Dec 2013 10:11:20 -0500 Subject: [PATCH 046/268] rebuild UI files --- .../exportDialogTemplate_pyqt.py | 31 +++++--- .../exportDialogTemplate_pyside.py | 6 +- pyqtgraph/canvas/CanvasTemplate_pyqt.py | 39 ++++++---- pyqtgraph/canvas/CanvasTemplate_pyside.py | 10 +-- pyqtgraph/canvas/TransformGuiTemplate_pyqt.py | 29 ++++--- .../canvas/TransformGuiTemplate_pyside.py | 6 +- pyqtgraph/console/template_pyqt.py | 37 +++++---- pyqtgraph/console/template_pyside.py | 6 +- .../flowchart/FlowchartCtrlTemplate_pyqt.py | 31 +++++--- .../flowchart/FlowchartCtrlTemplate_pyside.py | 8 +- pyqtgraph/flowchart/FlowchartTemplate_pyqt.py | 21 +++-- .../flowchart/FlowchartTemplate_pyside.py | 8 +- .../PlotItem/plotConfigTemplate_pyqt.py | 77 +++++++++++-------- .../PlotItem/plotConfigTemplate_pyside.py | 4 +- .../ViewBox/axisCtrlTemplate_pyqt.py | 59 ++++++++------ .../ViewBox/axisCtrlTemplate_pyside.py | 6 +- pyqtgraph/imageview/ImageViewTemplate_pyqt.py | 51 +++++++----- .../imageview/ImageViewTemplate_pyside.py | 8 +- 18 files changed, 259 insertions(+), 178 deletions(-) diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py index 3bbab155..ad7361ab 100644 --- a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/GraphicsScene/exportDialogTemplate.ui' # -# Created: Wed Jan 30 21:02:28 2013 -# by: PyQt4 UI code generator 4.9.3 +# Created: Mon Dec 23 10:10:52 2013 +# by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,16 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): @@ -57,12 +66,12 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("Form", "Item to export:", None, QtGui.QApplication.UnicodeUTF8)) - self.label_2.setText(QtGui.QApplication.translate("Form", "Export format", None, QtGui.QApplication.UnicodeUTF8)) - self.exportBtn.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8)) - self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8)) - self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8)) - self.copyBtn.setText(QtGui.QApplication.translate("Form", "Copy", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Export", None)) + self.label.setText(_translate("Form", "Item to export:", None)) + self.label_2.setText(_translate("Form", "Export format", None)) + self.exportBtn.setText(_translate("Form", "Export", None)) + self.closeBtn.setText(_translate("Form", "Close", None)) + self.label_3.setText(_translate("Form", "Export options", None)) + self.copyBtn.setText(_translate("Form", "Copy", None)) from ..parametertree import ParameterTree diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py index 8c95e717..f2e8dc70 100644 --- a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/GraphicsScene/exportDialogTemplate.ui' # -# Created: Wed Jan 30 21:02:28 2013 -# by: pyside-uic 0.2.13 running on PySide 1.1.1 +# Created: Mon Dec 23 10:10:53 2013 +# by: pyside-uic 0.2.14 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/pyqtgraph/canvas/CanvasTemplate_pyqt.py index e465640d..c809cb1d 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/canvas/CanvasTemplate.ui' # -# Created: Sun Sep 9 14:41:30 2012 -# by: PyQt4 UI code generator 4.9.1 +# Created: Mon Dec 23 10:10:52 2013 +# by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,16 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): @@ -85,16 +94,16 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.storeSvgBtn.setText(QtGui.QApplication.translate("Form", "Store SVG", None, QtGui.QApplication.UnicodeUTF8)) - self.storePngBtn.setText(QtGui.QApplication.translate("Form", "Store PNG", None, QtGui.QApplication.UnicodeUTF8)) - self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8)) - self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8)) - self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8)) - self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8)) - self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) - self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Form", None)) + self.storeSvgBtn.setText(_translate("Form", "Store SVG", None)) + self.storePngBtn.setText(_translate("Form", "Store PNG", None)) + self.autoRangeBtn.setText(_translate("Form", "Auto Range", None)) + self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas.", None)) + self.redirectCheck.setText(_translate("Form", "Redirect", None)) + self.resetTransformsBtn.setText(_translate("Form", "Reset Transforms", None)) + self.mirrorSelectionBtn.setText(_translate("Form", "Mirror Selection", None)) + self.reflectSelectionBtn.setText(_translate("Form", "MirrorXY", None)) -from ..widgets.GraphicsView import GraphicsView -from CanvasManager import CanvasCombo from ..widgets.TreeWidget import TreeWidget +from CanvasManager import CanvasCombo +from ..widgets.GraphicsView import GraphicsView diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside.py b/pyqtgraph/canvas/CanvasTemplate_pyside.py index 8350ed33..56d1ff47 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyside.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/canvas/CanvasTemplate.ui' # -# Created: Sun Sep 9 14:41:30 2012 -# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# Created: Mon Dec 23 10:10:52 2013 +# by: pyside-uic 0.2.14 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! @@ -90,6 +90,6 @@ class Ui_Form(object): self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) -from ..widgets.GraphicsView import GraphicsView -from CanvasManager import CanvasCombo from ..widgets.TreeWidget import TreeWidget +from CanvasManager import CanvasCombo +from ..widgets.GraphicsView import GraphicsView diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py index 1fb86d24..75c694c0 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './canvas/TransformGuiTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui' # -# Created: Sun Sep 9 14:41:30 2012 -# by: PyQt4 UI code generator 4.9.1 +# Created: Mon Dec 23 10:10:52 2013 +# by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,16 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): @@ -51,10 +60,10 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.translateLabel.setText(QtGui.QApplication.translate("Form", "Translate:", None, QtGui.QApplication.UnicodeUTF8)) - self.rotateLabel.setText(QtGui.QApplication.translate("Form", "Rotate:", None, QtGui.QApplication.UnicodeUTF8)) - self.scaleLabel.setText(QtGui.QApplication.translate("Form", "Scale:", None, QtGui.QApplication.UnicodeUTF8)) - self.mirrorImageBtn.setText(QtGui.QApplication.translate("Form", "Mirror", None, QtGui.QApplication.UnicodeUTF8)) - self.reflectImageBtn.setText(QtGui.QApplication.translate("Form", "Reflect", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Form", None)) + self.translateLabel.setText(_translate("Form", "Translate:", None)) + self.rotateLabel.setText(_translate("Form", "Rotate:", None)) + self.scaleLabel.setText(_translate("Form", "Scale:", None)) + self.mirrorImageBtn.setText(_translate("Form", "Mirror", None)) + self.reflectImageBtn.setText(_translate("Form", "Reflect", None)) diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyside.py b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py index 47b23faa..bce7b511 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyside.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './canvas/TransformGuiTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui' # -# Created: Sun Sep 9 14:41:30 2012 -# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# Created: Mon Dec 23 10:10:52 2013 +# by: pyside-uic 0.2.14 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! diff --git a/pyqtgraph/console/template_pyqt.py b/pyqtgraph/console/template_pyqt.py index 89ee6cff..e0852c93 100644 --- a/pyqtgraph/console/template_pyqt.py +++ b/pyqtgraph/console/template_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './console/template.ui' +# Form implementation generated from reading ui file './pyqtgraph/console/template.ui' # -# Created: Sun Sep 9 14:41:30 2012 -# by: PyQt4 UI code generator 4.9.1 +# Created: Mon Dec 23 10:10:53 2013 +# by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,16 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): @@ -97,15 +106,15 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Console", None, QtGui.QApplication.UnicodeUTF8)) - self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8)) - self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8)) - self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8)) - self.catchAllExceptionsBtn.setText(QtGui.QApplication.translate("Form", "Show All Exceptions", None, QtGui.QApplication.UnicodeUTF8)) - self.catchNextExceptionBtn.setText(QtGui.QApplication.translate("Form", "Show Next Exception", None, QtGui.QApplication.UnicodeUTF8)) - self.onlyUncaughtCheck.setText(QtGui.QApplication.translate("Form", "Only Uncaught Exceptions", None, QtGui.QApplication.UnicodeUTF8)) - self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8)) - self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Exception Info", None, QtGui.QApplication.UnicodeUTF8)) - self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Exception", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Console", None)) + self.historyBtn.setText(_translate("Form", "History..", None)) + self.exceptionBtn.setText(_translate("Form", "Exceptions..", None)) + self.exceptionGroup.setTitle(_translate("Form", "Exception Handling", None)) + self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions", None)) + self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception", None)) + self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions", None)) + self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame", None)) + self.exceptionInfoLabel.setText(_translate("Form", "Exception Info", None)) + self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None)) from .CmdInput import CmdInput diff --git a/pyqtgraph/console/template_pyside.py b/pyqtgraph/console/template_pyside.py index 0493a0fe..2db8ed95 100644 --- a/pyqtgraph/console/template_pyside.py +++ b/pyqtgraph/console/template_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './console/template.ui' +# Form implementation generated from reading ui file './pyqtgraph/console/template.ui' # -# Created: Sun Sep 9 14:41:30 2012 -# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# Created: Mon Dec 23 10:10:53 2013 +# by: pyside-uic 0.2.14 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py index 41b7647d..8afd43f8 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './flowchart/FlowchartCtrlTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartCtrlTemplate.ui' # -# Created: Sun Sep 9 14:41:30 2012 -# by: PyQt4 UI code generator 4.9.1 +# Created: Mon Dec 23 10:10:50 2013 +# by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,16 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): @@ -60,12 +69,12 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load..", None, QtGui.QApplication.UnicodeUTF8)) - self.saveBtn.setText(QtGui.QApplication.translate("Form", "Save", None, QtGui.QApplication.UnicodeUTF8)) - self.saveAsBtn.setText(QtGui.QApplication.translate("Form", "As..", None, QtGui.QApplication.UnicodeUTF8)) - self.reloadBtn.setText(QtGui.QApplication.translate("Form", "Reload Libs", None, QtGui.QApplication.UnicodeUTF8)) - self.showChartBtn.setText(QtGui.QApplication.translate("Form", "Flowchart", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Form", None)) + self.loadBtn.setText(_translate("Form", "Load..", None)) + self.saveBtn.setText(_translate("Form", "Save", None)) + self.saveAsBtn.setText(_translate("Form", "As..", None)) + self.reloadBtn.setText(_translate("Form", "Reload Libs", None)) + self.showChartBtn.setText(_translate("Form", "Flowchart", None)) -from ..widgets.FeedbackButton import FeedbackButton from ..widgets.TreeWidget import TreeWidget +from ..widgets.FeedbackButton import FeedbackButton diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py index 5695f8e6..b722000e 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './flowchart/FlowchartCtrlTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartCtrlTemplate.ui' # -# Created: Sun Sep 9 14:41:30 2012 -# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# Created: Mon Dec 23 10:10:51 2013 +# by: pyside-uic 0.2.14 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! @@ -62,5 +62,5 @@ class Ui_Form(object): self.reloadBtn.setText(QtGui.QApplication.translate("Form", "Reload Libs", None, QtGui.QApplication.UnicodeUTF8)) self.showChartBtn.setText(QtGui.QApplication.translate("Form", "Flowchart", None, QtGui.QApplication.UnicodeUTF8)) -from ..widgets.FeedbackButton import FeedbackButton from ..widgets.TreeWidget import TreeWidget +from ..widgets.FeedbackButton import FeedbackButton diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py b/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py index dabcdc32..06b10bfe 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './flowchart/FlowchartTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartTemplate.ui' # -# Created: Sun Feb 24 19:47:29 2013 -# by: PyQt4 UI code generator 4.9.3 +# Created: Mon Dec 23 10:10:51 2013 +# by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,16 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): @@ -53,7 +62,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Form", None)) -from ..widgets.DataTreeWidget import DataTreeWidget from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView +from ..widgets.DataTreeWidget import DataTreeWidget diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyside.py b/pyqtgraph/flowchart/FlowchartTemplate_pyside.py index 1d47ed05..2c693c60 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyside.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './flowchart/FlowchartTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartTemplate.ui' # -# Created: Sun Feb 24 19:47:30 2013 -# by: pyside-uic 0.2.13 running on PySide 1.1.1 +# Created: Mon Dec 23 10:10:51 2013 +# by: pyside-uic 0.2.14 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! @@ -50,5 +50,5 @@ class Ui_Form(object): def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) -from ..widgets.DataTreeWidget import DataTreeWidget from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView +from ..widgets.DataTreeWidget import DataTreeWidget diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py index 5335ee76..e09c9978 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py @@ -2,8 +2,8 @@ # Form implementation generated from reading ui file './pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui' # -# Created: Mon Jul 1 23:21:08 2013 -# by: PyQt4 UI code generator 4.9.3 +# Created: Mon Dec 23 10:10:51 2013 +# by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,16 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): @@ -139,35 +148,35 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.averageGroup.setToolTip(QtGui.QApplication.translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None, QtGui.QApplication.UnicodeUTF8)) - self.averageGroup.setTitle(QtGui.QApplication.translate("Form", "Average", None, QtGui.QApplication.UnicodeUTF8)) - self.clipToViewCheck.setToolTip(QtGui.QApplication.translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8)) - self.clipToViewCheck.setText(QtGui.QApplication.translate("Form", "Clip to View", None, QtGui.QApplication.UnicodeUTF8)) - self.maxTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) - self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8)) - self.downsampleCheck.setText(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) - self.peakRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by drawing a saw wave that follows the min and max of the original data. This method produces the best visual representation of the data but is slower.", None, QtGui.QApplication.UnicodeUTF8)) - self.peakRadio.setText(QtGui.QApplication.translate("Form", "Peak", None, QtGui.QApplication.UnicodeUTF8)) - self.maxTracesSpin.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) - self.forgetTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None, QtGui.QApplication.UnicodeUTF8)) - self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", None, QtGui.QApplication.UnicodeUTF8)) - self.meanRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the mean of N samples.", None, QtGui.QApplication.UnicodeUTF8)) - self.meanRadio.setText(QtGui.QApplication.translate("Form", "Mean", None, QtGui.QApplication.UnicodeUTF8)) - self.subsampleRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the first of N samples. This method is fastest and least accurate.", None, QtGui.QApplication.UnicodeUTF8)) - self.subsampleRadio.setText(QtGui.QApplication.translate("Form", "Subsample", None, QtGui.QApplication.UnicodeUTF8)) - self.autoDownsampleCheck.setToolTip(QtGui.QApplication.translate("Form", "Automatically downsample data based on the visible range. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8)) - self.autoDownsampleCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.downsampleSpin.setToolTip(QtGui.QApplication.translate("Form", "Downsample data before plotting. (plot every Nth sample)", None, QtGui.QApplication.UnicodeUTF8)) - self.downsampleSpin.setSuffix(QtGui.QApplication.translate("Form", "x", None, QtGui.QApplication.UnicodeUTF8)) - self.fftCheck.setText(QtGui.QApplication.translate("Form", "Power Spectrum (FFT)", None, QtGui.QApplication.UnicodeUTF8)) - self.logXCheck.setText(QtGui.QApplication.translate("Form", "Log X", None, QtGui.QApplication.UnicodeUTF8)) - self.logYCheck.setText(QtGui.QApplication.translate("Form", "Log Y", None, QtGui.QApplication.UnicodeUTF8)) - self.pointsGroup.setTitle(QtGui.QApplication.translate("Form", "Points", None, QtGui.QApplication.UnicodeUTF8)) - self.autoPointsCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.xGridCheck.setText(QtGui.QApplication.translate("Form", "Show X Grid", None, QtGui.QApplication.UnicodeUTF8)) - self.yGridCheck.setText(QtGui.QApplication.translate("Form", "Show Y Grid", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("Form", "Opacity", None, QtGui.QApplication.UnicodeUTF8)) - self.alphaGroup.setTitle(QtGui.QApplication.translate("Form", "Alpha", None, QtGui.QApplication.UnicodeUTF8)) - self.autoAlphaCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Form", None)) + self.averageGroup.setToolTip(_translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None)) + self.averageGroup.setTitle(_translate("Form", "Average", None)) + self.clipToViewCheck.setToolTip(_translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None)) + self.clipToViewCheck.setText(_translate("Form", "Clip to View", None)) + self.maxTracesCheck.setToolTip(_translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None)) + self.maxTracesCheck.setText(_translate("Form", "Max Traces:", None)) + self.downsampleCheck.setText(_translate("Form", "Downsample", None)) + self.peakRadio.setToolTip(_translate("Form", "Downsample by drawing a saw wave that follows the min and max of the original data. This method produces the best visual representation of the data but is slower.", None)) + self.peakRadio.setText(_translate("Form", "Peak", None)) + self.maxTracesSpin.setToolTip(_translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None)) + self.forgetTracesCheck.setToolTip(_translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None)) + self.forgetTracesCheck.setText(_translate("Form", "Forget hidden traces", None)) + self.meanRadio.setToolTip(_translate("Form", "Downsample by taking the mean of N samples.", None)) + self.meanRadio.setText(_translate("Form", "Mean", None)) + self.subsampleRadio.setToolTip(_translate("Form", "Downsample by taking the first of N samples. This method is fastest and least accurate.", None)) + self.subsampleRadio.setText(_translate("Form", "Subsample", None)) + self.autoDownsampleCheck.setToolTip(_translate("Form", "Automatically downsample data based on the visible range. This assumes X values are uniformly spaced.", None)) + self.autoDownsampleCheck.setText(_translate("Form", "Auto", None)) + self.downsampleSpin.setToolTip(_translate("Form", "Downsample data before plotting. (plot every Nth sample)", None)) + self.downsampleSpin.setSuffix(_translate("Form", "x", None)) + self.fftCheck.setText(_translate("Form", "Power Spectrum (FFT)", None)) + self.logXCheck.setText(_translate("Form", "Log X", None)) + self.logYCheck.setText(_translate("Form", "Log Y", None)) + self.pointsGroup.setTitle(_translate("Form", "Points", None)) + self.autoPointsCheck.setText(_translate("Form", "Auto", None)) + self.xGridCheck.setText(_translate("Form", "Show X Grid", None)) + self.yGridCheck.setText(_translate("Form", "Show Y Grid", None)) + self.label.setText(_translate("Form", "Opacity", None)) + self.alphaGroup.setTitle(_translate("Form", "Alpha", None)) + self.autoAlphaCheck.setText(_translate("Form", "Auto", None)) diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py index b8e0b19e..aff31211 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py @@ -2,8 +2,8 @@ # Form implementation generated from reading ui file './pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui' # -# Created: Mon Jul 1 23:21:08 2013 -# by: pyside-uic 0.2.13 running on PySide 1.1.2 +# Created: Mon Dec 23 10:10:52 2013 +# by: pyside-uic 0.2.14 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py index db14033e..d8ef1925 100644 --- a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './graphicsItems/ViewBox/axisCtrlTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui' # -# Created: Sun Sep 9 14:41:31 2012 -# by: PyQt4 UI code generator 4.9.1 +# Created: Mon Dec 23 10:10:51 2013 +# by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,16 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): @@ -69,25 +78,25 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("Form", "Link Axis:", None, QtGui.QApplication.UnicodeUTF8)) - self.linkCombo.setToolTip(QtGui.QApplication.translate("Form", "

Links this axis with another view. When linked, both views will display the same data range.

", None, QtGui.QApplication.UnicodeUTF8)) - self.autoPercentSpin.setToolTip(QtGui.QApplication.translate("Form", "

Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.

", None, QtGui.QApplication.UnicodeUTF8)) - self.autoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) - self.autoRadio.setToolTip(QtGui.QApplication.translate("Form", "

Automatically resize this axis whenever the displayed data is changed.

", None, QtGui.QApplication.UnicodeUTF8)) - self.autoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.manualRadio.setToolTip(QtGui.QApplication.translate("Form", "

Set the range for this axis manually. This disables automatic scaling.

", None, QtGui.QApplication.UnicodeUTF8)) - self.manualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.minText.setToolTip(QtGui.QApplication.translate("Form", "

Minimum value to display for this axis.

", None, QtGui.QApplication.UnicodeUTF8)) - self.minText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.maxText.setToolTip(QtGui.QApplication.translate("Form", "

Maximum value to display for this axis.

", None, QtGui.QApplication.UnicodeUTF8)) - self.maxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.invertCheck.setToolTip(QtGui.QApplication.translate("Form", "

Inverts the display of this axis. (+y points downward instead of upward)

", None, QtGui.QApplication.UnicodeUTF8)) - self.invertCheck.setText(QtGui.QApplication.translate("Form", "Invert Axis", None, QtGui.QApplication.UnicodeUTF8)) - self.mouseCheck.setToolTip(QtGui.QApplication.translate("Form", "

Enables mouse interaction (panning, scaling) for this axis.

", None, QtGui.QApplication.UnicodeUTF8)) - self.mouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse Enabled", None, QtGui.QApplication.UnicodeUTF8)) - self.visibleOnlyCheck.setToolTip(QtGui.QApplication.translate("Form", "

When checked, the axis will only auto-scale to data that is visible along the orthogonal axis.

", None, QtGui.QApplication.UnicodeUTF8)) - self.visibleOnlyCheck.setText(QtGui.QApplication.translate("Form", "Visible Data Only", None, QtGui.QApplication.UnicodeUTF8)) - self.autoPanCheck.setToolTip(QtGui.QApplication.translate("Form", "

When checked, the axis will automatically pan to center on the current data, but the scale along this axis will not change.

", None, QtGui.QApplication.UnicodeUTF8)) - self.autoPanCheck.setText(QtGui.QApplication.translate("Form", "Auto Pan Only", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Form", None)) + self.label.setText(_translate("Form", "Link Axis:", None)) + self.linkCombo.setToolTip(_translate("Form", "

Links this axis with another view. When linked, both views will display the same data range.

", None)) + self.autoPercentSpin.setToolTip(_translate("Form", "

Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.

", None)) + self.autoPercentSpin.setSuffix(_translate("Form", "%", None)) + self.autoRadio.setToolTip(_translate("Form", "

Automatically resize this axis whenever the displayed data is changed.

", None)) + self.autoRadio.setText(_translate("Form", "Auto", None)) + self.manualRadio.setToolTip(_translate("Form", "

Set the range for this axis manually. This disables automatic scaling.

", None)) + self.manualRadio.setText(_translate("Form", "Manual", None)) + self.minText.setToolTip(_translate("Form", "

Minimum value to display for this axis.

", None)) + self.minText.setText(_translate("Form", "0", None)) + self.maxText.setToolTip(_translate("Form", "

Maximum value to display for this axis.

", None)) + self.maxText.setText(_translate("Form", "0", None)) + self.invertCheck.setToolTip(_translate("Form", "

Inverts the display of this axis. (+y points downward instead of upward)

", None)) + self.invertCheck.setText(_translate("Form", "Invert Axis", None)) + self.mouseCheck.setToolTip(_translate("Form", "

Enables mouse interaction (panning, scaling) for this axis.

", None)) + self.mouseCheck.setText(_translate("Form", "Mouse Enabled", None)) + self.visibleOnlyCheck.setToolTip(_translate("Form", "

When checked, the axis will only auto-scale to data that is visible along the orthogonal axis.

", None)) + self.visibleOnlyCheck.setText(_translate("Form", "Visible Data Only", None)) + self.autoPanCheck.setToolTip(_translate("Form", "

When checked, the axis will automatically pan to center on the current data, but the scale along this axis will not change.

", None)) + self.autoPanCheck.setText(_translate("Form", "Auto Pan Only", None)) diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py index 18510bc2..9ddeb5d1 100644 --- a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './graphicsItems/ViewBox/axisCtrlTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui' # -# Created: Sun Sep 9 14:41:32 2012 -# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# Created: Mon Dec 23 10:10:51 2013 +# by: pyside-uic 0.2.14 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py index 8bdf1081..78156317 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './imageview/ImageViewTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/imageview/ImageViewTemplate.ui' # -# Created: Sun Sep 9 14:41:30 2012 -# by: PyQt4 UI code generator 4.9.1 +# Created: Mon Dec 23 10:10:52 2013 +# by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,16 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): @@ -138,23 +147,23 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) - self.normBtn.setText(QtGui.QApplication.translate("Form", "Norm", None, QtGui.QApplication.UnicodeUTF8)) - self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8)) - self.normSubtractRadio.setText(QtGui.QApplication.translate("Form", "Subtract", None, QtGui.QApplication.UnicodeUTF8)) - self.normDivideRadio.setText(QtGui.QApplication.translate("Form", "Divide", None, QtGui.QApplication.UnicodeUTF8)) - self.label_5.setText(QtGui.QApplication.translate("Form", "Operation:", None, QtGui.QApplication.UnicodeUTF8)) - self.label_3.setText(QtGui.QApplication.translate("Form", "Mean:", None, QtGui.QApplication.UnicodeUTF8)) - self.label_4.setText(QtGui.QApplication.translate("Form", "Blur:", None, QtGui.QApplication.UnicodeUTF8)) - self.normROICheck.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) - self.label_8.setText(QtGui.QApplication.translate("Form", "X", None, QtGui.QApplication.UnicodeUTF8)) - self.label_9.setText(QtGui.QApplication.translate("Form", "Y", None, QtGui.QApplication.UnicodeUTF8)) - self.label_10.setText(QtGui.QApplication.translate("Form", "T", None, QtGui.QApplication.UnicodeUTF8)) - self.normOffRadio.setText(QtGui.QApplication.translate("Form", "Off", None, QtGui.QApplication.UnicodeUTF8)) - self.normTimeRangeCheck.setText(QtGui.QApplication.translate("Form", "Time range", None, QtGui.QApplication.UnicodeUTF8)) - self.normFrameCheck.setText(QtGui.QApplication.translate("Form", "Frame", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Form", None)) + self.roiBtn.setText(_translate("Form", "ROI", None)) + self.normBtn.setText(_translate("Form", "Norm", None)) + self.normGroup.setTitle(_translate("Form", "Normalization", None)) + self.normSubtractRadio.setText(_translate("Form", "Subtract", None)) + self.normDivideRadio.setText(_translate("Form", "Divide", None)) + self.label_5.setText(_translate("Form", "Operation:", None)) + self.label_3.setText(_translate("Form", "Mean:", None)) + self.label_4.setText(_translate("Form", "Blur:", None)) + self.normROICheck.setText(_translate("Form", "ROI", None)) + self.label_8.setText(_translate("Form", "X", None)) + self.label_9.setText(_translate("Form", "Y", None)) + self.label_10.setText(_translate("Form", "T", None)) + self.normOffRadio.setText(_translate("Form", "Off", None)) + self.normTimeRangeCheck.setText(_translate("Form", "Time range", None)) + self.normFrameCheck.setText(_translate("Form", "Frame", None)) +from ..widgets.HistogramLUTWidget import HistogramLUTWidget from ..widgets.GraphicsView import GraphicsView from ..widgets.PlotWidget import PlotWidget -from ..widgets.HistogramLUTWidget import HistogramLUTWidget diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyside.py b/pyqtgraph/imageview/ImageViewTemplate_pyside.py index 1732d60e..2f8b570b 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyside.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './imageview/ImageViewTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/imageview/ImageViewTemplate.ui' # -# Created: Sun Sep 9 14:41:31 2012 -# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# Created: Mon Dec 23 10:10:52 2013 +# by: pyside-uic 0.2.14 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! @@ -150,6 +150,6 @@ class Ui_Form(object): self.normTimeRangeCheck.setText(QtGui.QApplication.translate("Form", "Time range", None, QtGui.QApplication.UnicodeUTF8)) self.normFrameCheck.setText(QtGui.QApplication.translate("Form", "Frame", None, QtGui.QApplication.UnicodeUTF8)) +from ..widgets.HistogramLUTWidget import HistogramLUTWidget from ..widgets.GraphicsView import GraphicsView from ..widgets.PlotWidget import PlotWidget -from ..widgets.HistogramLUTWidget import HistogramLUTWidget From f21e0b86a4d6c3c075fdbce6760728bb3a0b2ba3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 23 Dec 2013 10:22:53 -0500 Subject: [PATCH 047/268] fixed circular import --- pyqtgraph/GraphicsScene/GraphicsScene.py | 9 +-------- pyqtgraph/graphicsItems/GraphItem.py | 1 + 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index cce7ac4a..d61a1fa4 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -1,19 +1,11 @@ from ..Qt import QtCore, QtGui - from ..python2_3 import sortList -#try: - #from PyQt4 import QtOpenGL - #HAVE_OPENGL = True -#except ImportError: - #HAVE_OPENGL = False - import weakref from ..Point import Point from .. import functions as fn from .. import ptime as ptime from .mouseEvents import * from .. import debug as debug -from . import exportDialog if hasattr(QtCore, 'PYQT_VERSION'): try: @@ -552,6 +544,7 @@ class GraphicsScene(QtGui.QGraphicsScene): def showExportDialog(self): if self.exportDialog is None: + from . import exportDialog self.exportDialog = exportDialog.ExportDialog(self) self.exportDialog.show(self.contextMenuItem) diff --git a/pyqtgraph/graphicsItems/GraphItem.py b/pyqtgraph/graphicsItems/GraphItem.py index 63b5afbd..97759522 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -3,6 +3,7 @@ from .GraphicsObject import GraphicsObject from .ScatterPlotItem import ScatterPlotItem from ..Qt import QtGui, QtCore import numpy as np +from .. import getConfigOption __all__ = ['GraphItem'] From 9ffc172bf7c06b9366d8cc39f4822d68b62a6834 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 23 Dec 2013 15:20:56 -0500 Subject: [PATCH 048/268] Added documentation on using pyqtgraph as a subpackage. --- doc/source/how_to_use.rst | 54 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/doc/source/how_to_use.rst b/doc/source/how_to_use.rst index 0e00af59..e4ad3cdd 100644 --- a/doc/source/how_to_use.rst +++ b/doc/source/how_to_use.rst @@ -51,13 +51,13 @@ For the serious application developer, all of the functionality in pyqtgraph is #. Under "Header file", enter "pyqtgraph". #. Click "Add", then click "Promote". -See the designer documentation for more information on promoting widgets. +See the designer documentation for more information on promoting widgets. The "VideoSpeedTest" and "ScatterPlotSpeedTest" examples both demonstrate the use of .ui files that are compiled to .py modules using pyuic4 or pyside-uic. The "designerExample" example demonstrates dynamically generating python classes from .ui files (no pyuic4 / pyside-uic needed). PyQt and PySide --------------- -Pyqtgraph supports two popular python wrappers for the Qt library: PyQt and PySide. Both packages provide nearly identical +PyQtGraph supports two popular python wrappers for the Qt library: PyQt and PySide. Both packages provide nearly identical APIs and functionality, but for various reasons (discussed elsewhere) you may prefer to use one package or the other. When pyqtgraph is first imported, it automatically determines which library to use by making the fillowing checks: @@ -71,3 +71,53 @@ make sure it is imported before pyqtgraph:: import PySide ## this will force pyqtgraph to use PySide instead of PyQt4 import pyqtgraph as pg + + +Embedding PyQtGraph as a sub-package of a larger project +-------------------------------------------------------- + +When writing applications or python packages that make use of pyqtgraph, it is most common to install pyqtgraph system-wide (or within a virtualenv) and simply call `import pyqtgraph` from within your application. The main benefit to this is that pyqtgraph is configured independently of your application and thus you (or your users) are free to install newer versions of pyqtgraph without changing anything in your application. This is standard practice when developing with python. + +However, it is also often the case, especially for scientific applications, that software is written for a very specific purpose and then archived. If we want to ensure that the software will still work ten years later, then it is preferrable to tie the application to a very specific version of pyqtgraph and *avoid* importing the system-installed version of pyqtgraph, which may be much newer (and potentially incompatible). This is especially the case when the application requires site-specific modifications to the pyqtgraph package which may not be present in the main releases. + +PyQtGraph facilitates this usage through two mechanisms. First, all internal import statements in pyqtgraph are relative, which allows the package to be renamed or used as a sub-package without any naming conflicts with other versions of pyqtgraph on the system (that is, pyqtgraph never refers to itself internally as 'pyqtgraph'). Second, a git subtree repository is available at https://github.com/pyqtgraph/pyqtgraph-core.git that contains only the 'pyqtgraph/' subtree, allowing the code to be cloned directly as a subtree of the application which uses it. + +The basic approach is to clone the repository into the appropriate location in your package. When you import pyqtgraph from within your package, be sure to use the full name to avoid importing any system-installed pyqtgraph packages. For example, imagine a simple project has the following structure:: + + my_project/ + __init__.py + plotting.py + """Plotting functions used by this package""" + import pyqtgraph as pg + def my_plot_function(*data): + pg.plot(*data) + +To embed a specific version of pyqtgraph, we would clone the pyqtgraph-core repository inside the project:: + + my_project$ git clone github.com/pyqtgraph/pyqtgraph-core.git + +Then adjust the import statements accordingly:: + + my_project/ + __init__.py + pyqtgraph/ + plotting.py + """Plotting functions used by this package""" + import my_project.pyqtgraph as pg # be sure to use the local subpackage + # rather than any globally-installed + # versions. + def my_plot_function(*data): + pg.plot(*data) + +Use ``git checkout pyqtgraph-core-x.x.x`` to select a specific version of the repository, or use ``git pull`` to pull pyqtgraph updates from upstream (see the git documentation for more information). + +For projects that already use git for code control, it is also possible to include pyqtgraph as a git subtree within your own repository. The major advantage to this approach is that, in addition to being able to pull pyqtgraph updates from the upstream repository, it is also possible to commit your local pyqtgraph changes into the project repository and push those changes upstream:: + + my_project$ git remote add pyqtgraph-core https://github.com/pyqtgraph/pyqtgraph-core.git + my_project$ git fetch pyqtgraph-core + my_project$ git merge -s ours --no-commit pyqtgraph-core/develop + my_project$ mkdir pyqtgraph + my_project$ git read-tree -u --prefix=pyqtgraph/ pyqtgraph-core/develop + my_project$ git commit -m "Added pyqtgraph to project repository" + +See the ``git subtree`` documentation for more information. From f4e0f091dc4b181a39153ee954913aa437226145 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 23 Dec 2013 18:42:02 -0500 Subject: [PATCH 049/268] Fixes against relative-import reorganization --- pyqtgraph/exporters/ImageExporter.py | 4 ++-- pyqtgraph/exporters/Matplotlib.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index c8abb02b..78d93106 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -1,6 +1,6 @@ from .Exporter import Exporter from ..parametertree import Parameter -from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE\ +from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE from .. import functions as fn import numpy as np @@ -99,4 +99,4 @@ class ImageExporter(Exporter): self.png.save(fileName) ImageExporter.register() - \ No newline at end of file + diff --git a/pyqtgraph/exporters/Matplotlib.py b/pyqtgraph/exporters/Matplotlib.py index c2980b49..57c4cfdb 100644 --- a/pyqtgraph/exporters/Matplotlib.py +++ b/pyqtgraph/exporters/Matplotlib.py @@ -62,9 +62,9 @@ MatplotlibExporter.register() class MatplotlibWindow(QtGui.QMainWindow): def __init__(self): - from .. import widgets.MatplotlibWidget + from ..widgets import MatplotlibWidget QtGui.QMainWindow.__init__(self) - self.mpl = pyqtgraph.widgets.MatplotlibWidget.MatplotlibWidget() + self.mpl = MatplotlibWidget.MatplotlibWidget() self.setCentralWidget(self.mpl) self.show() From 9de301155683f6a55bbbecad23ebb04c50748c68 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Dec 2013 12:05:27 -0500 Subject: [PATCH 050/268] 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 051/268] 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 052/268] 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 053/268] 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 054/268] 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 055/268] 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 056/268] 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 057/268] 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 058/268] 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 059/268] 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 060/268] 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 061/268] 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 062/268] 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 063/268] 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 064/268] 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 065/268] 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 066/268] 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 067/268] 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 068/268] 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 069/268] 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 070/268] 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 071/268] 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 072/268] 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 baa6c4b82cbe4f4f6c277524bb0a66862af062ff Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Mon, 20 Jan 2014 22:15:14 -0500 Subject: [PATCH 073/268] Fix metaarray import in MultiPlotWidget.py example and MultiPlotItem.py --- examples/MultiPlotWidget.py | 6 +++--- pyqtgraph/graphicsItems/MultiPlotItem.py | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/MultiPlotWidget.py b/examples/MultiPlotWidget.py index 28492f64..28d4f2ee 100644 --- a/examples/MultiPlotWidget.py +++ b/examples/MultiPlotWidget.py @@ -10,11 +10,11 @@ from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg from pyqtgraph import MultiPlotWidget try: - from metaarray import * + from pyqtgraph.metaarray import * except: print("MultiPlot is only used with MetaArray for now (and you do not have the metaarray package)") exit() - + app = QtGui.QApplication([]) mw = QtGui.QMainWindow() mw.resize(800,800) @@ -22,7 +22,7 @@ pw = MultiPlotWidget() mw.setCentralWidget(pw) mw.show() -ma = MetaArray(random.random((3, 1000)), info=[{'name': 'Signal', 'cols': [{'name': 'Col1'}, {'name': 'Col2'}, {'name': 'Col3'}]}, {'name': 'Time', 'vals': linspace(0., 1., 1000)}]) +ma = MetaArray(random.random((3, 1000)), info=[{'name': 'Signal', 'cols': [{'name': 'Col1'}, {'name': 'Col2'}, {'name': 'Col3'}]}, {'name': 'Time', 'values': linspace(0., 1., 1000)}]) pw.plot(ma) ## Start Qt event loop unless running in interactive mode. diff --git a/pyqtgraph/graphicsItems/MultiPlotItem.py b/pyqtgraph/graphicsItems/MultiPlotItem.py index d20467a9..3ae1be5b 100644 --- a/pyqtgraph/graphicsItems/MultiPlotItem.py +++ b/pyqtgraph/graphicsItems/MultiPlotItem.py @@ -9,23 +9,23 @@ from numpy import ndarray from . import GraphicsLayout try: - from metaarray import * + from ..metaarray import * HAVE_METAARRAY = True except: #raise HAVE_METAARRAY = False - + __all__ = ['MultiPlotItem'] class MultiPlotItem(GraphicsLayout.GraphicsLayout): """ Automaticaly generates a grid of plots from a multi-dimensional array """ - + def plot(self, data): #self.layout.clear() self.plots = [] - + if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): if data.ndim != 2: raise Exception("MultiPlot currently only accepts 2D MetaArray.") @@ -53,12 +53,12 @@ class MultiPlotItem(GraphicsLayout.GraphicsLayout): title = info['name'] if 'units' in info: units = info['units'] - + pi.setLabel('left', text=title, units=units) - + else: raise Exception("Data type %s not (yet?) supported for MultiPlot." % type(data)) - + def close(self): for p in self.plots: p[0].close() From d81998461fbe4540daa2fd1025d61553ab7d25d7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 22 Jan 2014 14:23:10 -0500 Subject: [PATCH 074/268] 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 075/268] - 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 076/268] 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 077/268] 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 078/268] 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 From d4364ea17af93bde3dcfbd858fb5215517296388 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 25 Jan 2014 07:37:04 -0500 Subject: [PATCH 079/268] Fix MultiPlotWidget wrapping methods incorrectly. --- pyqtgraph/widgets/MultiPlotWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/MultiPlotWidget.py b/pyqtgraph/widgets/MultiPlotWidget.py index 58b71296..a5ae9b3e 100644 --- a/pyqtgraph/widgets/MultiPlotWidget.py +++ b/pyqtgraph/widgets/MultiPlotWidget.py @@ -25,7 +25,7 @@ class MultiPlotWidget(GraphicsView): m = getattr(self.mPlotItem, attr) if hasattr(m, '__call__'): return m - raise NameError(attr) + raise AttributeError(attr) def widgetGroupInterface(self): return (None, MultiPlotWidget.saveState, MultiPlotWidget.restoreState) From 5a1a663a5052317469911e7d3adcc3f7ec55c6c8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 25 Jan 2014 08:50:31 -0500 Subject: [PATCH 080/268] MultiPlotWidget now uses scroll bar when plots do not fit in widget area. --- examples/MultiPlotWidget.py | 10 ++++++- pyqtgraph/graphicsItems/MultiPlotItem.py | 33 ++++++++++-------------- pyqtgraph/widgets/MultiPlotWidget.py | 28 +++++++++++++++++--- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/examples/MultiPlotWidget.py b/examples/MultiPlotWidget.py index 28d4f2ee..5ab4b21d 100644 --- a/examples/MultiPlotWidget.py +++ b/examples/MultiPlotWidget.py @@ -22,7 +22,15 @@ pw = MultiPlotWidget() mw.setCentralWidget(pw) mw.show() -ma = MetaArray(random.random((3, 1000)), info=[{'name': 'Signal', 'cols': [{'name': 'Col1'}, {'name': 'Col2'}, {'name': 'Col3'}]}, {'name': 'Time', 'values': linspace(0., 1., 1000)}]) +data = random.normal(size=(3, 1000)) * np.array([[0.1], [1e-5], [1]]) +ma = MetaArray(data, info=[ + {'name': 'Signal', 'cols': [ + {'name': 'Col1', 'units': 'V'}, + {'name': 'Col2', 'units': 'A'}, + {'name': 'Col3'}, + ]}, + {'name': 'Time', 'values': linspace(0., 1., 1000), 'units': 's'} + ]) pw.plot(ma) ## Start Qt event loop unless running in interactive mode. diff --git a/pyqtgraph/graphicsItems/MultiPlotItem.py b/pyqtgraph/graphicsItems/MultiPlotItem.py index 3ae1be5b..be775d4a 100644 --- a/pyqtgraph/graphicsItems/MultiPlotItem.py +++ b/pyqtgraph/graphicsItems/MultiPlotItem.py @@ -7,26 +7,23 @@ Distributed under MIT/X11 license. See license.txt for more infomation. from numpy import ndarray from . import GraphicsLayout - -try: - from ..metaarray import * - HAVE_METAARRAY = True -except: - #raise - HAVE_METAARRAY = False +from ..metaarray import * __all__ = ['MultiPlotItem'] class MultiPlotItem(GraphicsLayout.GraphicsLayout): """ - Automaticaly generates a grid of plots from a multi-dimensional array + Automatically generates a grid of plots from a multi-dimensional array """ + def __init__(self, *args, **kwds): + GraphicsLayout.GraphicsLayout.__init__(self, *args, **kwds) + self.plots = [] + def plot(self, data): #self.layout.clear() - self.plots = [] - if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): + if hasattr(data, 'implements') and data.implements('MetaArray'): if data.ndim != 2: raise Exception("MultiPlot currently only accepts 2D MetaArray.") ic = data.infoCopy() @@ -44,18 +41,14 @@ class MultiPlotItem(GraphicsLayout.GraphicsLayout): pi.plot(data[tuple(sl)]) #self.layout.addItem(pi, i, 0) self.plots.append((pi, i, 0)) - title = None - units = None info = ic[ax]['cols'][i] - if 'title' in info: - title = info['title'] - elif 'name' in info: - title = info['name'] - if 'units' in info: - units = info['units'] - + title = info.get('title', info.get('name', None)) + units = info.get('units', None) pi.setLabel('left', text=title, units=units) - + info = ic[1-ax] + title = info.get('title', info.get('name', None)) + units = info.get('units', None) + pi.setLabel('bottom', text=title, units=units) else: raise Exception("Data type %s not (yet?) supported for MultiPlot." % type(data)) diff --git a/pyqtgraph/widgets/MultiPlotWidget.py b/pyqtgraph/widgets/MultiPlotWidget.py index a5ae9b3e..15c65d03 100644 --- a/pyqtgraph/widgets/MultiPlotWidget.py +++ b/pyqtgraph/widgets/MultiPlotWidget.py @@ -4,21 +4,24 @@ MultiPlotWidget.py - Convenience class--GraphicsView widget displaying a MultiP Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ - +from ..Qt import QtCore from .GraphicsView import GraphicsView from ..graphicsItems import MultiPlotItem as MultiPlotItem __all__ = ['MultiPlotWidget'] class MultiPlotWidget(GraphicsView): - """Widget implementing a graphicsView with a single PlotItem inside.""" + """Widget implementing a graphicsView with a single MultiPlotItem inside.""" def __init__(self, parent=None): + self.minPlotHeight = 150 + self.mPlotItem = MultiPlotItem.MultiPlotItem() GraphicsView.__init__(self, parent) self.enableMouse(False) - self.mPlotItem = MultiPlotItem.MultiPlotItem() self.setCentralItem(self.mPlotItem) ## Explicitly wrap methods from mPlotItem #for m in ['setData']: #setattr(self, m, getattr(self.mPlotItem, m)) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) def __getattr__(self, attr): ## implicitly wrap methods from plotItem if hasattr(self.mPlotItem, attr): @@ -26,6 +29,7 @@ class MultiPlotWidget(GraphicsView): if hasattr(m, '__call__'): return m raise AttributeError(attr) + def widgetGroupInterface(self): return (None, MultiPlotWidget.saveState, MultiPlotWidget.restoreState) @@ -43,3 +47,21 @@ class MultiPlotWidget(GraphicsView): self.mPlotItem = None self.setParent(None) GraphicsView.close(self) + + def setRange(self, *args, **kwds): + GraphicsView.setRange(self, *args, **kwds) + if self.centralWidget is not None: + r = self.range + minHeight = len(self.mPlotItem.plots) * self.minPlotHeight + if r.height() < minHeight: + r.setHeight(minHeight) + r.setWidth(r.width() - 25) + self.centralWidget.setGeometry(r) + + def resizeEvent(self, ev): + if self.closed: + return + if self.autoPixelRange: + self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height()) + MultiPlotWidget.setRange(self, self.range, padding=0, disableAutoPixel=False) ## we do this because some subclasses like to redefine setRange in an incompatible way. + self.updateMatrix() From d86beb5a5a4cdccf18641220934f6e71f44ba75e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 25 Jan 2014 08:58:54 -0500 Subject: [PATCH 081/268] Added setMinimumPlotHeight method, set default min height to 50px. --- pyqtgraph/widgets/MultiPlotWidget.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/widgets/MultiPlotWidget.py b/pyqtgraph/widgets/MultiPlotWidget.py index 15c65d03..abad55ef 100644 --- a/pyqtgraph/widgets/MultiPlotWidget.py +++ b/pyqtgraph/widgets/MultiPlotWidget.py @@ -12,7 +12,7 @@ __all__ = ['MultiPlotWidget'] class MultiPlotWidget(GraphicsView): """Widget implementing a graphicsView with a single MultiPlotItem inside.""" def __init__(self, parent=None): - self.minPlotHeight = 150 + self.minPlotHeight = 50 self.mPlotItem = MultiPlotItem.MultiPlotItem() GraphicsView.__init__(self, parent) self.enableMouse(False) @@ -29,7 +29,16 @@ class MultiPlotWidget(GraphicsView): if hasattr(m, '__call__'): return m raise AttributeError(attr) - + + def setMinimumPlotHeight(self, min): + """Set the minimum height for each sub-plot displayed. + + If the total height of all plots is greater than the height of the + widget, then a scroll bar will appear to provide access to the entire + set of plots. + """ + self.minPlotHeight = min + self.resizeEvent(None) def widgetGroupInterface(self): return (None, MultiPlotWidget.saveState, MultiPlotWidget.restoreState) @@ -55,7 +64,7 @@ class MultiPlotWidget(GraphicsView): minHeight = len(self.mPlotItem.plots) * self.minPlotHeight if r.height() < minHeight: r.setHeight(minHeight) - r.setWidth(r.width() - 25) + r.setWidth(r.width() - self.verticalScrollBar().width()) self.centralWidget.setGeometry(r) def resizeEvent(self, ev): From 797a8c0f08362d9d0c18a9d72a9ae3503bebacb3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 8 Dec 2013 09:38:43 -0500 Subject: [PATCH 082/268] Implementing user-defined limits for ViewBox --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 82 +++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 70012ec4..845efe43 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -118,6 +118,15 @@ class ViewBox(GraphicsWidget): 'wheelScaleFactor': -1.0 / 8.0, 'background': None, + + # Limits + 'limits': { + 'xRange': [None, None], # Maximum and minimum visible X values + 'yRange': [None, None], # Maximum and minimum visible Y values + 'minRange': [None, None], # Minimum allowed range for both axes + 'maxRange': [None, None], # Maximum allowed range for both axes + } + } self._updatingRange = False ## Used to break recursive loops. See updateAutoRange. self._itemBoundsCache = weakref.WeakKeyDictionary() @@ -571,6 +580,40 @@ class ViewBox(GraphicsWidget): else: padding = 0.02 return padding + + def setLimits(self, **kwds): + """ + Set limits that constrain the possible view ranges. + + =========== ============================================================ + Arguments + xRange (min, max) limits for x-axis range + yRange (min, max) limits for y-axis range + minRange (x, y) minimum allowed span + maxRange (x, y) maximum allowed span + xMin Minimum allowed x-axis range + xMax Maximum allowed x-axis range + yMin Minimum allowed y-axis range + yMax Maximum allowed y-axis range + =========== ============================================================ + """ + for kwd in ['xRange', 'yRange', 'minRange', 'maxRange']: + if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: + self.state['limits'][kwd] = kwds[kwd] + update = True + for axis in [0,1]: + for mnmx in [0,1]: + kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx] + lname = ['xRange', 'yRange'][axis] + if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]: + self.state['limits'][lname][mnmx] = kwds[kwd] + update = True + + if update: + self.updateViewRange() + + + def scaleBy(self, s=None, center=None, x=None, y=None): """ @@ -1351,7 +1394,6 @@ class ViewBox(GraphicsWidget): # then make the entire target range visible ax = 0 if targetRatio > viewRatio else 1 - #### these should affect viewRange, not targetRange! if ax == 0: ## view range needs to be taller than target dy = 0.5 * (tr.width() / viewRatio - tr.height()) @@ -1364,6 +1406,44 @@ class ViewBox(GraphicsWidget): if dx != 0: changed[0] = True viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] + + # check for any requested limits + rng = (self.state['limits']['xRange'], self.state['limits']['yRange']) + minRng = self.state['limits']['minRange'] + maxRng = self.state['limits']['maxRange'] + + for axis in [0, 1]: + # max range cannot be larger than bounds, if they are given + if rng[axis][0] is not None and rng[axis][1] is not None: + maxRng[axis] = min(maxRng[axis], rng[axis][1]-rng[axis][0]) + + diff = viewRange[axis][1] - viewRange[axis][0] + print axis, diff, maxRng[axis] + if maxRng[axis] is not None and diff > maxRng[axis]: + delta = maxRng[axis] - diff + changed[axis] = True + elif minRng[axis] is not None and diff < minRng[axis]: + delta = minRng[axis] - diff + changed[axis] = True + else: + delta = 0 + + viewRange[axis][0] -= diff/2. + viewRange[axis][1] += diff/2. + print viewRange + + mn, mx = rng[axis] + if mn is not None and viewRange[axis][0] < mn: + delta = mn - viewRange[axis][0] + viewRange[axis][0] += delta + viewRange[axis][1] += delta + elif mx is not None and viewRange[axis][1] > mx: + delta = mx - viewRange[axis][1] + viewRange[axis][0] += delta + viewRange[axis][1] += delta + + + changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange From b0cafce3b4129a72125be13d12fef5387d2d3949 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 30 Jan 2014 10:50:07 -0500 Subject: [PATCH 083/268] Basic view limits appear to be working. --- examples/SimplePlot.py | 6 +++-- examples/ViewLimits.py | 15 ++++++++++++ pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 28 +++++++++++++++------- 3 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 examples/ViewLimits.py diff --git a/examples/SimplePlot.py b/examples/SimplePlot.py index f572743a..cacdbbdc 100644 --- a/examples/SimplePlot.py +++ b/examples/SimplePlot.py @@ -2,13 +2,15 @@ import initExample ## Add path to library (just for examples; you do not need th from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg +import pyqtgraph.exporters import numpy as np plt = pg.plot(np.random.normal(size=100), title="Simplest possible plotting example") plt.getAxis('bottom').setTicks([[(x*20, str(x*20)) for x in range(6)]]) -## Start Qt event loop unless running in interactive mode or using pyside. -ex = pg.exporters.SVGExporter.SVGExporter(plt.plotItem.scene()) + +ex = pg.exporters.SVGExporter(plt.plotItem.scene()) ex.export('/home/luke/tmp/test.svg') +## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': import sys if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'): diff --git a/examples/ViewLimits.py b/examples/ViewLimits.py new file mode 100644 index 00000000..c08bf77c --- /dev/null +++ b/examples/ViewLimits.py @@ -0,0 +1,15 @@ +import initExample ## Add path to library (just for examples; you do not need this) + +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +import numpy as np + +plt = pg.plot(np.random.normal(size=100), title="View limit example") +plt.centralWidget.vb.setLimits(xRange=[-100, 100], minRange=[0.1, None], maxRange=[50, None]) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'): + pg.QtGui.QApplication.exec_() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 845efe43..d4467286 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -597,6 +597,8 @@ class ViewBox(GraphicsWidget): yMax Maximum allowed y-axis range =========== ============================================================ """ + update = False + for kwd in ['xRange', 'yRange', 'minRange', 'maxRange']: if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: self.state['limits'][kwd] = kwds[kwd] @@ -1409,16 +1411,24 @@ class ViewBox(GraphicsWidget): # check for any requested limits rng = (self.state['limits']['xRange'], self.state['limits']['yRange']) - minRng = self.state['limits']['minRange'] - maxRng = self.state['limits']['maxRange'] + minRng = self.state['limits']['minRange'][:] + maxRng = self.state['limits']['maxRange'][:] for axis in [0, 1]: + if rng[axis][0] is None and rng[axis][1] is None and minRng[axis] is None and maxRng[axis] is None: + continue + # max range cannot be larger than bounds, if they are given if rng[axis][0] is not None and rng[axis][1] is not None: - maxRng[axis] = min(maxRng[axis], rng[axis][1]-rng[axis][0]) + if maxRng[axis] is not None: + maxRng[axis] = min(maxRng[axis], rng[axis][1]-rng[axis][0]) + else: + maxRng[axis] = rng[axis][1]-rng[axis][0] + + #print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, rng[axis], minRng[axis], maxRng[axis]) + #print "Starting range:", viewRange[axis] diff = viewRange[axis][1] - viewRange[axis][0] - print axis, diff, maxRng[axis] if maxRng[axis] is not None and diff > maxRng[axis]: delta = maxRng[axis] - diff changed[axis] = True @@ -1428,9 +1438,10 @@ class ViewBox(GraphicsWidget): else: delta = 0 - viewRange[axis][0] -= diff/2. - viewRange[axis][1] += diff/2. - print viewRange + viewRange[axis][0] -= delta/2. + viewRange[axis][1] += delta/2. + + #print "after applying min/max:", viewRange[axis] mn, mx = rng[axis] if mn is not None and viewRange[axis][0] < mn: @@ -1442,8 +1453,7 @@ class ViewBox(GraphicsWidget): viewRange[axis][0] += delta viewRange[axis][1] += delta - - + #print "after applying edge limits:", viewRange[axis] changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange From d0ed3ba2457d5cb10cc80befbf1cd0734b89c441 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 31 Jan 2014 13:04:47 -0500 Subject: [PATCH 084/268] Removed duplicate limit-setting arguments Renamed args for clarity, improved documentation Fixed interaction bugs - zooming works correctly when view is against limit - no more phantom target range; target is reset during mouse interaction. --- examples/ViewLimits.py | 2 +- pyqtgraph/graphicsItems/GraphicsLayout.py | 9 +++ pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 86 ++++++++++++++-------- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/examples/ViewLimits.py b/examples/ViewLimits.py index c08bf77c..c8f0dd21 100644 --- a/examples/ViewLimits.py +++ b/examples/ViewLimits.py @@ -5,7 +5,7 @@ import pyqtgraph as pg import numpy as np plt = pg.plot(np.random.normal(size=100), title="View limit example") -plt.centralWidget.vb.setLimits(xRange=[-100, 100], minRange=[0.1, None], maxRange=[50, None]) +plt.centralWidget.vb.setLimits(xMin=-20, xMax=120, minXRange=5, maxXRange=100) ## Start Qt event loop unless running in interactive mode or using pyside. diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index a4016522..b8325736 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -31,6 +31,15 @@ class GraphicsLayout(GraphicsWidget): #ret = GraphicsWidget.resizeEvent(self, ev) #print self.pos(), self.mapToDevice(self.rect().topLeft()) #return ret + + def setBorder(self, *args, **kwds): + """ + Set the pen used to draw border between cells. + + See :func:`mkPen ` for arguments. + """ + self.border = fn.mkPen(*args, **kwds) + self.update() def nextRow(self): """Advance to next row for automatic item placement""" diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index d4467286..0fe6cd53 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -121,10 +121,10 @@ class ViewBox(GraphicsWidget): # Limits 'limits': { - 'xRange': [None, None], # Maximum and minimum visible X values - 'yRange': [None, None], # Maximum and minimum visible Y values - 'minRange': [None, None], # Minimum allowed range for both axes - 'maxRange': [None, None], # Maximum allowed range for both axes + 'xLimits': [None, None], # Maximum and minimum visible X values + 'yLimits': [None, None], # Maximum and minimum visible Y values + 'xRange': [None, None], # Maximum and minimum X range + 'yRange': [None, None], # Maximum and minimum Y range } } @@ -407,6 +407,13 @@ class ViewBox(GraphicsWidget): print("make qrectf failed:", self.state['targetRange']) raise + def _resetTarget(self): + # Reset target range to exactly match current view range. + # This is used during mouse interaction to prevent unpredictable + # behavior (because the user is unaware of targetRange). + if self.state['aspectLocked'] is False: # (interferes with aspect locking) + self.state['targetRange'] = [self.state['viewRange'][0][:], self.state['viewRange'][1][:]] + def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True): """ Set the visible range of the ViewBox. @@ -585,27 +592,38 @@ class ViewBox(GraphicsWidget): """ Set limits that constrain the possible view ranges. + **Panning limits**. The following arguments define the region within the + viewbox coordinate system that may be accessed by panning the view. =========== ============================================================ - Arguments - xRange (min, max) limits for x-axis range - yRange (min, max) limits for y-axis range - minRange (x, y) minimum allowed span - maxRange (x, y) maximum allowed span - xMin Minimum allowed x-axis range - xMax Maximum allowed x-axis range - yMin Minimum allowed y-axis range - yMax Maximum allowed y-axis range + xMin Minimum allowed x-axis value + xMax Maximum allowed x-axis value + yMin Minimum allowed y-axis value + yMax Maximum allowed y-axis value + =========== ============================================================ + + **Scaling limits**. These arguments prevent the view being zoomed in or + out too far. =========== ============================================================ + minXRange Minimum allowed left-to-right span across the view. + maxXRange Maximum allowed left-to-right span across the view. + minYRange Minimum allowed top-to-bottom span across the view. + maxYRange Maximum allowed top-to-bottom span across the view. + =========== ============================================================ """ update = False - for kwd in ['xRange', 'yRange', 'minRange', 'maxRange']: - if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: - self.state['limits'][kwd] = kwds[kwd] - update = True + #for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']: + #if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: + #self.state['limits'][kwd] = kwds[kwd] + #update = True for axis in [0,1]: for mnmx in [0,1]: kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx] + lname = ['xLimits', 'yLimits'][axis] + if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]: + self.state['limits'][lname][mnmx] = kwds[kwd] + update = True + kwd = [['minXRange', 'maxXRange'], ['minYRange', 'maxYRange']][axis][mnmx] lname = ['xRange', 'yRange'][axis] if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]: self.state['limits'][lname][mnmx] = kwds[kwd] @@ -1101,6 +1119,7 @@ class ViewBox(GraphicsWidget): center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) #center = ev.pos() + self._resetTarget() self.scaleBy(s, center) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) ev.accept() @@ -1158,6 +1177,7 @@ class ViewBox(GraphicsWidget): x = tr.x() if mask[0] == 1 else None y = tr.y() if mask[1] == 1 else None + self._resetTarget() self.translateBy(x=x, y=y) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) elif ev.button() & QtCore.Qt.RightButton: @@ -1177,6 +1197,7 @@ class ViewBox(GraphicsWidget): y = s[1] if mouseEnabled[1] == 1 else None center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton))) + self._resetTarget() self.scaleBy(x=x, y=y, center=center) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) @@ -1372,9 +1393,9 @@ class ViewBox(GraphicsWidget): viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] changed = [False, False] - # Make correction for aspect ratio constraint + #-------- Make correction for aspect ratio constraint ---------- - ## aspect is (widget w/h) / (view range w/h) + # aspect is (widget w/h) / (view range w/h) aspect = self.state['aspectLocked'] # size ratio / view ratio tr = self.targetRect() bounds = self.rect() @@ -1409,25 +1430,27 @@ class ViewBox(GraphicsWidget): changed[0] = True viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] - # check for any requested limits - rng = (self.state['limits']['xRange'], self.state['limits']['yRange']) - minRng = self.state['limits']['minRange'][:] - maxRng = self.state['limits']['maxRange'][:] + # ----------- Make corrections for view limits ----------- + + limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits']) + minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]] + maxRng = [self.state['limits']['xRange'][1], self.state['limits']['yRange'][1]] for axis in [0, 1]: - if rng[axis][0] is None and rng[axis][1] is None and minRng[axis] is None and maxRng[axis] is None: + if limits[axis][0] is None and limits[axis][1] is None and minRng[axis] is None and maxRng[axis] is None: continue # max range cannot be larger than bounds, if they are given - if rng[axis][0] is not None and rng[axis][1] is not None: + if limits[axis][0] is not None and limits[axis][1] is not None: if maxRng[axis] is not None: - maxRng[axis] = min(maxRng[axis], rng[axis][1]-rng[axis][0]) + maxRng[axis] = min(maxRng[axis], limits[axis][1]-limits[axis][0]) else: - maxRng[axis] = rng[axis][1]-rng[axis][0] + maxRng[axis] = limits[axis][1]-limits[axis][0] - #print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, rng[axis], minRng[axis], maxRng[axis]) + #print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, limits[axis], minRng[axis], maxRng[axis]) #print "Starting range:", viewRange[axis] + # Apply xRange, yRange diff = viewRange[axis][1] - viewRange[axis][0] if maxRng[axis] is not None and diff > maxRng[axis]: delta = maxRng[axis] - diff @@ -1443,19 +1466,22 @@ class ViewBox(GraphicsWidget): #print "after applying min/max:", viewRange[axis] - mn, mx = rng[axis] + # Apply xLimits, yLimits + mn, mx = limits[axis] if mn is not None and viewRange[axis][0] < mn: delta = mn - viewRange[axis][0] viewRange[axis][0] += delta viewRange[axis][1] += delta + changed[axis] = True elif mx is not None and viewRange[axis][1] > mx: delta = mx - viewRange[axis][1] viewRange[axis][0] += delta viewRange[axis][1] += delta + changed[axis] = True #print "after applying edge limits:", viewRange[axis] - changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] + changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange # emit range change signals From 355b38dcc11e454b87ed5c54daa3ea8ac98332c2 Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Fri, 31 Jan 2014 21:26:01 -0500 Subject: [PATCH 085/268] Typo --- pyqtgraph/graphicsItems/AxisItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 429ff49c..2a5380ec 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -692,7 +692,7 @@ class AxisItem(GraphicsWidget): ## determine mapping between tick values and local coordinates dif = self.range[1] - self.range[0] if dif == 0: - xscale = 1 + xScale = 1 offset = 0 else: if axis == 0: From 13aa00d915bab1b024cbcce201d0a4bc6ac3049d Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Fri, 31 Jan 2014 22:10:17 -0500 Subject: [PATCH 086/268] Check that textRects is not empty, otherwise np.max raises ValueError. --- pyqtgraph/graphicsItems/AxisItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 2a5380ec..a8509db3 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -810,10 +810,10 @@ class AxisItem(GraphicsWidget): ## measure all text, make sure there's enough room if axis == 0: textSize = np.sum([r.height() for r in textRects]) - textSize2 = np.max([r.width() for r in textRects]) + textSize2 = np.max([r.width() for r in textRects]) if textRects else 0 else: textSize = np.sum([r.width() for r in textRects]) - textSize2 = np.max([r.height() for r in textRects]) + textSize2 = np.max([r.height() for r in textRects]) if textRects else 0 ## If the strings are too crowded, stop drawing text now. ## We use three different crowding limits based on the number From fe11e6c1439bc21ae367afab154a388af7aee07e Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Fri, 31 Jan 2014 22:29:20 -0500 Subject: [PATCH 087/268] use examples directory for the output --- examples/SimplePlot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/SimplePlot.py b/examples/SimplePlot.py index f572743a..b4dba1ff 100644 --- a/examples/SimplePlot.py +++ b/examples/SimplePlot.py @@ -1,5 +1,6 @@ import initExample ## Add path to library (just for examples; you do not need this) +from os.path import * from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import numpy as np @@ -7,7 +8,7 @@ plt = pg.plot(np.random.normal(size=100), title="Simplest possible plotting exam plt.getAxis('bottom').setTicks([[(x*20, str(x*20)) for x in range(6)]]) ## Start Qt event loop unless running in interactive mode or using pyside. ex = pg.exporters.SVGExporter.SVGExporter(plt.plotItem.scene()) -ex.export('/home/luke/tmp/test.svg') +ex.export(join(dirname(__file__), 'test.svg')) if __name__ == '__main__': import sys From 95bddca0147ffea2428fb4de6d88e6a8c6c5ddb9 Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Fri, 31 Jan 2014 23:00:18 -0500 Subject: [PATCH 088/268] In ArrowItem allow individual parameter change through setStyle call. --- examples/Arrow.py | 3 ++- pyqtgraph/graphicsItems/ArrowItem.py | 9 ++++++--- pyqtgraph/graphicsItems/CurvePoint.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/Arrow.py b/examples/Arrow.py index 2cbff113..d5ea2a74 100644 --- a/examples/Arrow.py +++ b/examples/Arrow.py @@ -2,7 +2,7 @@ """ Display an animated arrowhead following a curve. This example uses the CurveArrow class, which is a combination -of ArrowItem and CurvePoint. +of ArrowItem and CurvePoint. To place a static arrow anywhere in a scene, use ArrowItem. To attach other types of item to a curve, use CurvePoint. @@ -45,6 +45,7 @@ p.setRange(QtCore.QRectF(-20, -10, 60, 20)) ## Animated arrow following curve c = p2.plot(x=np.sin(np.linspace(0, 2*np.pi, 1000)), y=np.cos(np.linspace(0, 6*np.pi, 1000))) a = pg.CurveArrow(c) +a.setStyle(headLen=40) p2.addItem(a) anim = a.makeAnimation(loop=-1) anim.start() diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index dcede02a..258f8b02 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -70,13 +70,16 @@ class ArrowItem(QtGui.QGraphicsPathItem): brush The brush used to fill the arrow. ================= ================================================= """ - self.opts = opts + try: + self.opts.update(opts) + except AttributeError: + self.opts = opts opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) self.path = fn.makeArrowPath(**opt) self.setPath(self.path) - if opts['pxMode']: + if self.opts['pxMode']: self.setFlags(self.flags() | self.ItemIgnoresTransformations) else: self.setFlags(self.flags() & ~self.ItemIgnoresTransformations) @@ -121,4 +124,4 @@ class ArrowItem(QtGui.QGraphicsPathItem): return pad - \ No newline at end of file + diff --git a/pyqtgraph/graphicsItems/CurvePoint.py b/pyqtgraph/graphicsItems/CurvePoint.py index 668830f7..d6fd2a08 100644 --- a/pyqtgraph/graphicsItems/CurvePoint.py +++ b/pyqtgraph/graphicsItems/CurvePoint.py @@ -112,6 +112,6 @@ class CurveArrow(CurvePoint): self.arrow = ArrowItem.ArrowItem(**opts) self.arrow.setParentItem(self) - def setStyle(**opts): + def setStyle(self, **opts): return self.arrow.setStyle(**opts) From ff232f4e3af7a96d39ceac0938fde957482a4994 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 1 Feb 2014 20:46:05 -0500 Subject: [PATCH 089/268] Added cylinder geometry to opengl MeshData --- examples/GLMeshItem.py | 89 ++++++++++++++++++++++-------------- pyqtgraph/opengl/MeshData.py | 36 ++++++++++++++- 2 files changed, 88 insertions(+), 37 deletions(-) diff --git a/examples/GLMeshItem.py b/examples/GLMeshItem.py index 5ef8eb51..f017f19b 100644 --- a/examples/GLMeshItem.py +++ b/examples/GLMeshItem.py @@ -67,7 +67,7 @@ w.addItem(m2) ## Example 3: -## icosahedron +## sphere md = gl.MeshData.sphere(rows=10, cols=20) #colors = np.random.random(size=(md.faceCount(), 4)) @@ -79,7 +79,7 @@ colors[:,1] = np.linspace(0, 1, colors.shape[0]) md.setFaceColors(colors) m3 = gl.GLMeshItem(meshdata=md, smooth=False)#, shader='balloon') -#m3.translate(-5, -5, 0) +m3.translate(-5, -5, 0) w.addItem(m3) @@ -91,49 +91,68 @@ m4 = gl.GLMeshItem(meshdata=md, smooth=False, drawFaces=False, drawEdges=True, e m4.translate(0,10,0) w.addItem(m4) +# Example 5: +# cylinder +md = gl.MeshData.cylinder(rows=10, cols=20, radius=[1., 2.0], length=5.) +md2 = gl.MeshData.cylinder(rows=10, cols=20, radius=[2., 0.5], length=10.) +colors = np.ones((md.faceCount(), 4), dtype=float) +colors[::2,0] = 0 +colors[:,1] = np.linspace(0, 1, colors.shape[0]) +md.setFaceColors(colors) +m5 = gl.GLMeshItem(meshdata=md, smooth=True, drawEdges=True, edgeColor=(1,0,0,1), shader='balloon') +colors = np.ones((md.faceCount(), 4), dtype=float) +colors[::2,0] = 0 +colors[:,1] = np.linspace(0, 1, colors.shape[0]) +md2.setFaceColors(colors) +m6 = gl.GLMeshItem(meshdata=md2, smooth=True, drawEdges=False, shader='balloon') +m6.translate(0,0,7.5) + +m6.rotate(0., 0, 1, 1) +#m5.translate(-3,3,0) +w.addItem(m5) +w.addItem(m6) - -#def psi(i, j, k, offset=(25, 25, 50)): - #x = i-offset[0] - #y = j-offset[1] - #z = k-offset[2] - #th = np.arctan2(z, (x**2+y**2)**0.5) - #phi = np.arctan2(y, x) - #r = (x**2 + y**2 + z **2)**0.5 - #a0 = 1 - ##ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th) - #ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1) +def psi(i, j, k, offset=(25, 25, 50)): + x = i-offset[0] + y = j-offset[1] + z = k-offset[2] + th = np.arctan2(z, (x**2+y**2)**0.5) + phi = np.arctan2(y, x) + r = (x**2 + y**2 + z **2)**0.5 + a0 = 1 + #ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th) + ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1) - #return ps + return ps - ##return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2 + #return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2 -#print("Generating scalar field..") -#data = np.abs(np.fromfunction(psi, (50,50,100))) +print("Generating scalar field..") +data = np.abs(np.fromfunction(psi, (50,50,100))) -##data = np.fromfunction(lambda i,j,k: np.sin(0.2*((i-25)**2+(j-15)**2+k**2)**0.5), (50,50,50)); -#print("Generating isosurface..") -#verts = pg.isosurface(data, data.max()/4.) - -#md = gl.MeshData.MeshData(vertexes=verts) - -#colors = np.ones((md.vertexes(indexed='faces').shape[0], 4), dtype=float) -#colors[:,3] = 0.3 -#colors[:,2] = np.linspace(0, 1, colors.shape[0]) -#m1 = gl.GLMeshItem(meshdata=md, color=colors, smooth=False) - -#w.addItem(m1) -#m1.translate(-25, -25, -20) - -#m2 = gl.GLMeshItem(vertexes=verts, color=colors, smooth=True) - -#w.addItem(m2) -#m2.translate(-25, -25, -50) +#data = np.fromfunction(lambda i,j,k: np.sin(0.2*((i-25)**2+(j-15)**2+k**2)**0.5), (50,50,50)); +# print("Generating isosurface..") +# verts = pg.isosurface(data, data.max()/4.) +# print dir(gl.MeshData) +# md = gl.GLMeshItem(vertexes=verts) +# +# colors = np.ones((md.vertexes(indexed='faces').shape[0], 4), dtype=float) +# colors[:,3] = 0.3 +# colors[:,2] = np.linspace(0, 1, colors.shape[0]) +# m1 = gl.GLMeshItem(meshdata=md, color=colors, smooth=False) +# +# w.addItem(m1) +# m1.translate(-25, -25, -20) +# +# m2 = gl.GLMeshItem(vertexes=verts, color=colors, smooth=True) +# +# w.addItem(m2) +# m2.translate(-25, -25, -50) diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index 3046459d..74bfea7d 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -1,5 +1,5 @@ -from ..Qt import QtGui -from .. import functions as fn +from pyqtgraph.Qt import QtGui +import pyqtgraph.functions as fn import numpy as np class MeshData(object): @@ -516,4 +516,36 @@ class MeshData(object): return MeshData(vertexes=verts, faces=faces) + @staticmethod + def cylinder(rows, cols, radius=[1.0, 1.0], length=1.0, offset=False, ends=False): + """ + Return a MeshData instance with vertexes and faces computed + for a cylindrical surface. + The cylinder may be tapered with different radii at each end (truncated cone) + ends are open if ends = False + No closed ends implemented yet... + The easiest way may be to add a vertex at the top and bottom in the center of the face? + """ + verts = np.empty((rows+1, cols, 3), dtype=float) + if isinstance(radius, int): + radius = [radius, radius] # convert to list + ## compute vertexes + th = ((np.arange(cols) * 2 * np.pi / cols).reshape(1, cols)) # angle around + r = (np.linspace(radius[0],radius[1],num=rows+1, endpoint=True)).reshape(rows+1, 1) # radius as a function of z + verts[...,2] = np.linspace(-length/2.0, length/2.0, num=rows+1, endpoint=True).reshape(rows+1, 1) # z + if offset: + th = th + ((np.pi / cols) * np.arange(rows+1).reshape(rows+1,1)) ## rotate each row by 1/2 column + verts[...,0] = r * np.cos(th) # x = r cos(th) + verts[...,1] = r * np.sin(th) # y = r sin(th) + verts = verts.reshape((rows+1)*cols, 3) # just reshape: no redundant vertices... + ## compute faces + faces = np.empty((rows*cols*2, 3), dtype=np.uint) + rowtemplate1 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 0]])) % cols) + np.array([[0, 0, cols]]) + rowtemplate2 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 1]])) % cols) + np.array([[cols, 0, cols]]) + for row in range(rows): + start = row * cols * 2 + faces[start:start+cols] = rowtemplate1 + row * cols + faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols + + return MeshData(vertexes=verts, faces=faces) \ No newline at end of file From 92d3b3fb94c09755c737979fedd4eb18e0707901 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 2 Feb 2014 10:45:32 -0500 Subject: [PATCH 090/268] cleanups --- examples/SimplePlot.py | 8 +------- pyqtgraph/graphicsItems/ArrowItem.py | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/examples/SimplePlot.py b/examples/SimplePlot.py index b4dba1ff..92106661 100644 --- a/examples/SimplePlot.py +++ b/examples/SimplePlot.py @@ -1,16 +1,10 @@ import initExample ## Add path to library (just for examples; you do not need this) -from os.path import * -from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import numpy as np plt = pg.plot(np.random.normal(size=100), title="Simplest possible plotting example") -plt.getAxis('bottom').setTicks([[(x*20, str(x*20)) for x in range(6)]]) -## Start Qt event loop unless running in interactive mode or using pyside. -ex = pg.exporters.SVGExporter.SVGExporter(plt.plotItem.scene()) -ex.export(join(dirname(__file__), 'test.svg')) if __name__ == '__main__': import sys - if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'): + if sys.flags.interactive != 1 or not hasattr(pg.QtCore, 'PYQT_VERSION'): pg.QtGui.QApplication.exec_() diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index 275f9250..c98ba127 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -16,12 +16,14 @@ class ArrowItem(QtGui.QGraphicsPathItem): Arrows can be initialized with any keyword arguments accepted by the setStyle() method. """ + self.opts = {} QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None)) + if 'size' in opts: opts['headLen'] = opts['size'] if 'width' in opts: opts['headWidth'] = opts['width'] - defOpts = { + defaultOpts = { 'pxMode': True, 'angle': -150, ## If the angle is 0, the arrow points left 'pos': (0,0), @@ -33,12 +35,9 @@ class ArrowItem(QtGui.QGraphicsPathItem): 'pen': (200,200,200), 'brush': (50,50,200), } - defOpts.update(opts) + defaultOpts.update(opts) - self.setStyle(**defOpts) - - self.setPen(fn.mkPen(defOpts['pen'])) - self.setBrush(fn.mkBrush(defOpts['brush'])) + self.setStyle(**defaultOpts) self.rotate(self.opts['angle']) self.moveBy(*self.opts['pos']) @@ -60,9 +59,9 @@ class ArrowItem(QtGui.QGraphicsPathItem): specified, ot overrides headWidth. default=25 baseAngle Angle of the base of the arrow head. Default is 0, which means that the base of the arrow head - is perpendicular to the arrow shaft. + is perpendicular to the arrow tail. tailLen Length of the arrow tail, measured from the base - of the arrow head to the tip of the tail. If + of the arrow head to the end of the tail. If this value is None, no tail will be drawn. default=None tailWidth Width of the tail. default=3 @@ -70,15 +69,15 @@ class ArrowItem(QtGui.QGraphicsPathItem): brush The brush used to fill the arrow. ================= ================================================= """ - try: - self.opts.update(opts) - except AttributeError: - self.opts = opts + self.opts.update(opts) opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) self.path = fn.makeArrowPath(**opt) self.setPath(self.path) + self.setPen(fn.mkPen(self.opts['pen'])) + self.setBrush(fn.mkBrush(self.opts['brush'])) + if self.opts['pxMode']: self.setFlags(self.flags() | self.ItemIgnoresTransformations) else: From 67685d80bc24c607745ae14e59068e04aa6f2529 Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Mon, 3 Feb 2014 20:33:55 +0100 Subject: [PATCH 091/268] No rendering of "Line style keyword arguments:" list because of missing blank line and mysterious unexpected identation error --- pyqtgraph/graphicsItems/PlotDataItem.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 8baab719..25a6433e 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -56,10 +56,11 @@ class PlotDataItem(GraphicsObject): =========================== ========================================= **Line style keyword arguments:** + ========== ================================================ - connect Specifies how / whether vertexes should be connected. - See :func:`arrayToQPath() ` - pen Pen to use for drawing line between points. + connect Specifies how / whether vertexes should be connected. See + :func:`arrayToQPath() ` + pen Pen to use for drawing line between points. Default is solid grey, 1px width. Use None to disable line drawing. May be any single argument accepted by :func:`mkPen() ` shadowPen Pen for secondary line to draw behind the primary line. disabled by default. From 92d7bbe18e1e5e7b03a785e002bf28c809f3b16b Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Mon, 3 Feb 2014 21:13:10 +0100 Subject: [PATCH 092/268] In list tables "**Arguments**", "Arguments:" changed to **Arguments:** --- pyqtgraph/canvas/CanvasItem.py | 2 +- pyqtgraph/colormap.py | 4 +-- pyqtgraph/console/Console.py | 2 +- pyqtgraph/debug.py | 2 +- pyqtgraph/dockarea/DockArea.py | 2 +- pyqtgraph/flowchart/Node.py | 2 +- pyqtgraph/flowchart/NodeLibrary.py | 2 +- pyqtgraph/functions.py | 8 +++--- pyqtgraph/graphicsItems/ArrowItem.py | 2 +- pyqtgraph/graphicsItems/GradientEditorItem.py | 26 +++++++++---------- pyqtgraph/graphicsItems/InfiniteLine.py | 2 +- pyqtgraph/graphicsItems/IsocurveItem.py | 4 +-- pyqtgraph/graphicsItems/LinearRegionItem.py | 4 +-- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 4 +-- pyqtgraph/graphicsItems/TextItem.py | 2 +- pyqtgraph/graphicsItems/VTickGroup.py | 4 +-- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 4 +-- pyqtgraph/multiprocess/parallelizer.py | 2 +- pyqtgraph/multiprocess/processes.py | 2 +- pyqtgraph/multiprocess/remoteproxy.py | 2 +- pyqtgraph/opengl/items/GLLinePlotItem.py | 2 +- pyqtgraph/opengl/items/GLScatterPlotItem.py | 2 +- pyqtgraph/parametertree/Parameter.py | 4 +-- pyqtgraph/widgets/GraphicsView.py | 2 +- 24 files changed, 46 insertions(+), 46 deletions(-) diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index a808765c..1e67a6ab 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -431,7 +431,7 @@ class CanvasItem(QtCore.QObject): def selectionChanged(self, sel, multi): """ Inform the item that its selection state has changed. - Arguments: + **Arguments:** sel: bool, whether the item is currently selected multi: bool, whether there are multiple items currently selected """ diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index cb1e882e..06054c3a 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -53,7 +53,7 @@ class ColorMap(object): def __init__(self, pos, color, mode=None): """ ========= ============================================================== - Arguments + **Arguments:** pos Array of positions where each color is defined color Array of RGBA colors. Integer data types are interpreted as 0-255; float data types @@ -194,7 +194,7 @@ class ColorMap(object): Return an RGB(A) lookup table (ndarray). ============= ============================================================================ - **Arguments** + **Arguments:** start The starting value in the lookup table (default=0.0) stop The final value in the lookup table (default=1.0) nPts The number of points in the returned lookup table. diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 0cbd2c3e..b13b7eb4 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -32,7 +32,7 @@ class ConsoleWidget(QtGui.QWidget): def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None): """ ============ ============================================================================ - Arguments: + **Arguments:** namespace dictionary containing the initial variables present in the default namespace historyFile optional file for storing command history text initial text to display in the console window diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 685780d4..df39ae29 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -828,7 +828,7 @@ def typeStr(obj): def searchRefs(obj, *args): """Pseudo-interactive function for tracing references backward. - Arguments: + **Arguments:** obj: The initial object from which to start searching args: A set of string or int arguments. each integer selects one of obj's referrers to be the new 'obj' diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index 5c367f0b..80017b0d 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -37,7 +37,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop): """Adds a dock to this area. =========== ================================================================= - Arguments: + **Arguments:** dock The new Dock object to add. If None, then a new Dock will be created. position 'bottom', 'top', 'left', 'right', 'above', or 'below' diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py index b6ed1e0f..6ae87765 100644 --- a/pyqtgraph/flowchart/Node.py +++ b/pyqtgraph/flowchart/Node.py @@ -37,7 +37,7 @@ class Node(QtCore.QObject): def __init__(self, name, terminals=None, allowAddInput=False, allowAddOutput=False, allowRemove=True): """ ============== ============================================================ - Arguments + **Arguments:** name The name of this specific node instance. It can be any string, but must be unique within a flowchart. Usually, we simply let the flowchart decide on a name when calling diff --git a/pyqtgraph/flowchart/NodeLibrary.py b/pyqtgraph/flowchart/NodeLibrary.py index a30ffb2a..40bf9f98 100644 --- a/pyqtgraph/flowchart/NodeLibrary.py +++ b/pyqtgraph/flowchart/NodeLibrary.py @@ -26,7 +26,7 @@ class NodeLibrary: Register a new node type. If the type's name is already in use, an exception will be raised (unless override=True). - Arguments: + **Arguments:** nodeClass - a subclass of Node (must have typ.nodeName) paths - list of tuples specifying the location(s) this diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index db01b6b9..8a4875ab 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -388,7 +388,7 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, For a graphical interface to this function, see :func:`ROI.getArrayRegion ` ============== ==================================================================================================== - Arguments: + **Arguments:** *data* (ndarray) the original dataset *shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape)) *origin* the location in the original dataset that will become the origin of the sliced data. @@ -752,7 +752,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): Both stages are optional. ============ ================================================================================== - Arguments: + **Arguments:** data numpy array of int/float types. If levels List [min, max]; optionally rescale data before converting through the lookup table. The data is rescaled such that min->0 and max->*scale*:: @@ -888,7 +888,7 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): freeing that memory while the image is in use. =========== =================================================================== - Arguments: + **Arguments:** imgData Array of data to convert. Must have shape (width, height, 3 or 4) and dtype=ubyte. The order of values in the 3rd axis must be (b, g, r, a). @@ -1241,7 +1241,7 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): Generate isocurve from 2D data using marching squares algorithm. ============= ========================================================= - Arguments + **Arguments:** data 2D numpy array of scalar values level The level at which to generate an isosurface connected If False, return a single long list of point pairs diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index c98ba127..1473c8ba 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -48,7 +48,7 @@ class ArrowItem(QtGui.QGraphicsPathItem): All arguments are optional: ================= ================================================= - Keyword Arguments + **Keyword Arguments:** angle Orientation of the arrow in degrees. Default is 0; arrow pointing to the left. headLen Length of the arrow head, from tip to base. diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index f5158a74..b8aa4b31 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -36,7 +36,7 @@ class TickSliderItem(GraphicsWidget): def __init__(self, orientation='bottom', allowAdd=True, **kargs): """ ============= ================================================================================= - **Arguments** + **Arguments:** orientation Set the orientation of the gradient. Options are: 'left', 'right' 'top', and 'bottom'. allowAdd Specifies whether ticks can be added to the item by the user. @@ -104,7 +104,7 @@ class TickSliderItem(GraphicsWidget): """Set the orientation of the TickSliderItem. ============= =================================================================== - **Arguments** + **Arguments:** orientation Options are: 'left', 'right', 'top', 'bottom' The orientation option specifies which side of the slider the ticks are on, as well as whether the slider is vertical ('right' @@ -137,7 +137,7 @@ class TickSliderItem(GraphicsWidget): Add a tick to the item. ============= ================================================================== - **Arguments** + **Arguments:** x Position where tick should be added. color Color of added tick. If color is not specified, the color will be white. @@ -266,7 +266,7 @@ class TickSliderItem(GraphicsWidget): """Set the color of the specified tick. ============= ================================================================== - **Arguments** + **Arguments:** tick Can be either an integer corresponding to the index of the tick or a Tick object. Ex: if you had a slider with 3 ticks and you wanted to change the middle tick, the index would be 1. @@ -285,7 +285,7 @@ class TickSliderItem(GraphicsWidget): Set the position (along the slider) of the tick. ============= ================================================================== - **Arguments** + **Arguments:** tick Can be either an integer corresponding to the index of the tick or a Tick object. Ex: if you had a slider with 3 ticks and you wanted to change the middle tick, the index would be 1. @@ -306,7 +306,7 @@ class TickSliderItem(GraphicsWidget): """Return the value (from 0.0 to 1.0) of the specified tick. ============= ================================================================== - **Arguments** + **Arguments:** tick Can be either an integer corresponding to the index of the tick or a Tick object. Ex: if you had a slider with 3 ticks and you wanted the value of the middle tick, the index would be 1. @@ -320,7 +320,7 @@ class TickSliderItem(GraphicsWidget): """Return the Tick object at the specified index. ============= ================================================================== - **Arguments** + **Arguments:** tick An integer corresponding to the index of the desired tick. If the argument is not an integer it will be returned unchanged. ============= ================================================================== @@ -367,7 +367,7 @@ class GradientEditorItem(TickSliderItem): All arguments are passed to :func:`TickSliderItem.__init__ ` ============= ================================================================================= - **Arguments** + **Arguments:** orientation Set the orientation of the gradient. Options are: 'left', 'right' 'top', and 'bottom'. allowAdd Default is True. Specifies whether ticks can be added to the item. @@ -446,7 +446,7 @@ class GradientEditorItem(TickSliderItem): Set the orientation of the GradientEditorItem. ============= =================================================================== - **Arguments** + **Arguments:** orientation Options are: 'left', 'right', 'top', 'bottom' The orientation option specifies which side of the gradient the ticks are on, as well as whether the gradient is vertical ('right' @@ -589,7 +589,7 @@ class GradientEditorItem(TickSliderItem): Return a color for a given value. ============= ================================================================== - **Arguments** + **Arguments:** x Value (position on gradient) of requested color. toQColor If true, returns a QColor object, else returns a (r,g,b,a) tuple. ============= ================================================================== @@ -649,7 +649,7 @@ class GradientEditorItem(TickSliderItem): Return an RGB(A) lookup table (ndarray). ============= ============================================================================ - **Arguments** + **Arguments:** nPts The number of points in the returned lookup table. alpha True, False, or None - Specifies whether or not alpha values are included in the table.If alpha is None, alpha will be automatically determined. @@ -703,7 +703,7 @@ class GradientEditorItem(TickSliderItem): Add a tick to the gradient. Return the tick. ============= ================================================================== - **Arguments** + **Arguments:** x Position where tick should be added. color Color of added tick. If color is not specified, the color will be the color of the gradient at the specified position. @@ -749,7 +749,7 @@ class GradientEditorItem(TickSliderItem): Restore the gradient specified in state. ============= ==================================================================== - **Arguments** + **Arguments:** state A dictionary with same structure as those returned by :func:`saveState ` diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index edf6b19e..abffd325 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -29,7 +29,7 @@ class InfiniteLine(GraphicsObject): def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None): """ ============= ================================================================== - **Arguments** + **Arguments:** pos Position of the line. This can be a QPointF or a single value for vertical/horizontal lines. angle Angle of line in degrees. 0 is horizontal, 90 is vertical. diff --git a/pyqtgraph/graphicsItems/IsocurveItem.py b/pyqtgraph/graphicsItems/IsocurveItem.py index 71113ba8..0e568046 100644 --- a/pyqtgraph/graphicsItems/IsocurveItem.py +++ b/pyqtgraph/graphicsItems/IsocurveItem.py @@ -19,7 +19,7 @@ class IsocurveItem(GraphicsObject): Create a new isocurve item. ============= =============================================================== - **Arguments** + **Arguments:** data A 2-dimensional ndarray. Can be initialized as None, and set later using :func:`setData ` level The cutoff value at which to draw the isocurve. @@ -46,7 +46,7 @@ class IsocurveItem(GraphicsObject): Set the data/image to draw isocurves for. ============= ======================================================================== - **Arguments** + **Arguments:** data A 2-dimensional ndarray. level The cutoff value at which to draw the curve. If level is not specified, the previously set level is used. diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py index 4f9d28dc..04524116 100644 --- a/pyqtgraph/graphicsItems/LinearRegionItem.py +++ b/pyqtgraph/graphicsItems/LinearRegionItem.py @@ -31,7 +31,7 @@ class LinearRegionItem(UIGraphicsItem): """Create a new LinearRegionItem. ============= ===================================================================== - **Arguments** + **Arguments:** values A list of the positions of the lines in the region. These are not limits; limits can be set by specifying bounds. orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal. @@ -90,7 +90,7 @@ class LinearRegionItem(UIGraphicsItem): """Set the values for the edges of the region. ============= ============================================== - **Arguments** + **Arguments:** rgn A list or tuple of the lower and upper values. ============= ============================================== """ diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 575a1599..138244a5 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -102,7 +102,7 @@ class PlotItem(GraphicsWidget): Any extra keyword arguments are passed to PlotItem.plot(). ============== ========================================================================================== - **Arguments** + **Arguments:** *title* Title to display at the top of the item. Html is allowed. *labels* A dictionary specifying the axis labels to display:: @@ -1113,7 +1113,7 @@ class PlotItem(GraphicsWidget): Set the label for an axis. Basic HTML formatting is allowed. ============= ================================================================= - **Arguments** + **Arguments:** axis must be one of 'left', 'bottom', 'right', or 'top' text text to display along the axis. HTML allowed. units units to display after the title. If units are given, diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index 2b5ea51c..61145594 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -10,7 +10,7 @@ class TextItem(UIGraphicsItem): def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0): """ =========== ================================================================================= - Arguments: + **Arguments:** *text* The text to display *color* The color of the text (any format accepted by pg.mkColor) *html* If specified, this overrides both *text* and *color* diff --git a/pyqtgraph/graphicsItems/VTickGroup.py b/pyqtgraph/graphicsItems/VTickGroup.py index 4b315678..2bf6a7fd 100644 --- a/pyqtgraph/graphicsItems/VTickGroup.py +++ b/pyqtgraph/graphicsItems/VTickGroup.py @@ -20,7 +20,7 @@ class VTickGroup(UIGraphicsItem): def __init__(self, xvals=None, yrange=None, pen=None): """ ============= =================================================================== - **Arguments** + **Arguments:** xvals A list of x values (in data coordinates) at which to draw ticks. yrange A list of [low, high] limits for the tick. 0 is the bottom of the view, 1 is the top. [0.8, 1] would draw ticks in the top @@ -57,7 +57,7 @@ class VTickGroup(UIGraphicsItem): """Set the x values for the ticks. ============= ===================================================================== - **Arguments** + **Arguments:** vals A list of x values (in data/plot coordinates) at which to draw ticks. ============= ===================================================================== """ diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 70012ec4..15434e53 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -72,7 +72,7 @@ class ViewBox(GraphicsWidget): def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None): """ ============= ============================================================= - **Arguments** + **Arguments:** *parent* (QGraphicsWidget) Optional parent widget *border* (QPen) Do draw a border around the view, give any single argument accepted by :func:`mkPen ` @@ -404,7 +404,7 @@ class ViewBox(GraphicsWidget): Must specify at least one of *rect*, *xRange*, or *yRange*. ================== ===================================================================== - **Arguments** + **Arguments:** *rect* (QRectF) The full range that should be visible in the view box. *xRange* (min,max) The range that should be visible along the x-axis. *yRange* (min,max) The range that should be visible along the y-axis. diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index 4ad30b6e..f4ddd95c 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -40,7 +40,7 @@ class Parallelize(object): def __init__(self, tasks=None, workers=None, block=True, progressDialog=None, randomReseed=True, **kwds): """ =============== =================================================================== - Arguments: + **Arguments:** tasks list of objects to be processed (Parallelize will determine how to distribute the tasks). If unspecified, then each worker will receive a single task with a unique id number. diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 5a4ccb99..393baa8b 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -39,7 +39,7 @@ class Process(RemoteEventHandler): def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None): """ ============ ============================================================= - Arguments: + **Arguments:** name Optional name for this process used when printing messages from the remote process. target Optional function to call after starting remote process. diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index eba42ef3..1021bcd6 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -300,7 +300,7 @@ class RemoteEventHandler(object): as it describes the internal protocol used to communicate between processes) ========== ==================================================================== - Arguments: + **Arguments:** request String describing the type of request being sent (see below) reqId Integer uniquely linking a result back to the request that generated it. (most requests leave this blank) diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py index a578dd1d..0b0c1793 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -27,7 +27,7 @@ class GLLinePlotItem(GLGraphicsItem): colors unchanged, etc. ==================== ================================================== - Arguments: + **Arguments:** ------------------------------------------------------------------------ pos (N,3) array of floats specifying point locations. color (N,4) array of floats (0.0-1.0) or diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index 9ddd3b34..123ff0e3 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -28,7 +28,7 @@ class GLScatterPlotItem(GLGraphicsItem): colors unchanged, etc. ==================== ================================================== - Arguments: + **Arguments:** ------------------------------------------------------------------------ pos (N,3) array of floats specifying point locations. color (N,4) array of floats (0.0-1.0) specifying diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 45e46e55..068ee982 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -108,7 +108,7 @@ class Parameter(QtCore.QObject): by most Parameter subclasses. ================= ========================================================= - Keyword Arguments + **Keyword Arguments:** name The name to give this Parameter. This is the name that will appear in the left-most column of a ParameterTree for this Parameter. @@ -676,7 +676,7 @@ class Parameter(QtCore.QObject): Called when the state of any sub-parameter has changed. ========== ================================================================ - Arguments: + **Arguments:** param The immediate child whose tree state has changed. note that the change may have originated from a grandchild. changes List of tuples describing all changes that have been made diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index f3f4856a..70472fd3 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -51,7 +51,7 @@ class GraphicsView(QtGui.QGraphicsView): def __init__(self, parent=None, useOpenGL=None, background='default'): """ ============ ============================================================ - Arguments: + **Arguments:** parent Optional parent widget useOpenGL If True, the GraphicsView will use OpenGL to do all of its rendering. This can improve performance on some systems, From bccbc2994061dc377f33cf419464f28b990450d4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 3 Feb 2014 22:24:45 -0500 Subject: [PATCH 093/268] Bugfixes: - isosurface works for arrays with shapes > 255 - Fixed ImageItem exception building histogram when image has only one value - Fixed MeshData exception caused when vertexes have no matching faces - Fixed GLViewWidget exception handler --- CHANGELOG | 5 +++++ pyqtgraph/functions.py | 5 +---- pyqtgraph/graphicsItems/ImageItem.py | 2 ++ pyqtgraph/opengl/GLViewWidget.py | 2 +- pyqtgraph/opengl/MeshData.py | 10 ++++++---- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dcc21eb5..15ce1536 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -48,6 +48,11 @@ pyqtgraph-0.9.9 [unreleased] - PlotCurveItem ignores clip-to-view when auto range is enabled - FillBetweenItem now forces PlotCurveItem to generate path - Fixed import errors and py3 issues in MultiPlotWidget + - Isosurface works for arrays with shapes > 255 + - Fixed ImageItem exception building histogram when image has only one value + - Fixed MeshData exception caused when vertexes have no matching faces + - Fixed GLViewWidget exception handler + pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index db01b6b9..99c45606 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1790,7 +1790,7 @@ def isosurface(data, level): [1, 1, 0, 2], [0, 1, 0, 2], #[9, 9, 9, 9] ## fake - ], dtype=np.ubyte) + ], dtype=np.uint16) # don't use ubyte here! This value gets added to cell index later; will need the extra precision. nTableFaces = np.array([len(f)/3 for f in triTable], dtype=np.ubyte) faceShiftTables = [None] for i in range(1,6): @@ -1889,7 +1889,6 @@ def isosurface(data, level): #profiler() if cells.shape[0] == 0: continue - #cellInds = index[(cells*ins[np.newaxis,:]).sum(axis=1)] cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round #profiler() @@ -1901,9 +1900,7 @@ def isosurface(data, level): #profiler() ### expensive: - #print verts.shape verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2) - #vertInds = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want. vertInds = cutEdges[verts] #profiler() nv = vertInds.shape[0] diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 7c80859d..6da8aedc 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -322,6 +322,8 @@ class ImageItem(GraphicsObject): mx = stepData.max() step = np.ceil((mx-mn) / 500.) bins = np.arange(mn, mx+1.01*step, step, dtype=np.int) + if len(bins) == 0: + bins = [mn, mx] else: bins = 500 diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index d74a13ce..0516bf08 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -180,7 +180,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): i.paint() except: from .. import debug - pyqtgraph.debug.printExc() + debug.printExc() msg = "Error while drawing item %s." % str(item) ver = glGetString(GL_VERSION) if ver is not None: diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index 3046459d..e6888c16 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -266,7 +266,11 @@ class MeshData(object): vertFaces = self.vertexFaces() self._vertexNormals = np.empty(self._vertexes.shape, dtype=float) for vindex in xrange(self._vertexes.shape[0]): - norms = faceNorms[vertFaces[vindex]] ## get all face normals + faces = vertFaces[vindex] + if len(faces) == 0: + self._vertexNormals[vindex] = (0,0,0) + continue + norms = faceNorms[faces] ## get all face normals norm = norms.sum(axis=0) ## sum normals norm /= (norm**2).sum()**0.5 ## and re-normalize self._vertexNormals[vindex] = norm @@ -403,12 +407,10 @@ class MeshData(object): Return list mapping each vertex index to a list of face indexes that use the vertex. """ if self._vertexFaces is None: - self._vertexFaces = [None] * len(self.vertexes()) + self._vertexFaces = [[] for i in xrange(len(self.vertexes()))] for i in xrange(self._faces.shape[0]): face = self._faces[i] for ind in face: - if self._vertexFaces[ind] is None: - self._vertexFaces[ind] = [] ## need a unique/empty list to fill self._vertexFaces[ind].append(i) return self._vertexFaces From 9093282a2a9ae0e9019dbdb46a1fccd8a5db7d06 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 4 Feb 2014 20:28:23 -0500 Subject: [PATCH 094/268] Wrap setLimits in PlotItem and PlotWidget --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 3 ++- pyqtgraph/widgets/PlotWidget.py | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 575a1599..77413fc2 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -69,6 +69,7 @@ class PlotItem(GraphicsWidget): :func:`setYLink `, :func:`setAutoPan `, :func:`setAutoVisible `, + :func:`setLimits `, :func:`viewRect `, :func:`viewRange `, :func:`setMouseEnabled `, @@ -195,7 +196,7 @@ class PlotItem(GraphicsWidget): ## Wrap a few methods from viewBox for m in [ 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', - 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', + 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY', 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. setattr(self, m, getattr(self.vb, m)) diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index f9b544f5..12176c74 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -33,6 +33,7 @@ class PlotWidget(GraphicsView): :func:`enableAutoRange `, :func:`disableAutoRange `, :func:`setAspectLocked `, + :func:`setLimits `, :func:`register `, :func:`unregister ` @@ -52,7 +53,10 @@ class PlotWidget(GraphicsView): self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem ## NOTE: If you change this list, update the documentation above as well. - for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', 'register', 'unregister', 'viewRect']: + for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', + 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', + 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', + 'setLimits', 'register', 'unregister', 'viewRect']: setattr(self, m, getattr(self.plotItem, m)) #QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged) From 8b6ff6b06a27eb9e9b6f3415069474e12d55c219 Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Wed, 5 Feb 2014 20:03:25 +0100 Subject: [PATCH 095/268] Bugfix: Malformed tables. --- pyqtgraph/colormap.py | 38 ++++++++++---------- pyqtgraph/parametertree/Parameter.py | 54 ++++++++++++++-------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index 06054c3a..38c12097 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -52,17 +52,17 @@ class ColorMap(object): def __init__(self, pos, color, mode=None): """ - ========= ============================================================== + =============== ============================================================== **Arguments:** - pos Array of positions where each color is defined - color Array of RGBA colors. - Integer data types are interpreted as 0-255; float data types - are interpreted as 0.0-1.0 - mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG) - indicating the color space that should be used when - interpolating between stops. Note that the last mode value is - ignored. By default, the mode is entirely RGB. - ========= ============================================================== + pos Array of positions where each color is defined + color Array of RGBA colors. + Integer data types are interpreted as 0-255; float data types + are interpreted as 0.0-1.0 + mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG) + indicating the color space that should be used when + interpolating between stops. Note that the last mode value is + ignored. By default, the mode is entirely RGB. + =============== ============================================================== """ self.pos = pos self.color = color @@ -193,16 +193,16 @@ class ColorMap(object): """ Return an RGB(A) lookup table (ndarray). - ============= ============================================================================ + =============== ============================================================================= **Arguments:** - start The starting value in the lookup table (default=0.0) - stop The final value in the lookup table (default=1.0) - nPts The number of points in the returned lookup table. - alpha True, False, or None - Specifies whether or not alpha values are included - in the table. If alpha is None, it will be automatically determined. - mode Determines return type: 'byte' (0-255), 'float' (0.0-1.0), or 'qcolor'. - See :func:`map() `. - ============= ============================================================================ + start The starting value in the lookup table (default=0.0) + stop The final value in the lookup table (default=1.0) + nPts The number of points in the returned lookup table. + alpha True, False, or None - Specifies whether or not alpha values are included + in the table. If alpha is None, it will be automatically determined. + mode Determines return type: 'byte' (0-255), 'float' (0.0-1.0), or 'qcolor'. + See :func:`map() `. + =============== ============================================================================= """ if isinstance(mode, basestring): mode = self.enumMap[mode.lower()] diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 068ee982..e8b051f7 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -107,33 +107,33 @@ class Parameter(QtCore.QObject): Parameter instance, the options available to this method are also allowed by most Parameter subclasses. - ================= ========================================================= + ======================= ========================================================= **Keyword Arguments:** - name The name to give this Parameter. This is the name that - will appear in the left-most column of a ParameterTree - for this Parameter. - value The value to initially assign to this Parameter. - default The default value for this Parameter (most Parameters - provide an option to 'reset to default'). - children A list of children for this Parameter. Children - may be given either as a Parameter instance or as a - dictionary to pass to Parameter.create(). In this way, - it is possible to specify complex hierarchies of - Parameters from a single nested data structure. - readonly If True, the user will not be allowed to edit this - Parameter. (default=False) - enabled If False, any widget(s) for this parameter will appear - disabled. (default=True) - visible If False, the Parameter will not appear when displayed - in a ParameterTree. (default=True) - renamable If True, the user may rename this Parameter. - (default=False) - removable If True, the user may remove this Parameter. - (default=False) - expanded If True, the Parameter will appear expanded when - displayed in a ParameterTree (its children will be - visible). (default=True) - ================= ========================================================= + name The name to give this Parameter. This is the name that + will appear in the left-most column of a ParameterTree + for this Parameter. + value The value to initially assign to this Parameter. + default The default value for this Parameter (most Parameters + provide an option to 'reset to default'). + children A list of children for this Parameter. Children + may be given either as a Parameter instance or as a + dictionary to pass to Parameter.create(). In this way, + it is possible to specify complex hierarchies of + Parameters from a single nested data structure. + readonly If True, the user will not be allowed to edit this + Parameter. (default=False) + enabled If False, any widget(s) for this parameter will appear + disabled. (default=True) + visible If False, the Parameter will not appear when displayed + in a ParameterTree. (default=True) + renamable If True, the user may rename this Parameter. + (default=False) + removable If True, the user may remove this Parameter. + (default=False) + expanded If True, the Parameter will appear expanded when + displayed in a ParameterTree (its children will be + visible). (default=True) + ======================= ========================================================= """ @@ -676,7 +676,7 @@ class Parameter(QtCore.QObject): Called when the state of any sub-parameter has changed. ========== ================================================================ - **Arguments:** + Arguments: param The immediate child whose tree state has changed. note that the change may have originated from a grandchild. changes List of tuples describing all changes that have been made From 2a13994a2a5b0bb79597b2257a9f71597097e081 Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Wed, 5 Feb 2014 21:04:33 +0100 Subject: [PATCH 096/268] Bugfix: Malformed tables. All argument lists with `**Arguments:**` --- pyqtgraph/graphicsItems/ArrowItem.py | 42 ++--- pyqtgraph/graphicsItems/AxisItem.py | 22 +-- pyqtgraph/graphicsItems/GradientEditorItem.py | 154 +++++++++--------- pyqtgraph/graphicsItems/GraphItem.py | 42 ++--- pyqtgraph/graphicsItems/InfiniteLine.py | 22 +-- pyqtgraph/graphicsItems/IsocurveItem.py | 24 +-- pyqtgraph/graphicsItems/LabelItem.py | 2 +- pyqtgraph/graphicsItems/LegendItem.py | 46 +++--- pyqtgraph/graphicsItems/LinearRegionItem.py | 30 ++-- pyqtgraph/graphicsItems/PlotDataItem.py | 24 +-- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 40 ++--- pyqtgraph/graphicsItems/ScatterPlotItem.py | 4 +- pyqtgraph/graphicsItems/TextItem.py | 22 +-- pyqtgraph/graphicsItems/VTickGroup.py | 22 +-- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 34 ++-- pyqtgraph/multiprocess/processes.py | 36 ++-- pyqtgraph/multiprocess/remoteproxy.py | 32 ++-- pyqtgraph/opengl/MeshData.py | 24 +-- pyqtgraph/opengl/items/GLMeshItem.py | 2 +- pyqtgraph/opengl/items/GLSurfacePlotItem.py | 16 +- pyqtgraph/parametertree/Parameter.py | 16 +- pyqtgraph/widgets/ColorMapWidget.py | 16 +- pyqtgraph/widgets/ValueLabel.py | 24 +-- 23 files changed, 348 insertions(+), 348 deletions(-) diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index 1473c8ba..74066fd7 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -47,27 +47,27 @@ class ArrowItem(QtGui.QGraphicsPathItem): Changes the appearance of the arrow. All arguments are optional: - ================= ================================================= - **Keyword Arguments:** - angle Orientation of the arrow in degrees. Default is - 0; arrow pointing to the left. - headLen Length of the arrow head, from tip to base. - default=20 - headWidth Width of the arrow head at its base. - tipAngle Angle of the tip of the arrow in degrees. Smaller - values make a 'sharper' arrow. If tipAngle is - specified, ot overrides headWidth. default=25 - baseAngle Angle of the base of the arrow head. Default is - 0, which means that the base of the arrow head - is perpendicular to the arrow tail. - tailLen Length of the arrow tail, measured from the base - of the arrow head to the end of the tail. If - this value is None, no tail will be drawn. - default=None - tailWidth Width of the tail. default=3 - pen The pen used to draw the outline of the arrow. - brush The brush used to fill the arrow. - ================= ================================================= + ====================== ================================================= + **Keyword** + angle Orientation of the arrow in degrees. Default is + 0; arrow pointing to the left. + headLen Length of the arrow head, from tip to base. + default=20 + headWidth Width of the arrow head at its base. + tipAngle Angle of the tip of the arrow in degrees. Smaller + values make a 'sharper' arrow. If tipAngle is + specified, ot overrides headWidth. default=25 + baseAngle Angle of the base of the arrow head. Default is + 0, which means that the base of the arrow head + is perpendicular to the arrow tail. + tailLen Length of the arrow tail, measured from the base + of the arrow head to the end of the tail. If + this value is None, no tail will be drawn. + default=None + tailWidth Width of the tail. default=3 + pen The pen used to draw the outline of the arrow. + brush The brush used to fill the arrow. + ====================== ================================================= """ self.opts.update(opts) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 66efeda5..981aca83 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -156,17 +156,17 @@ class AxisItem(GraphicsWidget): def setLabel(self, text=None, units=None, unitPrefix=None, **args): """Set the text displayed adjacent to the axis. - ============= ============================================================= - Arguments - text The text (excluding units) to display on the label for this - axis. - units The units for this axis. Units should generally be given - without any scaling prefix (eg, 'V' instead of 'mV'). The - scaling prefix will be automatically prepended based on the - range of data displayed. - **args All extra keyword arguments become CSS style options for - the tag which will surround the axis label and units. - ============= ============================================================= + ============== ============================================================= + **Arguments:** + text The text (excluding units) to display on the label for this + axis. + units The units for this axis. Units should generally be given + without any scaling prefix (eg, 'V' instead of 'mV'). The + scaling prefix will be automatically prepended based on the + range of data displayed. + **args All extra keyword arguments become CSS style options for + the tag which will surround the axis label and units. + ============== ============================================================= The final text generated for the label will look like:: diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index b8aa4b31..92a4a672 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -35,14 +35,14 @@ class TickSliderItem(GraphicsWidget): def __init__(self, orientation='bottom', allowAdd=True, **kargs): """ - ============= ================================================================================= + ============== ================================================================================= **Arguments:** - orientation Set the orientation of the gradient. Options are: 'left', 'right' - 'top', and 'bottom'. - allowAdd Specifies whether ticks can be added to the item by the user. - tickPen Default is white. Specifies the color of the outline of the ticks. - Can be any of the valid arguments for :func:`mkPen ` - ============= ================================================================================= + orientation Set the orientation of the gradient. Options are: 'left', 'right' + 'top', and 'bottom'. + allowAdd Specifies whether ticks can be added to the item by the user. + tickPen Default is white. Specifies the color of the outline of the ticks. + Can be any of the valid arguments for :func:`mkPen ` + ============== ================================================================================= """ ## public GraphicsWidget.__init__(self) @@ -103,13 +103,13 @@ class TickSliderItem(GraphicsWidget): ## public """Set the orientation of the TickSliderItem. - ============= =================================================================== + ============== =================================================================== **Arguments:** - orientation Options are: 'left', 'right', 'top', 'bottom' - The orientation option specifies which side of the slider the - ticks are on, as well as whether the slider is vertical ('right' - and 'left') or horizontal ('top' and 'bottom'). - ============= =================================================================== + orientation Options are: 'left', 'right', 'top', 'bottom' + The orientation option specifies which side of the slider the + ticks are on, as well as whether the slider is vertical ('right' + and 'left') or horizontal ('top' and 'bottom'). + ============== =================================================================== """ self.orientation = orientation self.setMaxDim() @@ -136,13 +136,13 @@ class TickSliderItem(GraphicsWidget): """ Add a tick to the item. - ============= ================================================================== + ============== ================================================================== **Arguments:** - x Position where tick should be added. - color Color of added tick. If color is not specified, the color will be - white. - movable Specifies whether the tick is movable with the mouse. - ============= ================================================================== + x Position where tick should be added. + color Color of added tick. If color is not specified, the color will be + white. + movable Specifies whether the tick is movable with the mouse. + ============== ================================================================== """ if color is None: @@ -265,14 +265,14 @@ class TickSliderItem(GraphicsWidget): def setTickColor(self, tick, color): """Set the color of the specified tick. - ============= ================================================================== + ============== ================================================================== **Arguments:** - tick Can be either an integer corresponding to the index of the tick - or a Tick object. Ex: if you had a slider with 3 ticks and you - wanted to change the middle tick, the index would be 1. - color The color to make the tick. Can be any argument that is valid for - :func:`mkBrush ` - ============= ================================================================== + tick Can be either an integer corresponding to the index of the tick + or a Tick object. Ex: if you had a slider with 3 ticks and you + wanted to change the middle tick, the index would be 1. + color The color to make the tick. Can be any argument that is valid for + :func:`mkBrush ` + ============== ================================================================== """ tick = self.getTick(tick) tick.color = color @@ -284,14 +284,14 @@ class TickSliderItem(GraphicsWidget): """ Set the position (along the slider) of the tick. - ============= ================================================================== + ============== ================================================================== **Arguments:** - tick Can be either an integer corresponding to the index of the tick - or a Tick object. Ex: if you had a slider with 3 ticks and you - wanted to change the middle tick, the index would be 1. - val The desired position of the tick. If val is < 0, position will be - set to 0. If val is > 1, position will be set to 1. - ============= ================================================================== + tick Can be either an integer corresponding to the index of the tick + or a Tick object. Ex: if you had a slider with 3 ticks and you + wanted to change the middle tick, the index would be 1. + val The desired position of the tick. If val is < 0, position will be + set to 0. If val is > 1, position will be set to 1. + ============== ================================================================== """ tick = self.getTick(tick) val = min(max(0.0, val), 1.0) @@ -305,12 +305,12 @@ class TickSliderItem(GraphicsWidget): ## public """Return the value (from 0.0 to 1.0) of the specified tick. - ============= ================================================================== + ============== ================================================================== **Arguments:** - tick Can be either an integer corresponding to the index of the tick - or a Tick object. Ex: if you had a slider with 3 ticks and you - wanted the value of the middle tick, the index would be 1. - ============= ================================================================== + tick Can be either an integer corresponding to the index of the tick + or a Tick object. Ex: if you had a slider with 3 ticks and you + wanted the value of the middle tick, the index would be 1. + ============== ================================================================== """ tick = self.getTick(tick) return self.ticks[tick] @@ -319,11 +319,11 @@ class TickSliderItem(GraphicsWidget): ## public """Return the Tick object at the specified index. - ============= ================================================================== + ============== ================================================================== **Arguments:** - tick An integer corresponding to the index of the desired tick. If the - argument is not an integer it will be returned unchanged. - ============= ================================================================== + tick An integer corresponding to the index of the desired tick. If the + argument is not an integer it will be returned unchanged. + ============== ================================================================== """ if type(tick) is int: tick = self.listTicks()[tick][0] @@ -366,14 +366,14 @@ class GradientEditorItem(TickSliderItem): Create a new GradientEditorItem. All arguments are passed to :func:`TickSliderItem.__init__ ` - ============= ================================================================================= + =============== ================================================================================= **Arguments:** - orientation Set the orientation of the gradient. Options are: 'left', 'right' - 'top', and 'bottom'. - allowAdd Default is True. Specifies whether ticks can be added to the item. - tickPen Default is white. Specifies the color of the outline of the ticks. - Can be any of the valid arguments for :func:`mkPen ` - ============= ================================================================================= + orientation Set the orientation of the gradient. Options are: 'left', 'right' + 'top', and 'bottom'. + allowAdd Default is True. Specifies whether ticks can be added to the item. + tickPen Default is white. Specifies the color of the outline of the ticks. + Can be any of the valid arguments for :func:`mkPen ` + =============== ================================================================================= """ self.currentTick = None self.currentTickColor = None @@ -445,13 +445,13 @@ class GradientEditorItem(TickSliderItem): """ Set the orientation of the GradientEditorItem. - ============= =================================================================== + ============== =================================================================== **Arguments:** - orientation Options are: 'left', 'right', 'top', 'bottom' - The orientation option specifies which side of the gradient the - ticks are on, as well as whether the gradient is vertical ('right' - and 'left') or horizontal ('top' and 'bottom'). - ============= =================================================================== + orientation Options are: 'left', 'right', 'top', 'bottom' + The orientation option specifies which side of the gradient the + ticks are on, as well as whether the gradient is vertical ('right' + and 'left') or horizontal ('top' and 'bottom'). + ============== =================================================================== """ TickSliderItem.setOrientation(self, orientation) self.translate(0, self.rectSize) @@ -588,11 +588,11 @@ class GradientEditorItem(TickSliderItem): """ Return a color for a given value. - ============= ================================================================== + ============== ================================================================== **Arguments:** - x Value (position on gradient) of requested color. - toQColor If true, returns a QColor object, else returns a (r,g,b,a) tuple. - ============= ================================================================== + x Value (position on gradient) of requested color. + toQColor If true, returns a QColor object, else returns a (r,g,b,a) tuple. + ============== ================================================================== """ ticks = self.listTicks() if x <= ticks[0][1]: @@ -648,12 +648,12 @@ class GradientEditorItem(TickSliderItem): """ Return an RGB(A) lookup table (ndarray). - ============= ============================================================================ + ============== ============================================================================ **Arguments:** - nPts The number of points in the returned lookup table. - alpha True, False, or None - Specifies whether or not alpha values are included - in the table.If alpha is None, alpha will be automatically determined. - ============= ============================================================================ + nPts The number of points in the returned lookup table. + alpha True, False, or None - Specifies whether or not alpha values are included + in the table.If alpha is None, alpha will be automatically determined. + ============== ============================================================================ """ if alpha is None: alpha = self.usesAlpha() @@ -702,13 +702,13 @@ class GradientEditorItem(TickSliderItem): """ Add a tick to the gradient. Return the tick. - ============= ================================================================== + ============== ================================================================== **Arguments:** - x Position where tick should be added. - color Color of added tick. If color is not specified, the color will be - the color of the gradient at the specified position. - movable Specifies whether the tick is movable with the mouse. - ============= ================================================================== + x Position where tick should be added. + color Color of added tick. If color is not specified, the color will be + the color of the gradient at the specified position. + movable Specifies whether the tick is movable with the mouse. + ============== ================================================================== """ @@ -748,16 +748,16 @@ class GradientEditorItem(TickSliderItem): """ Restore the gradient specified in state. - ============= ==================================================================== + ============== ==================================================================== **Arguments:** - state A dictionary with same structure as those returned by - :func:`saveState ` + state A dictionary with same structure as those returned by + :func:`saveState ` - Keys must include: + Keys must include: - - 'mode': hsv or rgb - - 'ticks': a list of tuples (pos, (r,g,b,a)) - ============= ==================================================================== + - 'mode': hsv or rgb + - 'ticks': a list of tuples (pos, (r,g,b,a)) + ============== ==================================================================== """ ## public self.setColorMode(state['mode']) diff --git a/pyqtgraph/graphicsItems/GraphItem.py b/pyqtgraph/graphicsItems/GraphItem.py index 97759522..6860790c 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -28,29 +28,29 @@ class GraphItem(GraphicsObject): """ Change the data displayed by the graph. - ============ ========================================================= - Arguments - pos (N,2) array of the positions of each node in the graph. - adj (M,2) array of connection data. Each row contains indexes - of two nodes that are connected. - pen The pen to use when drawing lines between connected - nodes. May be one of: + ============== ========================================================= + **Arguments:** + pos (N,2) array of the positions of each node in the graph. + adj (M,2) array of connection data. Each row contains indexes + of two nodes that are connected. + pen The pen to use when drawing lines between connected + nodes. May be one of: - * QPen - * a single argument to pass to pg.mkPen - * a record array of length M - with fields (red, green, blue, alpha, width). Note - that using this option may have a significant performance - cost. - * None (to disable connection drawing) - * 'default' to use the default foreground color. + * QPen + * a single argument to pass to pg.mkPen + * a record array of length M + with fields (red, green, blue, alpha, width). Note + that using this option may have a significant performance + cost. + * None (to disable connection drawing) + * 'default' to use the default foreground color. - symbolPen The pen used for drawing nodes. - ``**opts`` All other keyword arguments are given to - :func:`ScatterPlotItem.setData() ` - to affect the appearance of nodes (symbol, size, brush, - etc.) - ============ ========================================================= + symbolPen The pen used for drawing nodes. + ``**opts`` All other keyword arguments are given to + :func:`ScatterPlotItem.setData() ` + to affect the appearance of nodes (symbol, size, brush, + etc.) + ============== ========================================================= """ if 'adj' in kwds: self.adjacency = kwds.pop('adj') diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index abffd325..df1d5bda 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -28,18 +28,18 @@ class InfiniteLine(GraphicsObject): def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None): """ - ============= ================================================================== + ============== ================================================================== **Arguments:** - pos Position of the line. This can be a QPointF or a single value for - vertical/horizontal lines. - angle Angle of line in degrees. 0 is horizontal, 90 is vertical. - pen Pen to use when drawing line. Can be any arguments that are valid - for :func:`mkPen `. Default pen is transparent - yellow. - movable If True, the line can be dragged to a new position by the user. - bounds Optional [min, max] bounding values. Bounds are only valid if the - line is vertical or horizontal. - ============= ================================================================== + pos Position of the line. This can be a QPointF or a single value for + vertical/horizontal lines. + angle Angle of line in degrees. 0 is horizontal, 90 is vertical. + pen Pen to use when drawing line. Can be any arguments that are valid + for :func:`mkPen `. Default pen is transparent + yellow. + movable If True, the line can be dragged to a new position by the user. + bounds Optional [min, max] bounding values. Bounds are only valid if the + line is vertical or horizontal. + ============= ================================================================== """ GraphicsObject.__init__(self) diff --git a/pyqtgraph/graphicsItems/IsocurveItem.py b/pyqtgraph/graphicsItems/IsocurveItem.py index 0e568046..897df999 100644 --- a/pyqtgraph/graphicsItems/IsocurveItem.py +++ b/pyqtgraph/graphicsItems/IsocurveItem.py @@ -18,14 +18,14 @@ class IsocurveItem(GraphicsObject): """ Create a new isocurve item. - ============= =============================================================== + ============== =============================================================== **Arguments:** - data A 2-dimensional ndarray. Can be initialized as None, and set - later using :func:`setData ` - level The cutoff value at which to draw the isocurve. - pen The color of the curve item. Can be anything valid for - :func:`mkPen ` - ============= =============================================================== + data A 2-dimensional ndarray. Can be initialized as None, and set + later using :func:`setData ` + level The cutoff value at which to draw the isocurve. + pen The color of the curve item. Can be anything valid for + :func:`mkPen ` + ============== =============================================================== """ GraphicsObject.__init__(self) @@ -45,12 +45,12 @@ class IsocurveItem(GraphicsObject): """ Set the data/image to draw isocurves for. - ============= ======================================================================== + ============== ======================================================================== **Arguments:** - data A 2-dimensional ndarray. - level The cutoff value at which to draw the curve. If level is not specified, - the previously set level is used. - ============= ======================================================================== + data A 2-dimensional ndarray. + level The cutoff value at which to draw the curve. If level is not specified, + the previously set level is used. + ============== ======================================================================== """ if level is None: level = self.level diff --git a/pyqtgraph/graphicsItems/LabelItem.py b/pyqtgraph/graphicsItems/LabelItem.py index 37980ee3..57127a07 100644 --- a/pyqtgraph/graphicsItems/LabelItem.py +++ b/pyqtgraph/graphicsItems/LabelItem.py @@ -38,7 +38,7 @@ class LabelItem(GraphicsWidget, GraphicsWidgetAnchor): a CSS style string: ==================== ============================== - **Style Arguments:** + **Style** color (str) example: 'CCFF00' size (str) example: '8pt' bold (bool) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index ba6a6897..ea6798fb 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -21,17 +21,17 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): """ def __init__(self, size=None, offset=None): """ - ========== =============================================================== - Arguments - size Specifies the fixed size (width, height) of the legend. If - this argument is omitted, the legend will autimatically resize - to fit its contents. - offset Specifies the offset position relative to the legend's parent. - Positive values offset from the left or top; negative values - offset from the right or bottom. If offset is None, the - legend must be anchored manually by calling anchor() or - positioned by calling setPos(). - ========== =============================================================== + ============== =============================================================== + **Arguments:** + size Specifies the fixed size (width, height) of the legend. If + this argument is omitted, the legend will autimatically resize + to fit its contents. + offset Specifies the offset position relative to the legend's parent. + Positive values offset from the left or top; negative values + offset from the right or bottom. If offset is None, the + legend must be anchored manually by calling anchor() or + positioned by calling setPos(). + ============== =============================================================== """ @@ -61,14 +61,14 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): """ Add a new entry to the legend. - =========== ======================================================== - Arguments - item A PlotDataItem from which the line and point style - of the item will be determined or an instance of - ItemSample (or a subclass), allowing the item display - to be customized. - title The title to display for this item. Simple HTML allowed. - =========== ======================================================== + ============== ======================================================== + **Arguments:** + item A PlotDataItem from which the line and point style + of the item will be determined or an instance of + ItemSample (or a subclass), allowing the item display + to be customized. + title The title to display for this item. Simple HTML allowed. + ============== ======================================================== """ label = LabelItem(name) if isinstance(item, ItemSample): @@ -85,10 +85,10 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): """ Removes one item from the legend. - =========== ======================================================== - Arguments - title The title displayed for this item. - =========== ======================================================== + ============== ======================================================== + **Arguments:** + title The title displayed for this item. + ============== ======================================================== """ # Thanks, Ulrich! # cycle for a match diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py index 04524116..88322112 100644 --- a/pyqtgraph/graphicsItems/LinearRegionItem.py +++ b/pyqtgraph/graphicsItems/LinearRegionItem.py @@ -30,19 +30,19 @@ class LinearRegionItem(UIGraphicsItem): def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None): """Create a new LinearRegionItem. - ============= ===================================================================== + ============== ===================================================================== **Arguments:** - values A list of the positions of the lines in the region. These are not - limits; limits can be set by specifying bounds. - orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal. - If not specified it will be vertical. - brush Defines the brush that fills the region. Can be any arguments that - are valid for :func:`mkBrush `. Default is - transparent blue. - movable If True, the region and individual lines are movable by the user; if - False, they are static. - bounds Optional [min, max] bounding values for the region - ============= ===================================================================== + values A list of the positions of the lines in the region. These are not + limits; limits can be set by specifying bounds. + orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal. + If not specified it will be vertical. + brush Defines the brush that fills the region. Can be any arguments that + are valid for :func:`mkBrush `. Default is + transparent blue. + movable If True, the region and individual lines are movable by the user; if + False, they are static. + bounds Optional [min, max] bounding values for the region + ============= ===================================================================== """ UIGraphicsItem.__init__(self) @@ -89,10 +89,10 @@ class LinearRegionItem(UIGraphicsItem): def setRegion(self, rgn): """Set the values for the edges of the region. - ============= ============================================== + ============= ============================================== **Arguments:** - rgn A list or tuple of the lower and upper values. - ============= ============================================== + rgn A list or tuple of the lower and upper values. + ============= ============================================== """ if self.lines[0].value() == rgn[0] and self.lines[1].value() == rgn[1]: return diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 25a6433e..b0b0c8ac 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -291,18 +291,18 @@ class PlotDataItem(GraphicsObject): Set the downsampling mode of this item. Downsampling reduces the number of samples drawn to increase performance. - =========== ================================================================= - Arguments - ds (int) Reduce visible plot samples by this factor. To disable, - set ds=1. - auto (bool) If True, automatically pick *ds* based on visible range - mode 'subsample': Downsample by taking the first of N samples. - This method is fastest and least accurate. - 'mean': Downsample by taking the mean of N samples. - 'peak': Downsample by drawing a saw wave that follows the min - and max of the original data. This method produces the best - visual representation of the data but is slower. - =========== ================================================================= + ============== ================================================================= + **Arguments:** + ds (int) Reduce visible plot samples by this factor. To disable, + set ds=1. + auto (bool) If True, automatically pick *ds* based on visible range + mode 'subsample': Downsample by taking the first of N samples. + This method is fastest and least accurate. + 'mean': Downsample by taking the mean of N samples. + 'peak': Downsample by drawing a saw wave that follows the min + and max of the original data. This method produces the best + visual representation of the data but is slower. + ============== ================================================================= """ changed = False if ds is not None: diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index aab546b2..b035bce7 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -931,18 +931,18 @@ class PlotItem(GraphicsWidget): def setDownsampling(self, ds=None, auto=None, mode=None): """Change the default downsampling mode for all PlotDataItems managed by this plot. - =========== ================================================================= - Arguments - ds (int) Reduce visible plot samples by this factor, or - (bool) To enable/disable downsampling without changing the value. - auto (bool) If True, automatically pick *ds* based on visible range - mode 'subsample': Downsample by taking the first of N samples. - This method is fastest and least accurate. - 'mean': Downsample by taking the mean of N samples. - 'peak': Downsample by drawing a saw wave that follows the min - and max of the original data. This method produces the best - visual representation of the data but is slower. - =========== ================================================================= + ============== ================================================================= + **Arguments:** + ds (int) Reduce visible plot samples by this factor, or + (bool) To enable/disable downsampling without changing the value. + auto (bool) If True, automatically pick *ds* based on visible range + mode 'subsample': Downsample by taking the first of N samples. + This method is fastest and least accurate. + 'mean': Downsample by taking the mean of N samples. + 'peak': Downsample by drawing a saw wave that follows the min + and max of the original data. This method produces the best + visual representation of the data but is slower. + ============= ================================================================= """ if ds is not None: if ds is False: @@ -1113,15 +1113,15 @@ class PlotItem(GraphicsWidget): """ Set the label for an axis. Basic HTML formatting is allowed. - ============= ================================================================= + ============== ================================================================= **Arguments:** - axis must be one of 'left', 'bottom', 'right', or 'top' - text text to display along the axis. HTML allowed. - units units to display after the title. If units are given, - then an SI prefix will be automatically appended - and the axis values will be scaled accordingly. - (ie, use 'V' instead of 'mV'; 'm' will be added automatically) - ============= ================================================================= + axis must be one of 'left', 'bottom', 'right', or 'top' + text text to display along the axis. HTML allowed. + units units to display after the title. If units are given, + then an SI prefix will be automatically appended + and the axis values will be scaled accordingly. + (ie, use 'V' instead of 'mV'; 'm' will be added automatically) + ============== ================================================================= """ self.getAxis(axis).setLabel(text=text, units=units, **args) self.showAxis(axis) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 1c11fcf9..5310cf4d 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -253,13 +253,13 @@ class ScatterPlotItem(GraphicsObject): def setData(self, *args, **kargs): """ - **Ordered Arguments:** + **Ordered** * If there is only one unnamed argument, it will be interpreted like the 'spots' argument. * If there are two unnamed arguments, they will be interpreted as sequences of x and y values. ====================== =============================================================================================== - **Keyword Arguments:** + **Keyword** *spots* Optional list of dicts. Each dict specifies parameters for a single spot: {'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}. This is just an alternate method of passing in data for the corresponding arguments. diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index 61145594..1a681d78 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -9,18 +9,18 @@ class TextItem(UIGraphicsItem): """ def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0): """ - =========== ================================================================================= + ============== ================================================================================= **Arguments:** - *text* The text to display - *color* The color of the text (any format accepted by pg.mkColor) - *html* If specified, this overrides both *text* and *color* - *anchor* A QPointF or (x,y) sequence indicating what region of the text box will - be anchored to the item's position. A value of (0,0) sets the upper-left corner - of the text box to be at the position specified by setPos(), while a value of (1,1) - sets the lower-right corner. - *border* A pen to use when drawing the border - *fill* A brush to use when filling within the border - =========== ================================================================================= + *text* The text to display + *color* The color of the text (any format accepted by pg.mkColor) + *html* If specified, this overrides both *text* and *color* + *anchor* A QPointF or (x,y) sequence indicating what region of the text box will + be anchored to the item's position. A value of (0,0) sets the upper-left corner + of the text box to be at the position specified by setPos(), while a value of (1,1) + sets the lower-right corner. + *border* A pen to use when drawing the border + *fill* A brush to use when filling within the border + =========== ================================================================================= """ ## not working yet diff --git a/pyqtgraph/graphicsItems/VTickGroup.py b/pyqtgraph/graphicsItems/VTickGroup.py index 2bf6a7fd..20793a13 100644 --- a/pyqtgraph/graphicsItems/VTickGroup.py +++ b/pyqtgraph/graphicsItems/VTickGroup.py @@ -19,15 +19,15 @@ class VTickGroup(UIGraphicsItem): """ def __init__(self, xvals=None, yrange=None, pen=None): """ - ============= =================================================================== + ============== =================================================================== **Arguments:** - xvals A list of x values (in data coordinates) at which to draw ticks. - yrange A list of [low, high] limits for the tick. 0 is the bottom of - the view, 1 is the top. [0.8, 1] would draw ticks in the top - fifth of the view. - pen The pen to use for drawing ticks. Default is grey. Can be specified - as any argument valid for :func:`mkPen` - ============= =================================================================== + xvals A list of x values (in data coordinates) at which to draw ticks. + yrange A list of [low, high] limits for the tick. 0 is the bottom of + the view, 1 is the top. [0.8, 1] would draw ticks in the top + fifth of the view. + pen The pen to use for drawing ticks. Default is grey. Can be specified + as any argument valid for :func:`mkPen` + ============== =================================================================== """ if yrange is None: yrange = [0, 1] @@ -56,10 +56,10 @@ class VTickGroup(UIGraphicsItem): def setXVals(self, vals): """Set the x values for the ticks. - ============= ===================================================================== + ============= ===================================================================== **Arguments:** - vals A list of x values (in data/plot coordinates) at which to draw ticks. - ============= ===================================================================== + vals A list of x values (in data/plot coordinates) at which to draw ticks. + ============= ===================================================================== """ self.xvals = vals self.rebuildTicks() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 59cabfa7..9d0c3240 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -71,16 +71,16 @@ class ViewBox(GraphicsWidget): def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None): """ - ============= ============================================================= + ============== ============================================================= **Arguments:** - *parent* (QGraphicsWidget) Optional parent widget - *border* (QPen) Do draw a border around the view, give any - single argument accepted by :func:`mkPen ` - *lockAspect* (False or float) The aspect ratio to lock the view - coorinates to. (or False to allow the ratio to change) - *enableMouse* (bool) Whether mouse can be used to scale/pan the view - *invertY* (bool) See :func:`invertY ` - ============= ============================================================= + *parent* (QGraphicsWidget) Optional parent widget + *border* (QPen) Do draw a border around the view, give any + single argument accepted by :func:`mkPen ` + *lockAspect* (False or float) The aspect ratio to lock the view + coorinates to. (or False to allow the ratio to change) + *enableMouse* (bool) Whether mouse can be used to scale/pan the view + *invertY* (bool) See :func:`invertY ` + ============== ============================================================= """ @@ -562,14 +562,14 @@ class ViewBox(GraphicsWidget): Note that this is not the same as enableAutoRange, which causes the view to automatically auto-range whenever its contents are changed. - =========== ============================================================ - Arguments - padding The fraction of the total data range to add on to the final - visible range. By default, this value is set between 0.02 - and 0.1 depending on the size of the ViewBox. - items If specified, this is a list of items to consider when - determining the visible range. - =========== ============================================================ + ============== ============================================================ + **Arguments:** + padding The fraction of the total data range to add on to the final + visible range. By default, this value is set between 0.02 + and 0.1 depending on the size of the ViewBox. + items If specified, this is a list of items to consider when + determining the visible range. + ============== ============================================================ """ if item is None: bounds = self.childrenBoundingRect(items=items) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 393baa8b..a08b449c 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -38,25 +38,25 @@ class Process(RemoteEventHandler): def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None): """ - ============ ============================================================= + ============== ============================================================= **Arguments:** - name Optional name for this process used when printing messages - from the remote process. - target Optional function to call after starting remote process. - By default, this is startEventLoop(), which causes the remote - process to process requests from the parent process until it - is asked to quit. If you wish to specify a different target, - it must be picklable (bound methods are not). - copySysPath If True, copy the contents of sys.path to the remote process - debug If True, print detailed information about communication - with the child process. - wrapStdout If True (default on windows) then stdout and stderr from the - child process will be caught by the parent process and - forwarded to its stdout/stderr. This provides a workaround - for a python bug: http://bugs.python.org/issue3905 - but has the side effect that child output is significantly - delayed relative to the parent output. - ============ ============================================================= + name Optional name for this process used when printing messages + from the remote process. + target Optional function to call after starting remote process. + By default, this is startEventLoop(), which causes the remote + process to process requests from the parent process until it + is asked to quit. If you wish to specify a different target, + it must be picklable (bound methods are not). + copySysPath If True, copy the contents of sys.path to the remote process + debug If True, print detailed information about communication + with the child process. + wrapStdout If True (default on windows) then stdout and stderr from the + child process will be caught by the parent process and + forwarded to its stdout/stderr. This provides a workaround + for a python bug: http://bugs.python.org/issue3905 + but has the side effect that child output is significantly + delayed relative to the parent output. + ============== ============================================================= """ if target is None: target = startEventLoop diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 1021bcd6..70ce90a6 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -299,23 +299,23 @@ class RemoteEventHandler(object): (The docstring has information that is nevertheless useful to the programmer as it describes the internal protocol used to communicate between processes) - ========== ==================================================================== + ============== ==================================================================== **Arguments:** - request String describing the type of request being sent (see below) - reqId Integer uniquely linking a result back to the request that generated - it. (most requests leave this blank) - callSync 'sync': return the actual result of the request - 'async': return a Request object which can be used to look up the - result later - 'off': return no result - timeout Time in seconds to wait for a response when callSync=='sync' - opts Extra arguments sent to the remote process that determine the way - the request will be handled (see below) - returnType 'proxy', 'value', or 'auto' - byteData If specified, this is a list of objects to be sent as byte messages - to the remote process. - This is used to send large arrays without the cost of pickling. - ========== ==================================================================== + request String describing the type of request being sent (see below) + reqId Integer uniquely linking a result back to the request that generated + it. (most requests leave this blank) + callSync 'sync': return the actual result of the request + 'async': return a Request object which can be used to look up the + result later + 'off': return no result + timeout Time in seconds to wait for a response when callSync=='sync' + opts Extra arguments sent to the remote process that determine the way + the request will be handled (see below) + returnType 'proxy', 'value', or 'auto' + byteData If specified, this is a list of objects to be sent as byte messages + to the remote process. + This is used to send large arrays without the cost of pickling. + ============== ==================================================================== Description of request strings and options allowed for each: diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index e6888c16..bfad5625 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -23,18 +23,18 @@ class MeshData(object): def __init__(self, vertexes=None, faces=None, edges=None, vertexColors=None, faceColors=None): """ - ============= ===================================================== - Arguments - vertexes (Nv, 3) array of vertex coordinates. - If faces is not specified, then this will instead be - interpreted as (Nf, 3, 3) array of coordinates. - faces (Nf, 3) array of indexes into the vertex array. - edges [not available yet] - vertexColors (Nv, 4) array of vertex colors. - If faces is not specified, then this will instead be - interpreted as (Nf, 3, 4) array of colors. - faceColors (Nf, 4) array of face colors. - ============= ===================================================== + ============== ===================================================== + **Arguments:** + vertexes (Nv, 3) array of vertex coordinates. + If faces is not specified, then this will instead be + interpreted as (Nf, 3, 3) array of coordinates. + faces (Nf, 3) array of indexes into the vertex array. + edges [not available yet] + vertexColors (Nv, 4) array of vertex colors. + If faces is not specified, then this will instead be + interpreted as (Nf, 3, 4) array of colors. + faceColors (Nf, 4) array of face colors. + ============== ===================================================== All arguments are optional. """ diff --git a/pyqtgraph/opengl/items/GLMeshItem.py b/pyqtgraph/opengl/items/GLMeshItem.py index 14d178f8..c80fd488 100644 --- a/pyqtgraph/opengl/items/GLMeshItem.py +++ b/pyqtgraph/opengl/items/GLMeshItem.py @@ -19,7 +19,7 @@ class GLMeshItem(GLGraphicsItem): def __init__(self, **kwds): """ ============== ===================================================== - Arguments + **Arguments:** meshdata MeshData object from which to determine geometry for this item. color Default face color used if no vertex or face colors diff --git a/pyqtgraph/opengl/items/GLSurfacePlotItem.py b/pyqtgraph/opengl/items/GLSurfacePlotItem.py index 9c41a878..e39ef3bb 100644 --- a/pyqtgraph/opengl/items/GLSurfacePlotItem.py +++ b/pyqtgraph/opengl/items/GLSurfacePlotItem.py @@ -36,14 +36,14 @@ class GLSurfacePlotItem(GLMeshItem): """ Update the data in this surface plot. - ========== ===================================================================== - Arguments - x,y 1D arrays of values specifying the x,y positions of vertexes in the - grid. If these are omitted, then the values will be assumed to be - integers. - z 2D array of height values for each grid vertex. - colors (width, height, 4) array of vertex colors. - ========== ===================================================================== + ============== ===================================================================== + **Arguments:** + x,y 1D arrays of values specifying the x,y positions of vertexes in the + grid. If these are omitted, then the values will be assumed to be + integers. + z 2D array of height values for each grid vertex. + colors (width, height, 4) array of vertex colors. + ============== ===================================================================== All arguments are optional. diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index e8b051f7..c62432f2 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -108,7 +108,7 @@ class Parameter(QtCore.QObject): by most Parameter subclasses. ======================= ========================================================= - **Keyword Arguments:** + **Keyword** name The name to give this Parameter. This is the name that will appear in the left-most column of a ParameterTree for this Parameter. @@ -675,13 +675,13 @@ class Parameter(QtCore.QObject): """ Called when the state of any sub-parameter has changed. - ========== ================================================================ - Arguments: - param The immediate child whose tree state has changed. - note that the change may have originated from a grandchild. - changes List of tuples describing all changes that have been made - in this event: (param, changeDescr, data) - ========== ================================================================ + ============== ================================================================ + **Arguments:** + param The immediate child whose tree state has changed. + note that the change may have originated from a grandchild. + changes List of tuples describing all changes that have been made + in this event: (param, changeDescr, data) + ============== ================================================================ This function can be extended to react to tree state changes. """ diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index 1874f5d1..8cd72e15 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -86,14 +86,14 @@ class ColorMapParameter(ptree.types.GroupParameter): """ Return an array of colors corresponding to *data*. - ========= ================================================================= - Arguments - data A numpy record array where the fields in data.dtype match those - defined by a prior call to setFields(). - mode Either 'byte' or 'float'. For 'byte', the method returns an array - of dtype ubyte with values scaled 0-255. For 'float', colors are - returned as 0.0-1.0 float values. - ========= ================================================================= + ============== ================================================================= + **Arguments:** + data A numpy record array where the fields in data.dtype match those + defined by a prior call to setFields(). + mode Either 'byte' or 'float'. For 'byte', the method returns an array + of dtype ubyte with values scaled 0-255. For 'float', colors are + returned as 0.0-1.0 float values. + ============== ================================================================= """ colors = np.zeros((len(data),4)) for item in self.children(): diff --git a/pyqtgraph/widgets/ValueLabel.py b/pyqtgraph/widgets/ValueLabel.py index d395cd43..4e5b3011 100644 --- a/pyqtgraph/widgets/ValueLabel.py +++ b/pyqtgraph/widgets/ValueLabel.py @@ -16,18 +16,18 @@ class ValueLabel(QtGui.QLabel): def __init__(self, parent=None, suffix='', siPrefix=False, averageTime=0, formatStr=None): """ - ============ ================================================================================== - Arguments - suffix (str or None) The suffix to place after the value - siPrefix (bool) Whether to add an SI prefix to the units and display a scaled value - averageTime (float) The length of time in seconds to average values. If this value - is 0, then no averaging is performed. As this value increases - the display value will appear to change more slowly and smoothly. - formatStr (str) Optionally, provide a format string to use when displaying text. The text - will be generated by calling formatStr.format(value=, avgValue=, suffix=) - (see Python documentation on str.format) - This option is not compatible with siPrefix - ============ ================================================================================== + ============== ================================================================================== + **Arguments:** + suffix (str or None) The suffix to place after the value + siPrefix (bool) Whether to add an SI prefix to the units and display a scaled value + averageTime (float) The length of time in seconds to average values. If this value + is 0, then no averaging is performed. As this value increases + the display value will appear to change more slowly and smoothly. + formatStr (str) Optionally, provide a format string to use when displaying text. The text + will be generated by calling formatStr.format(value=, avgValue=, suffix=) + (see Python documentation on str.format) + This option is not compatible with siPrefix + ============== ================================================================================== """ QtGui.QLabel.__init__(self, parent) self.values = [] From 193367a56b0928c63d2f9e4ae6862e3204e1d756 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 7 Feb 2014 10:38:41 -0500 Subject: [PATCH 097/268] Signal cleanup: - Renamed GraphicsView signals to avoid collision with ViewBox signals that are wrapped in PlotWidget: sigRangeChanged => sigDeviceRangeChanged and sigTransformChanged => sigDeviceTransformChanged. - All signal disconnections that catch TypeError now also catch RuntimeError for pyside compatibility. --- CHANGELOG | 3 +++ pyqtgraph/flowchart/Flowchart.py | 19 +++++--------- pyqtgraph/graphicsItems/FillBetweenItem.py | 2 +- pyqtgraph/graphicsItems/GraphicsItem.py | 29 +++++++++++++--------- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- pyqtgraph/graphicsWindows.py | 7 ++++-- pyqtgraph/parametertree/Parameter.py | 2 +- pyqtgraph/widgets/GraphicsLayoutWidget.py | 20 ++++++++++++++- pyqtgraph/widgets/GraphicsView.py | 14 +++++------ pyqtgraph/widgets/PlotWidget.py | 4 ++- 10 files changed, 63 insertions(+), 39 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 81e384ee..ec558564 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,9 @@ pyqtgraph-0.9.9 [unreleased] - ImageItem is faster by avoiding makeQImage(transpose=True) - ComboBox will raise error when adding multiple items of the same name - ArrowItem.setStyle now updates style options rather than replacing them + - Renamed GraphicsView signals to avoid collision with ViewBox signals that + are wrapped in PlotWidget: sigRangeChanged => sigDeviceRangeChanged and + sigTransformChanged => sigDeviceTransformChanged. New Features: - Added ViewBox.setLimits() method diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 27586040..48357b30 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -227,18 +227,11 @@ class Flowchart(Node): def nodeClosed(self, node): del self._nodes[node.name()] self.widget().removeNode(node) - try: - node.sigClosed.disconnect(self.nodeClosed) - except TypeError: - pass - try: - node.sigRenamed.disconnect(self.nodeRenamed) - except TypeError: - pass - try: - node.sigOutputChanged.disconnect(self.nodeOutputChanged) - except TypeError: - pass + for signal in ['sigClosed', 'sigRenamed', 'sigOutputChanged']: + try: + getattr(node, signal).disconnect(self.nodeClosed) + except (TypeError, RuntimeError): + pass self.sigChartChanged.emit(self, 'remove', node) def nodeRenamed(self, node, oldName): @@ -769,7 +762,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): #self.disconnect(item.bypassBtn, QtCore.SIGNAL('clicked()'), self.bypassClicked) try: item.bypassBtn.clicked.disconnect(self.bypassClicked) - except TypeError: + except (TypeError, RuntimeError): pass self.ui.ctrlList.removeTopLevelItem(item) diff --git a/pyqtgraph/graphicsItems/FillBetweenItem.py b/pyqtgraph/graphicsItems/FillBetweenItem.py index 3cf33acd..d2ee393c 100644 --- a/pyqtgraph/graphicsItems/FillBetweenItem.py +++ b/pyqtgraph/graphicsItems/FillBetweenItem.py @@ -28,7 +28,7 @@ class FillBetweenItem(QtGui.QGraphicsPathItem): for c in self.curves: try: c.sigPlotChanged.disconnect(self.curveChanged) - except TypeError: + except (TypeError, RuntimeError): pass curves = [curve1, curve2] diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 8d2238b8..e34086bd 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -479,24 +479,29 @@ class GraphicsItem(object): ## disconnect from previous view if oldView is not None: - #print "disconnect:", self, oldView - try: - oldView.sigRangeChanged.disconnect(self.viewRangeChanged) - except TypeError: - pass - - try: - oldView.sigTransformChanged.disconnect(self.viewTransformChanged) - except TypeError: - pass + for signal, slot in [('sigRangeChanged', self.viewRangeChanged), + ('sigDeviceRangeChanged', self.viewRangeChanged), + ('sigTransformChanged', self.viewTransformChanged), + ('sigDeviceTransformChanged', self.viewTransformChanged)]: + try: + getattr(oldView, signal).disconnect(slot) + except (TypeError, AttributeError, RuntimeError): + # TypeError and RuntimeError are from pyqt and pyside, respectively + pass self._connectedView = None ## connect to new view if view is not None: #print "connect:", self, view - view.sigRangeChanged.connect(self.viewRangeChanged) - view.sigTransformChanged.connect(self.viewTransformChanged) + if hasattr(view, 'sigDeviceRangeChanged'): + # connect signals from GraphicsView + view.sigDeviceRangeChanged.connect(self.viewRangeChanged) + view.sigDeviceTransformChanged.connect(self.viewTransformChanged) + else: + # connect signals from ViewBox + view.sigRangeChanged.connect(self.viewRangeChanged) + view.sigTransformChanged.connect(self.viewTransformChanged) self._connectedView = weakref.ref(view) self.viewRangeChanged() self.viewTransformChanged() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 0fe6cd53..06e0ec1f 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -881,7 +881,7 @@ class ViewBox(GraphicsWidget): try: getattr(oldLink, signal).disconnect(slot) oldLink.sigResized.disconnect(slot) - except TypeError: + except (TypeError, RuntimeError): ## This can occur if the view has been deleted already pass diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index 6e7d6305..1aa3f3f4 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -19,11 +19,14 @@ def mkQApp(): class GraphicsWindow(GraphicsLayoutWidget): + """ + Convenience subclass of :class:`GraphicsLayoutWidget + `. This class is intended for use from + the interactive python prompt. + """ def __init__(self, title=None, size=(800,600), **kargs): mkQApp() - #self.win = QtGui.QMainWindow() GraphicsLayoutWidget.__init__(self, **kargs) - #self.win.setCentralWidget(self) self.resize(*size) if title is not None: self.setWindowTitle(title) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 45e46e55..11c81581 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -516,7 +516,7 @@ class Parameter(QtCore.QObject): self.sigChildRemoved.emit(self, child) try: child.sigTreeStateChanged.disconnect(self.treeStateChanged) - except TypeError: ## already disconnected + except (TypeError, RuntimeError): ## already disconnected pass def clearChildren(self): diff --git a/pyqtgraph/widgets/GraphicsLayoutWidget.py b/pyqtgraph/widgets/GraphicsLayoutWidget.py index 3c34ca58..ec7b9e0d 100644 --- a/pyqtgraph/widgets/GraphicsLayoutWidget.py +++ b/pyqtgraph/widgets/GraphicsLayoutWidget.py @@ -4,9 +4,27 @@ from .GraphicsView import GraphicsView __all__ = ['GraphicsLayoutWidget'] class GraphicsLayoutWidget(GraphicsView): + """ + Convenience class consisting of a :class:`GraphicsView + ` with a single :class:`GraphicsLayout + ` as its central item. + + This class wraps several methods from its internal GraphicsLayout: + :func:`nextRow ` + :func:`nextColumn ` + :func:`addPlot ` + :func:`addViewBox ` + :func:`addItem ` + :func:`getItem ` + :func:`addLabel ` + :func:`addLayout ` + :func:`removeItem ` + :func:`itemIndex ` + :func:`clear ` + """ def __init__(self, parent=None, **kargs): GraphicsView.__init__(self, parent) self.ci = GraphicsLayout(**kargs) - for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLabel', 'addLayout', 'addLabel', 'addViewBox', 'removeItem', 'itemIndex', 'clear']: + for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']: setattr(self, n, getattr(self.ci, n)) self.setCentralItem(self.ci) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index f3f4856a..8f811c3f 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -40,8 +40,8 @@ class GraphicsView(QtGui.QGraphicsView): The view can be panned using the middle mouse button and scaled using the right mouse button if enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality).""" - sigRangeChanged = QtCore.Signal(object, object) - sigTransformChanged = QtCore.Signal(object) + sigDeviceRangeChanged = QtCore.Signal(object, object) + sigDeviceTransformChanged = QtCore.Signal(object) sigMouseReleased = QtCore.Signal(object) sigSceneMouseMoved = QtCore.Signal(object) #sigRegionChanged = QtCore.Signal(object) @@ -219,8 +219,8 @@ class GraphicsView(QtGui.QGraphicsView): else: self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio) - self.sigRangeChanged.emit(self, self.range) - self.sigTransformChanged.emit(self) + self.sigDeviceRangeChanged.emit(self, self.range) + self.sigDeviceTransformChanged.emit(self) if propagate: for v in self.lockedViewports: @@ -287,7 +287,7 @@ class GraphicsView(QtGui.QGraphicsView): image.setPxMode(True) try: self.sigScaleChanged.disconnect(image.setScaledMode) - except TypeError: + except (TypeError, RuntimeError): pass tl = image.sceneBoundingRect().topLeft() w = self.size().width() * pxSize[0] @@ -368,14 +368,14 @@ class GraphicsView(QtGui.QGraphicsView): delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50)) scale = 1.01 ** delta self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos)) - self.sigRangeChanged.emit(self, self.range) + self.sigDeviceRangeChanged.emit(self, self.range) elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button. px = self.pixelSize() tr = -delta * px self.translate(tr[0], tr[1]) - self.sigRangeChanged.emit(self, self.range) + self.sigDeviceRangeChanged.emit(self, self.range) def pixelSize(self): """Return vector with the length and width of one view pixel in scene coordinates""" diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index 12176c74..e27bce60 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -12,7 +12,9 @@ from ..graphicsItems.PlotItem import * __all__ = ['PlotWidget'] class PlotWidget(GraphicsView): - #sigRangeChanged = QtCore.Signal(object, object) ## already defined in GraphicsView + # signals wrapped from PlotItem / ViewBox + sigRangeChanged = QtCore.Signal(object, object) + sigTransformChanged = QtCore.Signal(object) """ :class:`GraphicsView ` widget with a single From 71db5aea9151f3175d0ff374e9417b17fda274cc Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Sat, 8 Feb 2014 09:15:28 +0100 Subject: [PATCH 098/268] IDE files added to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b8e7af73..bd9cbb44 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ build MANIFEST deb_build dist +.idea +rtr.cvs From f1f097ec21b6bf89448409724f1ef0b8900a2b95 Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Sat, 8 Feb 2014 17:16:45 +0100 Subject: [PATCH 099/268] - Links in plot function added/resolved (PlotWidget.plot() can not resolved, maybe useless) -Links in "Organization of Plotting Classes" added/resolved - GraphicsLayoutItem changed to GraphicsLayout (svg/png picture also) graphicswindow.rst now useless (no link) Yes it "makes" .... --- doc/source/plotting.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/doc/source/plotting.rst b/doc/source/plotting.rst index ee9ed6dc..20956957 100644 --- a/doc/source/plotting.rst +++ b/doc/source/plotting.rst @@ -3,12 +3,12 @@ Plotting in pyqtgraph There are a few basic ways to plot data in pyqtgraph: -================================================================ ================================================== -:func:`pyqtgraph.plot` Create a new plot window showing your data -:func:`PlotWidget.plot() ` Add a new set of data to an existing plot widget -:func:`PlotItem.plot() ` Add a new set of data to an existing plot widget -:func:`GraphicsWindow.addPlot() ` Add a new plot to a grid of plots -================================================================ ================================================== +=================================================================== ================================================== +:func:`pyqtgraph.plot` Create a new plot window showing your data +:func:`PlotWidget.plot() ` Add a new set of data to an existing plot widget +:func:`PlotItem.plot() ` Add a new set of data to an existing plot widget +:func:`GraphicsLayout.addPlot() ` Add a new plot to a grid of plots +=================================================================== ================================================== All of these will accept the same basic arguments which control how the plot data is interpreted and displayed: @@ -31,17 +31,17 @@ Organization of Plotting Classes There are several classes invloved in displaying plot data. Most of these classes are instantiated automatically, but it is useful to understand how they are organized and relate to each other. Pyqtgraph is based heavily on Qt's GraphicsView framework--if you are not already familiar with this, it's worth reading about (but not essential). Most importantly: 1) Qt GUIs are composed of QWidgets, 2) A special widget called QGraphicsView is used for displaying complex graphics, and 3) QGraphicsItems define the objects that are displayed within a QGraphicsView. * Data Classes (all subclasses of QGraphicsItem) - * PlotCurveItem - Displays a plot line given x,y data - * ScatterPlotItem - Displays points given x,y data - * :class:`PlotDataItem ` - Combines PlotCurveItem and ScatterPlotItem. The plotting functions discussed above create objects of this type. + * :class:`PlotCurveItem ` - Displays a plot line given x,y data + * :class:`ScatterPlotItem ` - Displays points given x,y data + * :class:`PlotDataItem ` - Combines PlotCurveItem and ScatterPlotItem. The plotting functions discussed above create objects of this type. * Container Classes (subclasses of QGraphicsItem; contain other QGraphicsItem objects and must be viewed from within a GraphicsView) - * PlotItem - Contains a ViewBox for displaying data as well as AxisItems and labels for displaying the axes and title. This is a QGraphicsItem subclass and thus may only be used from within a GraphicsView - * GraphicsLayoutItem - QGraphicsItem subclass which displays a grid of items. This is used to display multiple PlotItems together. - * ViewBox - A QGraphicsItem subclass for displaying data. The user may scale/pan the contents of a ViewBox using the mouse. Typically all PlotData/PlotCurve/ScatterPlotItems are displayed from within a ViewBox. - * AxisItem - Displays axis values, ticks, and labels. Most commonly used with PlotItem. + * :class:`PlotItem ` - Contains a ViewBox for displaying data as well as AxisItems and labels for displaying the axes and title. This is a QGraphicsItem subclass and thus may only be used from within a GraphicsView + * :class:`GraphicsLayout ` - QGraphicsItem subclass which displays a grid of items. This is used to display multiple PlotItems together. + * :class:`ViewBox ` - A QGraphicsItem subclass for displaying data. The user may scale/pan the contents of a ViewBox using the mouse. Typically all PlotData/PlotCurve/ScatterPlotItems are displayed from within a ViewBox. + * :class:`AxisItem ` - Displays axis values, ticks, and labels. Most commonly used with PlotItem. * Container Classes (subclasses of QWidget; may be embedded in PyQt GUIs) - * PlotWidget - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget. - * GraphicsLayoutWidget - QWidget subclass displaying a single GraphicsLayoutItem. Most of the methods provided by GraphicsLayoutItem are also available through GraphicsLayoutWidget. + * :class:`PlotWidget ` - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget. + * :class:`GraphicsLayoutWidget ` - QWidget subclass displaying a single GraphicsLayoutItem. Most of the methods provided by GraphicsLayoutItem are also available through GraphicsLayoutWidget. .. image:: images/plottingClasses.png From f8c9406f84c6f5c9007c705e5f80b0d5997df6e7 Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Sat, 8 Feb 2014 17:27:15 +0100 Subject: [PATCH 100/268] Renaming of GraphicsLayoutItem to GraphicsLayout --- doc/source/images/plottingClasses.png | Bin 68667 -> 44314 bytes doc/source/images/plottingClasses.svg | 14 +++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/source/images/plottingClasses.png b/doc/source/images/plottingClasses.png index 7c8325a5bbe0809aa3b40ac3a8f76972b4d802a9..3f968f509c57cd8f8e42cf5e214893c13ffc9515 100644 GIT binary patch literal 44314 zcmX`T1z1$u_dh&{VgMptiZn=fNlBL=DM(33H%gZX2+}CsDkTEa9n#$h2-4l%@viya z|L^5_?sJuyIcJ}}*IJ)gYYcj!D2;iK6ySe1 z9mQnT?!b@79be93E_HS0t)@~y~&qzt3Bou`+_KWZ=aCdJn#2{|rx zhkQ><^AV%Q3HrXXGxU+tTT((o;;IV|?^GM{;)NQDml$Pa+>J9y*2~_)z`($E@5oru z)-0oEx34rF%iX(oFG7a859Pzx(QB_$_@G1`dX6ZrFlm4ad3}3flHnH_>MS3WX?+E3 zZ0yKwF{6g%A$yd)D&_uD9HQO-Odr^hfh^X3VT%Pg)pf9P|i>4b;f=H zuPtmZZBB?56>*?+-mqwQjzbqw36cHddI|{hDRT>$q1$~lI|Mbs&>(q}S zIwa>sx=KzA@exa0Nr~FpNNH5{l{rzJS{H+yiwn{2-9pU~TeS^6GtCF9tE(->49qtF z>Lk`Rl3yq*UuJsfJf3d)s)sr-Pq|k>!Ofkq^8!0NQ~@OuB}lX}%U$IYM&1RsS3%A0 z@iFaFxFS=A)r5nB*>QuY{>4SDtdi2V{7U?#cTCEvs;napC}>#g z-L3D=gD?N!q_ZlXa$Mrj%w=RN`oCP#C z+&ny(gxt8PI2e-TU-m{sCm+V{vC1`nbpHA3>2=~hHufp$3j~vofX@>S+plD4_vyW> zSWkPXh2DpRyfx`2Yxznu6nJ*>{>vBYht5AUrqi6LsavCeq}{r8%dZxlRzE?1Lk~{< zX9jXqT? zu<86FyD!(x%gOnny?w_vYP0F%F_S*G^w!^^Xb~e@kvsVK_zn&ZaZkM|MXotgc6QhX zhla+gy*X>oODP3t*AhEff}FR-+q=3LopdQ9ch={gn`K*iyh~x|WVjHteK;@>*|No~ zy~~+yV7uC%|5R6i|AHnySfyG+cbJ=t-`--HcbP`z$1tKh@?BCcs#JT;CkwoRAb+&4 z50xKNX%@aE%?kTB*D!@%QnKDqgkS7>xis=p_Lt}KtFM$z>^XX$hfDAT^()YiQ!0eW zIXO{B+noI^-xpj42D*H7O+Hgd*x`ImGU53yul~zFcisoPv$~P{p)c}Y z0;JTqe)!JE7@3*JoNKVJP9NWt{?+?0 zEKn;xs&6TIvLDZ7aR2-`DN8pof3ji~qrE2jcuRj-e8IgpN$94imlxTTa38p9=Z9&T~ma%9W4LA$*{O%n!$ZI619N^9%Y*v&VD7y_^*0a6OC3!7+#uuXwcc z_Otx);OOUrcM#x-UL|omOB+Mn={;WXeKKLu0qr{p{kh zw$kyKfjOakKImMWLf6nxu;YoWv8gFGDJip#_uSQk>ZQ&0&d#Wz3EOEjhl^%CGT|!q zpDDJ!7yR}PkNbqkK1UGIWhKtd8&$fjQqw7J5_*kBsYdbHTRgr(VXgFB=0L>eC#3`I zS7qMq-K?)5GcYI=b5^_(mu~XM?N5sZ<)t-;qSGkDgi{p?S#awu3NmJ6% zeI(^&by%7-p!_CG+M%Tt59z!BXMR$1Aj%M}k}gJLl~^)2m!aygcoXdm2kQBj?s-%k zk&;K_a*?n26mnydSRN8iaqEk)@+Z!$auQzXxuP$_IpUBIG zx_bUR%5BEa^m6Pv2kTLz?o>3 zdORf|Ne~lmQ`|6E!O|VCb7V%iA6Xmj?64fHP6yD)n?6W=fZlLx`SZ`|ia7n)zOT{+ zCh!Vc0ilw=8x;8>iyqbk(yEo#eg2GVAL>kAA@KTrR`BJ%F1v$oxS~l!_*&N-y1A^scE*`28g%i<@ka*@WcbQ zueIIyxQt*eF6V-DA|m0!mlJnBb@XlD0@%1W^{@XIdBF7;Dil=O=SfWjZPE3|azTL? zs`QA~cEOMM5w2L=DC)+zhgWP~3t#u|wuYtXF<}^;4d22bV!8XB6gwAQx`$L_UOOfo>~ET-Ni&V-0ANR>_xR1}xgLm-8lOWl<3(N8y9X;u?Mo)2Y4E-aC%d^6N znfK$NqgzgYZM>lJS|5X_#ASS}Y^pEJyQEUvjy z)6sm*G8NTzWQ00=o?Nc9@HmZxgQ^fMmF|fpRbtIoKLyjPN(UzD6>Fc@YIsqxv2Acb z#DcH!KA>H-C-Po}5Km)qa-wG{B(z0aE^Ah~aGID9$Yh*u{4I`*A0qsmn%aiGZOWuq z@g6oiDj~r)Gm}7Rl58+f-WD;3YuM4QAkENYIy+~M@tEiRGgPmIsmt{wA?LWM`{M0A zJ#TggQ;D8h2wa|rHk|G~x&QF5lzjDlxP14SE7ScWRALS@3}t0(jE94fRZAKQM7+Vd zqrJsuzulPDwB~151C;$c%`7ZXW|o&FYpXO?)=3F0Z_A^T&hiNfMO*SFX_mYe`lAtg z_pT?hPHjwklBuh!duj3c2lr+XaXZ|DD$A!U_&X)pRO_UR#A$Q99T2|KZVQ{7)MC8s zlzxBd=fceFX3|q{ykNIK-!n3z;;7MM9VRPdJaWDiXKh6#M0L;1>i#OSXx@DK@&#jV z?<#J22Rzol0LdEn$G-OJxK+;EZT`V@ekW2&tj>c&gJz>;q~!Yq%gf9AyYsibQh382 zX@?uV8Hqc`vSD-Y)y&s($TDo{baAU3O4oHxsSpuWdYNk4r<w5kWc}R`Cr+`za)g>zI+jNki+3tus|OzDlTsC z=)msf<}L-rL@gi?72Q=f(D36f;tyuRPSeg^VA(@Z^aY&EMFC6~J39Wx&)RYg=lTeCZqrq$YbpObjTuLf+zTalLQo+fIfb>siz*hIVCJyd-B-Z08 z+(UL>Wf4V5-RB?jb=`0uKR(aG{9a(%o_TmX$3O)g;e7m2(7sv;PTI^OdVUvo+`fMT zF*u}ZZZh!+KX+;HyFEHVul{XO|Gnt3xeDK@+JFli{wjepUlxX+sn1chQv_W_4=uRu z9lb?4CwQP>?KE$q2vr}q^;}Z${3&aB+j(mlcZ}P%?I5RUc5P&xU}lpUy{&S^ech|K2f9mmMEZ70=@0;!;+7LCsGS^#0QOyX1ZuVmNog2ZEZ7jb7i) zxQ`xn%%a93er20JH#X)A3fjGSv(C6Hvk92qH|{=WWPFVMpoBVx6IqkZlHE?|!O$%4>LYPtI3gCN!O zfq_3XbiB8flz7$DyZX~Toh!L)KcEJMg*E^7BENU9t2yCUaw1f7 zN!RMmMkV7nDaB`$kuRFQ$FTYRtNbizUXBylZLC1&72{ zRl>CV%=c9ZXuJh(Uw{4#`C&mUSUc&uvqSqlv1B2(GS%JQ(;f2-pnP}2E@MK%E1kE( z?>+laTIS-&L@JdQ<HsBQ0U(ZNRz&lY7U2HW1Fce ziPR}^*Q*Qm_1+_KRwQ$t1#jQ7eS&;;%+(@#GT`6C-JMl?`B$ig5Pfc!Mb%zQmkoe zurScMXL2&CqJpTXo+GnTGT5m?xUOsF**AM2)PVky$$)2=Z1Zf zQUOCy2xcdXQc7Qwdj)#1(j`W#8{N=9M=6>e1eIaZ9pma9Ef4Er<#EjDIZ@$Pe#Angv|Bw|+YQCBSDAU~5W@xNN zEV@2_iIyGZ9}+U1&osxoE|N-ak&bwf<+y%CL-CMFK;RR{n@&Ts7tb=c z#iwRgPyGUTdisa^=Q0YAe38rg?>*QfgaVZ5Z^4w;_qZZfYipF+-q7b0p6a3&s+dRvwYS zW9^@Kj{4=x7i7qTUUcY#lvl}A(0cpMn3Pn88YhII(}c&=_uM z_Y`P1MWv;&glgyXenb7L93M0(UB9G_Q4}k+;9wnh45xzwdf-+s>HZmT`GRX*TETKS zQ-#I|us)LDF3;$|0B#tg`pW~DQy)l^p6X7Hq{iVX$;lP5 z-9+jCUie7vlN^`rpMZVEHqdy~psr$=|9Y!Tox0R}^hQaN`kOM4s8h@}Jsl9mJLRQ# zmbs26;`xarz?;wa>({ToTGr@#CxYMPqAd^?T-Fqz=mOOel$tVI#r|^qc;jcV0>_uB z@VK>)GnT`+pQ!W8lMi{xIXY9T#O96mdU`B!a;YQs1M^WY)YPIl)%-rmQ8Q^(ygxb7 zBonOZ=+#}Xa9qB}7{ebE(PgSpkQgp|_zL;D!{S>aq5N>W0_k$Ejj7_u@855N(bk>6 znI+tjCZng9AHhOLcN^$G9)56Fin{Mle}bNw zT9{v1ncAf!k86_mEiaFeaQUU0;+r>bev)#f!yUZGXzL}R;s?87?i{&>jnjRwxfqKN zdHfVNZR}W^XhQeJi-v=EDq>zITGZPi5`fb&*6ZyV%ps>Yfs{ojJ}UTr(DpeB4D(fXHtDBEjwZ*7KdEj#g844-in7$DCg?+S{{(WPjpPu(M#8QM)4a>+?$pA3gP z2<$qSzQ~Pvw5S&t#m1H`YGuXsB&VZML|mqw^l#a!j^P;q-tOc)JNDzL-F=LB>E1F~ zd9W?~K3NcNyeeTl3JD7%15PI)Ev-`LAqiUK&RthjA9GC3s3=_jy$qEnkA+)2h)C4P z0sqE1y(N{7!(+GDIev5(FXuN_X82pzXUQm5BME>o z{AcEfi>2+iE|k59cu;)mYCqa9jIls&nwuGE&ka5U;*p*GqsXl#45zENqh|?ph^7{g zn6h#ZT*H2|^;SrzTM5OYpTE4N4AsG??qHu)uC8mX&#wy)~FxPkqLL?At?hg|*BLjG|8b~g9x5QnUWebO z)VsUvZ#Vis_xF>(D&3F)^vTi9W!WcCJZq}?KIj3~*6Xt`x4tlDHMNC5-f>@KcA4pn z9{Sx2lDIrC&9H?*?FaSxWJ0{w%=)oddmE@)B;bGlF46Kyeeqynp0aQWCrG0Ab*|1h z(YJfid`=$WGkl2neGv7YMC#d0U$nmbP=9{s&WRv)>vG0NHsuyLfvc-jI)#b~uM`Ze zw(h9@U*%$1A`A?^$Fu2bxh9|d`ii2Ts1m@{K)*6GVGd`FzWDP~Uvyrohdt+NoPP(- zpr%HU`d$5nfv9KScsDSgAP+uX>C9zkpeoQL71CLdcH^QH`=I&mW(sp{w+PORI zOk3ThHrR>cva&(zysS76AIfa&iSHbH%bq*^vs>Z61Nx#hB^y{NGr7bG1 zh37$*UUJkp7Nb9;jn()b{XIq)e^%{W>M~?Y94}is7#FX>yxUw7Kx|>Lrc>pS06<8q zc%s!#N4;>E`WKgS8vkxlSdm!dyrJ7}cy*4q80P0!!#~V>e~X8OJYXu8g_R7z%wq>jW?M>GY1Gv9*ct;Eu8*ll|v{&4!MMcGS_LWVa-b{RoRxUKeXL!l8 zv=Sr~@=!Qv$P_F6=l773G@qh3BiRNj!L{eSoMsK-Iu$M|E43sqxqlBf1_-(HvO9Gv zfzP(Lz8C-VRavrJ^pCIU>9XMNA)5@}-Ix`xT8ApEItY-qFxZKOHvN9X!+1bp_&luW za9evA3We0v)Lb8v$(fj-6UIZ5gFSDT6%FlDGcYi46)Qs>X3??+aLSeW?{Bzs1yGq-zl`HsDx&y%W@&*+_}1h`1tBz zWrbP9bC|^r{r=%ab@X_$wT8baNT)AFgm$Ngv9U>9Swn>&q=PkGy(=t9*gX(BA4W$1 zzR=lVjTNo3fo@Sq;P8W%fWhk*WWbIw9t-HX^!8k$e9rU=!-l;wUeBaLy2}M!riOZ1 z!oUg+R=NEcU-k|5w-S6_D_z-S^DD$X>u{y~h(+9&ZC@MD_3n;a6`G(!CG(`M$wWeO z^`AofeU&M++o1KyF+%@v>6fVy z5u?qmEx+t9y$;@Deroa1;LLxYo{qgwpdePAbib2l)e+n?4ho_A1pzxddP>i6LL+S{lQ(Eq~zA#PU3#k~ zaL?J(uJ3?C`4V>>vKk!6@6L6FQfFpiws#QEI8oj~OQkhp+ugU{3P*$oZMuw4>On!5 zfiWih{B7M6Go!GQ!*@E* zVE>uIc{ds+XqV-sf%t?QO1@5G)qeOA+!KXZ#abCT#pbGKxK@dse^La%uHK&d*k90R z=d}H=Ax?Ry5YF<%PZKHWL>uIx4=ERJb9s}-<8nz-G3Bpx9~r9IbQx)KHSfaFJ6nF+ zF>uW%yH-fiIB(AiU!bA zQ-kxV^K#X55%~ry7j&u)fA8gL(svTw868>)o?n4}Jx%}vG_;mx(yf1LF13RKMQ%p( zPSC=_!Y|WlDS(VLxkZZu2O@Uy*8h>B;63ZP0k!-0F&`^nR%!9G5*4#7{C7Z;HYch9 zsn=uoy!x{MYJyU@2C4?J<$9)==|IyEX6{+2pMlA>Ppn$LRY*V;8hD`w;2{8o0JBf8 z?tq+%^8>JcKglFeq|ne+^6Ff|`t;%Q9S*F>(08h$Cf~u!P$|gA#|gS2 z%qqmjo)eTGCJg&gQC6kGV<~*>;6wie#EQ5&O)HbX;E+&<`~p*bXDymnJ@k2n{XQl1gKjNLDk>h)RTdYPltgN;tsf@7gnsEj zeb;Did1T9_%jWujvjDyqrzh-DYa=+MyJc{LvfbAE-^=n1xzNLg3Ybt+BnQK>F#ejv)CWV zEu1SG(?ts!qrYiGHE4hP6)W|fAx?z)*Y_v-bI|*U8!i0qar(*F#H7D47n3(%_?n^b zVSCIXw4ia_N-}zKIB&b|&ONcz*gVtTyUv0xFUI5XWcF{FLq8T%Z?^;oUi|w~_FnT9 zUMtH~^`VLl$7FpZz#O!sv<}waCh(YRb)z2Iwm>Z`T_-GZTT}$wTC2pEgepMkt92sl z<5pJVi|_9bHrPc2pR!t8Z7T3PpQXHhed#?P>;IX(!00q~bRYkTP~9KVi@t5oedu-< z;+_`+N_agSLO)MIt^3P&lZe*@txqVWo%!I&-eMBJa~vqjpw{%j_EgIB1ba+8Jp60( zXIEb$`pGk|xRR=t4zE>3b_foCuKvxy0(df-I8w(T;Y)~ddUU0thwiNvT)Zz^nGq_7 zJO6{$TL62pz7tYeMoIq7PQ!qAispiToA=a*I~;^ZiRD+Ce~M!g2QEr4ru4cqeuLiw zje=|S^RyV+IlXSmYo4n-G702JhPQ9QA-UHOb+k9Sn;Y8Lnx{L$Y5;RyKJkb857kn+ z!+Pdx?}Wx%2FM`|__;d2SpFFI|NbH&Ax8)z5H9n-pGa}82mk*o3-r-M&Ct!<8>>Az zvZjQe4B65jncXKP*~lppn7)wCAqu20u+7#kC%AwAenw<6a(;vT1}ZB5+m$g9<^bfI zu5V`W1dn%jhx8`bd*sr?Z6XVvX=`i$ccVJoYu1=bTlD8qy zX`QN_5Qz2_dDSO-b1kZ6Hu~>FLwiJ+KOrw{l&@Re`C0mzgS$~I?brA8@G+In60<2^ zPE)1pAw&4og@}}TY4>!(wX^P$Ny{hv?&PPjm5$3rPMfbTE-w7s_m_H%M)P&OPduPK zQ(0MwsBzi#ZTg6XT7G2ZPe*I70aJKS?%;$+;^YVK9?&8Mr+L9sSnbr7S4+oFcSj669m*dXNZ`)q^|5mmz-Q)b& zaBDaRS3OtbR+{5-uco$kvDG+7C6}A`N?5)wjES-+Cq z-QA0m`EY7#>et4`%@?Qp{B8&I(qRvuF+}e*VTrJLoY>*u;>HWP^OTj9Jrs0_XbmOh ze7B}%(jAA=5Je;ZtE8kE);Kfs8V!pKqhX=rbNo|hmie}D!p@EH;;h#`5S72YF5NIN zF=gfDjTSqjH~y9aVwnz)j1aRM-uzeXybSMVdR1}T6OPn%b--m7P}>EpoQ z;2iAh+wG}jI5|%5D-XDnW}OFrYisLI-Rc;(!!`D|D=&~UAOYp#|F`cAB2}N|BqVOa zzDvrk7AGe}M@vFr67yIQmD^5BfzC1?${fhoeM-V@(cI{dzB*M~9igYB6h81xdgz-p z))GWtt^2WNwG;F0`wwS!W}AL$m)|EOB;4EI=eM7`i4)`p@#MJqmm8kw_twAa!qU=~ zfdOpfoLT~}?R`-_Ju(&+77P+D8ATgZRMY?r;v1rhiuc>XI%1~cxGf2O|NhO#&u@Qr zV9sOxw|T742#PozFK;X)%jf85|IeRsVJG4FG}`y!v5^NX` z%KP{4r_V-$0|W6W1OyU({`{%ZcMQ8$<9%H>b_kie5Ajvd%L=lorlw}_!Db?04}_Vt z%>kUS57uMh3uX@3q}Y&<5JOmBeQzSKEo3&l8Am#-?iLmn*5to`^4v<1*SdAp@A({; z6n^LOyy^U6I3*59tER4QZ)YbeF8(2sO8VYZ73=I^hP_Dh5{4KQvC-J+= z6Y&6yXVTKQA(3B|Sp~t;Hz&&5CbY|#{0j6PA>gx?Y^HGi#TDCG-($g#Fs^69ol@1&rRP3Xf9` zSc8k38%X?+%1VA-+bIdk$h2?Y#Fu-MpXk>KnwXfp-5A4$ocLR2%~*ozlaWDFS68Qh z;R)6Eivsv-VPRov1$vL*T$z}e3kwS)gL|$toVF%A)<+A1b^AIy&*afoAz`$By?$mM zcr?QI^eJ?Bn3HTL%0hj7z@EqN=Z@-KtDVy z%d{{ED<~+4h?w~9GCqUa$&Xkee*XC8W{H~G+TEk0*2YF(s1=B`^z`>Xepm@qfciT- zJ40k=XOl9EMg!$Zr(g@=Ih(i;A_?__uWE;>3DEiIbQCw#_xO;b=P z$HvAU5D^8Yq>%mi@k4BCZEelBTn&;J0_~0iDG?DOEjwHD=SyM&f`Ih&bUT(v8Nag0 zs&|a}`T4^I`lSJ~F0N1hUrl9vYjNWkp<{oyvh z#|hik)>cJzH6R=5Qg?jQciGQ3m6F55vFz>beP7Z;#6$Sc&(2aTKwCOIHkMFJOH0t} zqC&!Rb$*`ets08OaQ3sF1aANR;bAp0AvbE%6&MW9ZRaM--v_KM!jOKBKFzVZ&hM7&2Z4zb? zVq-svKU^Oz_B?m$jAdojS)Z;Kxja9FH_6 zW04dHoX?Sw&j43fCdyeMV_r{I*drNLy7(_417S)2B~KBHmeaOjq0&2NCp7Wzd z-mCPlUmM4ZO_3xE@+RaVKaQuT=fg*j78}c95Ie^E--K$llQkL!ktoni^XWRFwy;M= zmuH7Q-7XWg?&WNS-yQ(J1rFHo9`!a6w*?-ArNKgbq*|^`dBaA(N9j7nhg7O`akz zH&x?GL`1~qeq;pz%;&uI5E#fcWjrt+Vono`JnizsBg4H9FZb(UFpwiK_sHg~t?=nzFL6@DGU}O5`>1ll;Ux?>ZORy29 z>O6y^qoeJZ>FH%w+BJTt-GxD{sap3oIHba&B1#I1_wUyK4CJWNAriPP1A>FmF-du- zA?P|j(-|2ViGRHR2$*f?YjQ}=Sjk`l7{v;WjEvM-CV1TxfO&Fynx2y*o5*8*2OWJL zICqKFcobY2l>>D>T;}=nH;~{+H4x!SuMboZsHB~p-D`*junO#E8*f3FYSy}OgHH0b zvHe%YZQ7fZ4k#Hw}FT)^(4NXm@Va7T8RVf1$aiDX{ zSoL5xiOf}#1QN37M65FtzS&#o_`=3IAsNB_nsM<@ zsP*!IMgT(4wXs`h$Hu|Iv2%132QF4>IeM>;gqYZVZ$Xw~TG;dKMZK3NYy;6#CniWN zz|3#ZF^A>zx$Ka`@VN<)QZq|S)L*83WELYoZuul6kig|pU(YoMHAb8iz3B=J3YuN) ziv6imslrC`)M+CSR2t;8Z`Qk&{`AJEXCfCnsDRpH9zeC|VD&JDRRoL}=wXglab}Y9 z>#i7PBsYq1WknaNOG!zAHuhEeZB6*PM4$4D7Y{Uq>4#~CGM|GE`2cki(tCBRkP1m5 zJ}St|L+0rH^1k+hx_{^X{Us>5VqX4?L~wXr?;H}j%y0Mr8#FgJcfjQXLH-0NCF}~~ z?dD(n2x1p1>9FSE;V-}$FIOG3;RR&rl$Mr;3=0eoM=Gfji;;UE1b{t47q@wEFzOyY z!-EG88de6r`7fh4Ie7JnUN8e4H9R|5^jOiavt zz1mKo=`dPFK}97V_>zmA9eb{~r>BP>NONJ?FtpQncr+9<;ru#@FQ}wK5Hne+UWNxN z0~?c-xVLZLrd3M0`1o%B_;?uf;JXi&*7aB^^K2K==Dj7UhV*WWo>gCJwX8*5UU!G9Whkkwc=}lwX(JbdD{p4Yz&mWZK5cgI&1%TSzItRS}W}55q@E_zYLoExZu3$85nNZ zU0t{WG3EC>8Oe2}*236if_^ z3WvpeK>i@h2fj+88%X*`@!8EFfQdK$Es;E2hdRaQuyDu1!Xku(8#dR7F!T*@_DsE6 zH@Iyl@h^6Eb_WVUe=~FQyM>{!@Wx-Z#U?#Ss=>ygq86Kzn~RR57E9Mf z)T^Ht)1T>RXe8MjBqgpGu2(pCuQg~DlL8@1bqWr&VJPB3fX7uZ5|e>WRK6nsYv4+N z@nya-0P{m3H)MSj_PS`_oo};uc4i%rzn*Qn4KgQEh8hUV2f*0D!9j#p+g^XVIB;7> z;07BT8+9^h%*@Q)5SYNBA#E^-*nH3lS$WCY=Xu?L0Hx9doW;M_93GJ0{fKE`VBqG>n=^BBVp)pGNbLu4OifD* zg{cuD(~rWQo15Ff@b#=5zCV(fD1VazhY3?fRn_%)>k*(0rI3&=QO>EgHc&mvXu0uf zXV%WnPTv}!(@9UA!@9c_SoG^ITCB$boTuu&`#_P)%E+XF{Jx2X#$ow~Nk>-~%w-0! z2UyrAbIkr#Ic;_VWs^}*pcWQRfj>c0?Ua+`AKvsaUk`8bO)f1TSBNASlkf^9Awgd#y@PXk1 zAe_v$Q#A&_Xdrh#_4mK%jA53Ca=!v56MUPh(YO?Rm#!A8aws>@Y8v^W4)KO!jQi{! z-O~8qzZ`ZmqVR@~xw(wMa}*M|f}SVxC}%1V|4`4B0#61C#slG(1$u9QVEOs^!HM36 z5;j(5O$wxwMp&2(HW!qse7Vg(KcZFG+ksNcmu|;fJs{Jxz{SEO;X>;4Tj&J+#imLG zta{xwuKRx2QK?>ML9Y8t$ddpPyb$BR+MkXJVf%Z097buhIouA+=H}*Ht}gfi0J1<{ z00|eB5i#w3xHVm`1b1WO;xYyq1%6Z@wAe{_tYlulCI?pD`1fzZWA6*>h)^H=e2sLd zI3adWERD>}0ztU|zRbhQ4z{OLQ@yW<(b3VpE_S7%-bjL;8wW)Q-Un(L@INu>*TB{w zr)O7I+5xqH{rZ*We!LYz#O@2omX?zf1aOixFbegAf~gCHPFytbNNI3+z!JPDBUD@P zCc9f?u!Q9^ySdKVKg4;mrl66REM2_xM@#{vd!UT@{MJ_Q$?j+ViqX?PTvTA6B^IVE zNjG*Ip3dOUV4k3Ezn36Kr`K8gfBmAjYxE-B$25~Qiy^Vc;xKV?InsB958%WGK_KZ7A-$k_)KWOQJ3lyFr3 z982}@?qEj8%r>XSq*KY_}4Sc-?e%w`7%Y#$z#_Qn%F@R~{l#=4& z^QlmW7WAJ1H-@cuC>Hw9M!(9!kch~0bpgZD`xORw^|w6F4obaSjSz1O$3t-jG!GVOlJo>wL?^?qi$#4EG z4GIY{2L6fg`}h%kLeLY7MW>M< z;ek}oV159Rz#tRU95Y#Sw>#M}K(bp64HK}$xva(>_?Y&k*dMGY19Si@ycf;*vbCuR z6)+BLJEQ^yf=aVg304MxO`yO3EvV*8?U7WlmKVh)&yfyl1Nd44LqqdWBakVbmPQT_ zD+w3}BK342(4L-|o&62wWrg>(FpTp0lvz*UfD_Shb$Jf9Bt)zOEbVA}IvnUF zRDO&{0(h2|mf+(10?`LwKKsWH4v@jXl2OJg?5QXyDB|Md*Ow)MuoJwj?P06mKzi0;UOUy9Gslv01*)Ocmdf)hK7M*VJ1*uxojrm zK*--HGT2BFa-UmS0h19OcntUqk#TW=ml$Aym;%XwBlz;vd242E4f!1x*jS{h0Vl3w zF@?Z`zW@PMKu!x zEE^`RBEO9dD_{Vula{cpfZ@=*aoiYV2O9$(!eF-1AMySB_Z{%LMVOnw7Xe9u1S{}j zAgT^F#y=fsbRms9D)7^ zF-T_sP6enVpn!2xEQhnP5n!0S;1|v#?=DD${?XB8kb^-LjK8I~$-^6v#SyPcEj)+rvx6zEQ|%NAr6B?Y zHBt$t7h2o|o4~+jUJ}`KmvLd&f%t;CkXq zPq2aWlt_N~wbgGN?Tt`FbSI{4Yz_KANBWRzYzu9xT|( z%G0L>$xTR*{rT+$J&3xt&dyIjo_3-00{(X&Bv3qPFl~KXYwMmsY^v`RsL(2dUcJ}O z6v}_F?P6->OMb^UVnV_T4@21w%thhUiZ#D`sBTm(Io<#GpdxqvgcJ#ZT9A3qbW?ftw3K00+Vi$U=t8X<-ID+)+(m8 zo+x`W)9@Zd;|ClXIlGG!`=5Dv(=wU3xVXv;(a_gGnvrS@_|T+8AO!-*`9k+&8_>4G zL?0l>_m}&Y=URewYTZB#;=!ve;7+alEoFqlO-D<+lza>s>j*A9A<~@#uR23MHWLW; z;~ExGP0hIBB6__0kk{1YODlYjLrdcrPphE#RO(Y$A zDL76bl;!?4!rQ^4^+#F&V7v;vT@fv{7~unL2%NJCD5l8u1uhhjy7R54GwP>_;ECGr z%sd0B4yE8OI3ZFQDkw9Hi?o1Q;A)mj)DFHV{ z^MRJ39TXy@IshUsmk|XpC_C!p==jaJBMK@b7`)->=@iJ)2)X-(Nh=CIk(QMu4c!3k zGOKhDAaA!OwKk_nxy%}185O{Q0DUvyqy}rtt;TWu#dd$YhlBq```N(6G4~q_!uYBW%-slz?%bF);pN=ig?@R7^+XKFNlO7dxFQTy{Ep`|ZN3h=u?7xnlAtS& zi@Hp$*X5Z?;UNreaJ+8t376Z!g3d80<0)+ny?#Z|>JPzu3yn?!6FD6EkBk~WUuX*) zs)8Pjh=}mfVabAJfzMVe+EawTrx7Xl0A*uyQ>)x2IkV}3-;H$OM+x_zxrBRPoqmMY z^kD8wR;YiP(5nFrVbJJz7kF)bx!zyohz9ULaP*K^yVR5TIVq{z`)dEONnc8GxHH89 z4cLr8PQ?NB!5+D>V^NxgY=T>k=Cht@6=)X1xAfdW=+Wnu&`gO0-&bCedT=uxQCwcW ze7;?u1;zbhS>!rW<5nk(ZHWPi0+S9&6`(yA2QR`L$6YYPLd4P|!jkzNX(9KGp|-#r za-qkm1B@hygC4%VI%j2wzGrJ|3o2Er-s^G>S#sPZNW1>Ev)nz;r^UI|3ZtLE~QNi1x!60@5r7 zLjk^Aa^p2DB7{sR;fZRd-Qk)#e2)@2j{)2=Ta5wvHeghH`xE2~9FgSre96sFGE_R? zXmd33NI2pZ{(1qoMea7tXTY{mLgfT_F*G;t0GcuqlePi$qqnygS?i$T1G(J_b^6K6 z{D)9BA=!~$F0#fUi4u&KfSQNiTO1^wgL+V#pbg5MoYb4<7Ot*#-lO`~sW_D0&(3wt*3hpL7U|Avt&eWkX7e6alMFPM1?h zjv%96{lH$En`?w-j>uX~mQpH|V|&;;;H5AIY6_GQVDUw*+dBkwPO??A?gKKzZsOf* zA+=0FPcQ{+MfL(QF*AT`B5ia)lh096H+%OUU*k?D`&H!9dzGR1gwRl)v7Ddp0*WBU?UKC-Yb! zN@!mtp`%2STNHvjY-DLE4~R46+6{tGv(y3~nhW0RSCC2u&;tPYa-^Y!gN7CWvY8qX z5PmM+NDyNN1>ELt_~Z9bB_zm^1-01x^y~ zYAIkOFhVq_me8K=hD@M`Edgl~9R`Z00jAl&cY{cJqF2KQUGCUd6t_01ua+%yKsZB( z@9K9E644dn*k+T@z0Ws!ZKmrm8^F%3oj zd9I*P;kutWyct)_EFh48oL%F#Yz2gNfe8%<=%yoG_9sj63M z(A+u&k_GJygN`Vg$y)bVaFLa3T~J3^n2n@Cu|MxtQ&{_mJftk*Mswk2Pn&O-t4G!LS`gDIna?sc9zyrsIR387cvr`XnB!yLqD1z#C zi2o6BpKR&?^~9p5gW@4BP2wFFv4d`VHr=qCviOk(-#CnwcX?Mt%!9-hAu@b!P$_%_ zRkYRb)`?&=e+3;Qo}*CEF|v66EU-{_$n)mR3S$|(>=U@)Yl(@M!7&o>6*fr^YYwy8 zQ4@ceD7%BZGdTEyofQR&Rm>*n0P!FQdmrF};gh~qSIYPA-zE9?HF_?Y$5M^I4x;?g z%JFmvK47;Z!nR$`j{p2VO#wrYpd6?^1(*iSNp$Tu-WxqIcYj0%1xNU=ukW6)b&+va zu|V_BOg_p=-dy#hcS}pN%kj~R@9%Ekh{bF#>SV8 z2FBNT{{2}bA)YH>o7CyUp%7pdYydFM1uzJX8RpNoyTV6^!;6(Z+m ztAy{5x;BqkI!N1H;x4&OSV7Fq%@LJz*+J&9)=yb{<558{6bn=@!Fs^D&#fv(hcqIs zkF`k^?8~3uHL)U)(k*)pG7UKYfW3mvVI_hiC2iWPiu3fXnm;d}oCgas69}fzW!)%ndY1y)64x>k(FYG+hegyg903>`tJXg<$gDqb057_W`Uu zAIi$wQC#(njErOhVSAL^$*NN^-i^{3g{FdpVj5{W{6oPAi=er~-A1&sHh)^0Ym+^j zj=}Sbhj_(2dOyGOt-AXg(2yYOS;unTjTZzj`@qxw`1z-4lSJy@UrRnLD*8@za(H=7 z@tfni5^rxwhtbQ85^E>D_nd**!Txnz)i$Tr5OVjI;@Wq2Ms)iDSgo5<`;LYJ7Fzgd z4>$Dy;YjK2QtGL%8i%=tvVCMh1@|Z``eL5G~20s>$?b?59H@VbJXHn9~y)2U&OXepaQG75wRY+A?d-zGIneS!pRx z=Rr}C#h2n4tiWV=Dqfde}7850{#G$X@rw__5Z> zLiBeum`6ypCk}7<`u6DKC+^#}-E^xl&(6;N^!D}y_>ryyCR1?896WSLoNn+F+0m4J z2=%e>(tJe@0`<;Kyyx(gJLfCeQDsWd>Ng0PM*>@os$1Dam*O*|KGP&u2jnHb>m~QP zQW1QhruGA$MBW@_e)SQDazBa;!B{v>+zaHf*DM7hEIa0vCi&FbfP9VOtBWT%9DGR(zZm17`^X8CJVfZe@#;-%N$M`S z>(bYaFZxNEJTZ&53W8xwz5e&-z4C8YmZ-1kVEgRkoz{VOyF#RnnuS7LGbo#>Im~A=zGEZ*O(QNpk*{}n8XuyC07cC_fb?_F=onYjXT)*|H zj;`{k^w|p(Ew%$0z=%hVAODq6Ni~yys^2{l1Ho$+JsF*2%tdNJEl=HEZD-qqF9 zF4 z9@4B|zjGe*FBo&KR8<3%%A?0Fn_(>zXSmByrnRY3;fh6UOX2SHbE8yL=2=FwnGpAn1qV0Xvx7Cy`2AAv!r-3zx|H!B;pz}mszaJ#{lYtU2N5>O znIDx?P!J3^QZvSwwEA0MVg%jJi*~!CLy{n_RE?ADCex{OgsE~b(!!zEHjip17rV6p z?+-maMgD#qbYu4J^W7-Kif@mlD871i^pBrE-)}o}WXi_Ey$;N#=dW59rH_Wv#L!^&C{MbHF8nZ z@i>gni?^N$4i*6U+o<5V3l?;=Ui4uBZ6u&8!V@B>KHBsvVnu%gOH}aazN=vJObLC6KXo32EgYF8WEM%d#~1+;igBAAIGh;_lH%uY2+`3W$fFt zXZ=khhpDO69jcuC$G3)y9lrt-8!`k?TBe%)?1q(*er5pPeWq?7MMA!LMw z67te^)vB57404Ap!9DwoW&lr%tazXH8fEjVec_cnc`OnlMFA+T^A-Z<14(afZ=MLL z6Rx|b1Jvw_RjYhJLUrY)rTsESR;t>mPlgOLYN15PewMmn3cllB|h;PXM{{hFOUR;&F=Vk z4IrW@L|EaYXU{$($KJ;30@O<{EkNy7S&S61*}On?X2mWXa2u~g#G+lUg6;&qUx&chQX91>j}KoXK< zFLWbM+_cMbkT@2@Mjn(nHWCQMqd(!6veP#rD-*&Lf$)@%Y1-|QiFiJ-YY@ZKUS4;x1OrXlJh zbue_AiDQWqc@CVq8g_w%;0qPS(=mc-0V&;$s*u?9Q1f?V<2aOSM3vn%r*t2reID^a z;!{=C3K%ZrRtdN{f$2r(8D-|2%JJ&zlK2IC!!C$g4ld&2S8FgzRX_+DHS|pTyIEPC zB!&(RoAmG;dcO%gx&-RTPE-#d*a|G2!kZ1QOs>x_B`O)>+@l@=%0bLN0!~U?jK5)M zXc%c0*;9h3Zko7JIBO3c{BuhoR7BM*g2Ze>YIuXZ1l?!!PQ?^X_%TKZ!w@03rgjt3 zg(kyZ_@}euf5#ZGnkX_rbECfe{^7l}L)+I41VQPdxQOjOKDaAqGybnI|3bx52e!pc zwu>#t9{eC2w(M;92N5)Ya`YFQZA;+`k;ZKS*|;E=+uI9EJx>|zMXc&30WX+=iBK?e zxIBO}5l;g*ZQiUjY?vhe4}`Dv8#b)KTy6Tj;rGF^;q#7P+f-@9Y3n=5`Apnj_DNnD z(T2tfpYA)nV9UeDG!hn!VI2S!&j|OJ4l~}p3n5CR{EX*|grU*!n9P0`(-_jO+UacS zy1)6X3gU46y+%^L_kxDnwALE{=FPRme;=ljO7c>-ZxhCE_>jMaMK*X8_)FoCjj#KS z+0NbFT{t6vy^}Y8?hBZ|k2PA9i#Jxy!%VF|$+gXpp6Lz3OFmkc_{QYUcdCDZ8h1X;6-b45|u$#fWBuqNXQI}lO*REJ`kE~~4XehX8ay3=U z(I*uIoQ?|@E`*kOKWFv(OE5uy{?zBta3VY>6P@4s-MKSG^j|Gl&>aPV)h@7Jc*Sg_ zAO&Em=u5!nehy*t8w-W_`@=*h;M%ouk*iEZvRlR(`v}JHavJyyZhQjg>5@^% z;MUyEDn|#W&Zch_r%3qg%Y)&3*t5H+Iz(ATN@0yfE%9N|6H#R0XjWn)b1>JgTi0g2 zn_JMS&xZ~k6#kKEs@+&YIB2+!oqW}A)jGM@UL~j?OF2!CDmGFlXF(t00~>?7BAQ>B zp(ER)hv+o{@K09?z=|Jz{B6wQn|l#6M1u$kfdJbXcsN;0D=*Q-=B$pFKL!~IzD__= zGC!3~!3QIUO-f-WwMA}{n-k`vPeri`8*iUM0bGw?5lxtHqc`S6J~$!rxGm`z@{0mE8^{Iu#jdWwKfOPTlGn({hFyx$^Eg%1{ z7l6Df6i&7~0@<&C(q}FdU;}XOhV`Eq7c@sk=RBgAV~&kNKpWvUim!wx_!6-2*-0%4 zxK1H|d#Ex=0g+yHYOHDa{vVbW78Y~Oa7gr{+XEirM9*xx5*Jcw#@AO>SDT_XRNW@? zS|#qhd2<2<4-6> z=h_NLLpgpKO7um{veOWOkPIgg56DXAe|`^=%_^Esn1aLVk-z*_h0qv=Fw$0Ea*bWae}CL=lsXL z(|OqSPsXOLr=$oC3xhYgtmo5NxQW^@@AtFzC^yWfrqYcw=Ch{_Ig1M&68p7(e7!RO@Th8T^% zNrcq1>@9<`O0UKe-?vgB;xEv~)M%GwH+Fv8>_PL0x@P<}70dTcheebZrag{zFipLp zO9#wF*RS8pmzER*;m1bq9_$$mxTchuj69*!(m1sL#I=Lf)dMNO1<2rtP?P9JnmKBj zU66V5?3ogMOZ>K>Z>R>WDOo~N^o2RPIkZ~P&@ABf-JNaDxgLTxACHG9p|k%AC3 zR)(oCh&kIZZyDeXmkWXSQkC&{pThXT)y$JVl(NUd?mg~Bbnj9~JIJJnLziVGOX{_~ zTF$WhQ`8D4uF+n{;(&S-Crqe6)bG&lBmFtkz+fzJb>A!I1FlFIz#eU#EFPW6r0_B!HARgBP(MX;bpDF+ z{?YSf(3_?10)se@JFN_bQ!gJCD=q*Eb%%-&;?n)UYW z8J+=8X@je4pwe8&$4^hgAC?>JGBB^-Q#jmDlWzz}j(e@psV>q~=KDiFAWcHzHBGAy zCd!TPq4(2$Ujy+dC}!!ppw(i3zb>ZOV8wjko+jl4De zmjvv9oQ;hQLhvhIa<*#S2@ApZeg8gcz1@)dV}->IOy0GBe!dMofk#r+o1P4n@(Bi@ zVA&EKPI;|~t*TAKq;8KKKHP`;o3C{F*QKjbQS*E{%Q4UDXyMsD)3Ry1!yn124_7_0 zL!m# zx-!$wQFP(~_yFoOT{kayzWIlwaPRwyDk=w{=Na(WFTO%=6B~lo#~koC+E`tGjpaUx z6UL7pLgyw77=_Sduw`Gm?~{_+PmvQXLWD-)R)yy%Z~nPw&z^qcmQUZ+I;~=2bjll$ z1+ikNoJoiDmos!94TZiYnJ-x9)*&}zbh*zNgOQXLwO{16PBoyTJRNx~c)rs1HjX)) zQh~Yq-}e7o!~cq@3hg>{<%Wa+>494IS{J)N4qls&z zztq*(z_N)h5$dPpw4-fdFc0!|dkz{DqY*!x-W)NeL2!Pw$6ZAAZrUhRKNOAHwwkjh z#5%0oO()Y_+QX1>{eN`%`tjqhS(?(iGO1^v8z^il$rSh-=e@D9Ik4!tG=3izWcjL9 zi#`hsZzO&ETjD>}!#+K>X^H^okD8IOgx_>ZMVI;l%dLmXAneVeudJuEgA3b%? zqHj2~8bSksiB64+$eQgxu0b|g2y4N*c%ID|7LcWk(~5l8Ypk{Qd#elHZzmv?pjQc^}vZszLMN=)a#F?b{>X!z7;ug=zMObJ&tWLX++#wekaJD_ZeP{zQK_UM&I-sNXZj~`(L`9EY?#r?v2b6+L<U?5j~*v3GZcXB!!zyE6tjAr85x@n4tlJ0K*lYfiq;h;2lKFG*|!)spXcY# z;H+-_lV+pY3{h48&AIc$XhWGTUtVWmKVf!8*=d(l(dx;BfEnxZ_79f2otd9c%l$wK z_PyA%p!io(EuWH}*C^qmr%S}@*RLNRWv+`myx?018PWq=M!n~0)5vnPYmSm22VG>( z0XuFpV&Rixp(zJTpwh~;*1uzojEoetTVVS|d4zonPW+Xp-?jD1eYbZxSArNOEWy7e zU!t$#4-R{EUh=|`B)}c(+EBvNPZL!? zEFZ$c!UH%^{!Nw*t{jl{V*lz{`ug_CQTj5SRrpa;Q_~j-;TJFZsV!~)lIA-)P}|U# z%|=htXfr`wJssN}CGz%hRSWSP&YIPeehYp5^ghGQSwHcCX}FCySkP4UGiJ4JvzVHI zY)SWR@wT@_DhI+R;e+CE8~P5H@-d~@68r|WDq{x% z;deyp;h&Np{IBzeU9RwX8Xs*Ij1MN!ErY_?QjOmf3Go=>+Ea?qH89)Owsb`1aR_kO zAP^BXqUZOTO#vVgq63y^2X*O^{H&GZMK2r&nw_()Mh*3RsJ8YQDyoW2u0DGT{dHY8 zY`6fML&|SQA{@|VZ7y2NgJ6l1?ms^D;K9Isr!-g*L;#&U13$Xmumi!|?|2we5pyQy zBY$t`JH&G@RVhtL*;paoTdt_!D~gt6_E4W&8;xG;)mX{Mb#Bi&r*DkwS$qJLzy`bl z2f$AbTH3nGt}uwg!N8z{5p)jmxTx*E{rowc2CV#&PA)4}uKZzO_gmOwuv~Ls^)47k zeA)K<$4GHjCr^$D$$$CsBr+D_zJa^@t=E$u!XlTJl?lBWZK;eC0Ejtn8j0^gZ`T~@ zO_c8T%cf2*X%$p32jfcglMqIGP|*fY-Vkazp{0xcvM#p5w2q5@cFDyoFDklWBS&^3StJ%J1wN?| zWFidc#@3eYh*hFo4L9Z6c{5LdBYy71sgY9W?o%k@Z@97iUR%WWws&DgBP?RinvWi8 zdBkDaZu{lScLJ6{>){aEr=(}gbWVYG9Pd)BZ8j=MdFY}?bZ?PS1mQ!y#k=h=Tc^jE z6CNx>Trvv_6Xw9deLqJ*$i82iv_nbI7q@Jk4Tm9; zPoQ+ar?;LHfdcPO%JpSR0cvzzb<-bKE*Ct1FDwvtoY_hNg1R_-Hqx1Ib@ZsopxMe& zE_5Q@&CK+r{1QVxsJoT7PTIJOx2bJbUxL@m(|?vs=QNkprjKDY<(*!Wz`J7&V2Fp3 zRonK_xpZkG&t)G`oH5No)urTJ+<0Q6t3C|)nKK6|i^*|JJNwRd6}zbm{@JjoD7UoV z^7T(Ay_b+tP&kaW0|zqf*ii@_hsom=W9=PR$>}!sq_PP;W-ul%X>qnD#k&{;M72lr z#2_6V9Z;giz~jbu2|WV#=UjPj9+-m24J2+CySiJ;--h=G`%f}@VA5&#;6DmS2Yehu zxX;hai&%DNB08WQ>F&>u`~|GALH>$P?U&NAL@w;*vrs1&qg+#6rb4)>nAyUK+8?t9 zqDF77lhB?r$oX6x_@F?{oR0$V2MFbLGoTcLe43jeXuaEu~w#EONXJq>KKS13NZ@@1ppC4nL{D?Ep zG?_Nv9J8)*QpeG&$D7v|R%Cd$hQTtE*D29Xn4bl*?z;4#C*;(3zruvUMsydF{3K!v zn@F5un#rkdbCr(-&K*u`<0^!1$N1}5kYIlJ%^u!MpVp`QAYG13%5@iH2a!(EIGhHL|5Aq|HymKLd|9Z%)`dR91Yv-GxfRLMz59p@s-fAHUwkt?PCl848I^_)XbVcmpQ3rKLsG zE&_vXa(!JUBtuvbL5}llqJ;?2AY#Nw9J_NIzSwNRCdy)q4vg#AencPt=*6aDlL(>8 zKJQ{Do9#afrW_W7eu&I#{%gu!I6C)7Zbs95@AW}J}-to!Z6nQsR#|=hI^f@wzhU77HYu9qX$y)GbpMf zVX!1nND9`g&-A5bm~>M+^wfsXa%wfKI@+gEcjqQgrBniXK=vR?6jhlB8ND z4+4wt51a8irYE@8~~XfJ$=O zMg*Kl(d*z#%y*o8<9;L-7X@vlkt3@PS@%+O5#j-g7nPd&!2N@%Vhu$Y$mVS`yxW#^ zBg14;V3K6K3Tw9g_;k;wOCK!I7c6f!wN9an8C2VPvG-y{mq@cwoDTL&k*l(gHEHhB z3>k0F%kiB;eS9)TBuaPkv7zsS`fE9=m5u9JW_>)T8;a2>gCEAX^Of>lX)>u5D;{Ux16czPRvku1<0x}ghqtpiu22X5dx!Q?cVGkHsn}uG=Up?v zMsy7#R)FC&F1F2`DXb+z?ZC8zI=7)JZ+O)<#egj40_3mv7P`GP3w@o_2!u~|voYB& z+sAsJpC64Q?KoKDES@zKSlE1ZO+b76j zLtpciFi2e)+{ZvZje&D7SU_f(+@GbqkyEx+2|TNSCm8Z)4^A}uaier)F+v3E zcunIJJ_@*@j%j%G(pyJ_)PbFa6{742NZo&5;uqRiF4i|%tX0=>c1~KR%-Mrcai&Vl zU@4DTBo?F&MiwGs=wC1v6s+2tIKS6yd^8A#^=Rb8i1AjOR$tmP|5t7Sr?7BSUeSIZ zHT+Plj#tq%h|Ea2IkZQyf&0AFa)N6pVe>*-;#Z;FjiLlPX7}J;rsSTM`WzkM1Qj+4 zpb{W;vRid82|96msP3Do@9C*c4edF)Vy_v2i@NMN6%1ct|+Nmr|U-}qM4S#KfEGcTE0pW;6N0zXnM3LLCujbKL1QO)f z^!eH2z!5e@?j^(iP@@-*);w7`o)eYUxMd8nobOQf<%=P7)TWV1Zx=%CN)OVGm?*6R z%LZ}M&hL8KIk>KcSG`TWffyWC-1F(R20cuVDCtwUL>MdMJsC^CeY?Tq#~)*Lyf*xu zqPtBb8tQ|3YBDFSz_F`sZS~OhXo47s4*J4%vI|eJj5o&s&2>3GRCWYa%!sjbj<+_M zqAY!WcgNzY%d+%O)jM7M0B-JqU%1M#)*kEWH5dAdo0{2UniPz``ly z=I#JNx*jf88OIegl=O9DhmL+C-e6*FtdeE%N? zGCDVHAZAHnjE1hFfYd#ET(hYsDkmPb5{%3AJK=}Ub)93S0t$v%Y0{+sT1t-Oy;k#sS_o&Q?nW(I)cFdr3Vrz= zil9Bh0>Dfts^kwCGE*%wLgQ#G;^jR5G@OgxhT=IB;k{7Rpk=de_%$*ksIH3&42bpcSaS z2sa?X@*aSXEF?xR0YMLpSp{kuqUW?kredQeo!<9ezm^9+d??7i3z~G8e8WO`{rG&m zjaQg`1I?*oRw}~4Oxxguw_;co{?3Q|7x%<-*U28v&(G_MiDd+@UVa;nQ-3d|wU3}8 zWHsUgsu2<2IR65WS;}L&lWaOZh~&D5i;a8wVrImEK8Jb@KDe@fGeOLcGY%dXZV-7t z<2&gOACBKxJ_p55h$HF)jZz~PMfGMp1xkXY6NH3f=zAfglP#4IFiIlt>IX4nfFi+u{AdYcGGZiFy+cp~sr9$S9$<_ZH1x8; zR(_pF!5Q%NKk;~WH8y>`Og6jDjH$Lq2h**<_@zvYctR}|EyMEubGI>`W1ld1)4oWJ zETz{5cVTZFqQiQo=k$=3-NO!o*@i3^9s&qnOq`bir>3P5oj@Y)Tek{-`(EjRzl+5v zu6b-h>;P$TXQRJe;^>MB6E^}$Bvo|%vKPy+Er~t>yA2>LUqGWQBZH_Hgb>GFY{-Mu zH&?FTiVK0HMXJkp?MsXvjNDh`D%CT(W~>&PD{=V(M1@44vWb|$$ON*7NcGXzZ!x!} z@A#EcZq+YVfJGI4o?>-wAUx+-huysP2A=|lX9aWMW^TUvkx(N{tM_P2;oV`;DShp%;_P+NnkjM%ExSTG;RJZ1QN&r?`cr zC|Zz~Q_Gn}80+Sj*aw2-)i3HTr2Ml%b83STQP^GBy_^-3_Nt{e^#U_~@X?BAVX=6|AL;QY92B~-byNuQgY8O~j-LuPvPHJp! z9(v+QziFP}&5`5aU>NPQ1~a1k?Nk^9331kEbE#x#4ms&4|YKd-o^zG@0(%-)%AuA%Vbo~!Y6mQMu1Vlol?^@jZ z+BiYM(g4SvI~QBj<{g9+fsB4H&u`j-qlVimZ4DYMrC?aoVnsFCh)=G zz3;P+zk6!GM;D^Ul?jhxk5xR@^!i;j0#;)VBw5UvS~mdZDo!Mw@+B1&8Gu|;W7bbT z{I_rG>4nK663|;w;5xazKMXTxZUwy|OPq+-G3x>Bx*Qb3v^MgQXhfmJm^Wce{e9ZKxFAXz4F9yC&2Qo=)YD!Pz8Y?qrBlZ4 z#G*#zc);{xup}XYtgr?)!P3g=V|Dzr*;L*HKPn=}m7zxy#EfTv^GCEjdaOP^(ZDt; zP>&cKL6#5>I%nr+X{{{>Ko+PkAMVtqJ~?5z{|atdFN2^b&eB`Bv`BC5AB94DcRQAhn4?dF8~}?jC1k($BfCog;SeA{3KwQD-8#( z)i3suN_Utg=4QSm{NjO4dZ}Ltx+`wix1G*aR#!e{FS3Bvjv=b5-B2w>cmAOQd3-&?5eOO^8|}+~zg`V_F1&tNM9a4w+YC?u zcg1I<4 z+CwXkQ&T&dy3tAdL+fiwIx(^vpBGo-jb0FQ^Z~ULhssd3?(b5t)g}0820f*?rEx5g zQoz#sL)#z59-YNUv`lut?w;Q?2kWiqq!u%1YTRm>K?bF4tlSGx>kOaT$#wCQ(2Eo( zNZ)-(!i9`U6g1W^rrFGUu#9l3v$gAHEM(BXVloL=p{r^!!eR7ZFLhdSugvi4G-kJP zoc+2kp)j39zU&iD8Z;NcV^%zIpZdQ!Rdi7`(c!AY?afAKKBTqArc60}AnBRHp2q*&oh+m4ze2k2>W>YXlV!t7J9TYWTUn;oJvg|wq1nT$$F$_iO@lP=I*7fMIz8B{Jss)^ZVDAd4O4` zi`sQluz~bfD(gP=hYS{qitb*G?Z6!=cJG}DXul149IIi<9iT}$R7=>%7& z^icF(vf<5PZ_#UtzX0LmCT?LQOXizzjt`XU*Ux~dK=DhPw_krkUbuYv*zk<>Wf%*o&~>f4_=(y5g0`!AM>M|$^AqsVg)~)DOe}u+ zEQ%@+raJP$cht_}UrlUPC3ZEK`)ytwa?O@v4__=&cs?YHJt{*StBf9V>o38Ju( zg2b|lRWJH5$4$K>X(cbLbrU$Dj0AEOKeL;Zw!`6}6Cg0MG(g*i_Tulpvf z@6O~=F*EAeRz>fK5pO5zuBD=KN&7pIe)9EQBb_MlZ`BDNiW*k{hy_@p zbs7E#iE#pY(3Dft>AuUL#`Y-j=_5=@1k$80P7;FEp-R2(kgvq(evcoyvjqYJx*W?= zcCN+?JA!Z*VpJK|b&udWL=Pj^@WQUy%|h@gc+LmmftNZDk@m9Xe6}qtb={; zk;mOxWf%9)Yy&i(Eo&dRO~RwHn9;}dn%&164}Q1abg;m&0w>hk*Kzw`?g4HyDZYd~ ziTrq{rmy}N^30R|F3GwGVY9+;OZ9!VZLySM^*A1J5pLXBvpRIxvkOufXVVj%mb%08 zZ+2h3^yBNh(`@R0zO0<74NDvP-h7o!fM9!j2B~Jq_U-!!tFoYtNmD#f{&_wMKRw8+ z5nsW``Ap6gbB$<6xN>o+$+m9)+JOyFKs~TpF%0+1K*P|UF)JpX4(YAgNAInCxM`JT z_qb^r-biks>hWaEoC0tWQIF?SN6Al$Rln#yJ8D=myyH6DA# zoZEZwWE^W9tLVI^z;R}znW)_?d`-*Pq-M&!EJydwtS8nidNXlb%yE3L06+m6kfe-i z#cZ{c;#=e+{qdw#puQIH$#*P`3Ktu{E(E&aU8)hM>gRm))F+xgR{@BAa(Wkdgi zvxbtpUETVQDM{#5YIk_*LxtAH+4~P15dE~Hf~rsbOs?*Xm?Sh!V5A5v$K?7S<55hg>=GJM1FPHMzoxWUCVurc-J-tuwZYQ0*QY+lqu&rd6Roou?Ozh2rM zt^-)*^VRSND#OiE0>?;4TTu}10d?JgmdkAWk}&hYUi~}sG+t%o?n`=RuRb16)sL-H zsm|VRt4%=?xmU2F6GvFdIkDjEtF<4I4TSp_7bi~zokBnv3i-zcC5!t1S0)K3-tsp= zE8(+}ad;HmsOj|bc#k~Cc>8#(%lE#Z|G8RKUmX8ylZtfvTe3<4odU94Es7+TocKhb z9+30o@#E)?@mt&s!*U;yxX)PM0n0(mq~IoAJ%NL_MeYILu(tZo>5-#GG4(S9h`G?x z(r;FnB9V&`1ckxx4tUGxP2O~;w|+oYI<&B8+sA0KDS1%Oo~3a=&6fSpX|2~05)c)~ zP3L!7XSaRTncKMgHCJt8B2Z9KE?X*tF@~sCPnuqqa$lJJJr&XeQ4GU%yiHm zBMwjdt&Cd#5V@51g_GFy?gpNag$^ewh`!gYqDg*p|n)^GmyVdV2Q_8-DkW_hma@&fkCpW8pi*`^~L49i+E>+}tU2YVIeXSNOKT z!c}9_{@k$BJl#;dE%x;jr}wETLwn%w(PSTONM55Z-}L>-STSw^$)5%AVQR}coFPQw zm4MFb$qld=2M10$PGid%`XY`2fb}k%r-CZ4sJM>O^Z3aVk6*9WEM{zoRrGN%}$O`i`fe_M+llkjE+QqU{nxn!Yo>#HR&tr zuKi7dEoF<}QM>WO9)BlMmJ>|bz8aYW`dH$}td@-E+ zt3sXk-$mYvZDCQY9F(&QQHY>rLK_db-l!l(zv6eDtkx&!B7e%10GP;(yh zRfSf}TmZPhlZnM!4+xfkeZg-v{w%VASLh~WVZ!h#D1HJ5!$O`RwhGP;@@m7|B@9X= z8*Y^n z^oW_tf-CMps{T1EYj4ly(VW1XYi@(vKefu8$`epG1 zYW&prsBBS$w6OSZxw922eEL{_U`CMWw&vrF10D3rK9gpVps&IeR1KYeo5bxXmF zQ+jeD(>Ls(hlDWZ8b8br7URa<8!~z?CO8KVPO#5ydmuxPOe+Kos6)s7c@HpqN@N#U$_)T(QIS-7ilUsEQs4{^L zQmRRz@tH>zRBdmr7ejdx)Bbh?Pyq(vX1AQWy6LB{AAVC{zQmGw;=&3e3Sy>;5QWc* zQT#ijgEA-}zklCIz<$tC!SgQYOAJSnK(xx_xQ^>PV8Tb0=DQq=vN@+3n8eMO^`0LnZ4=S0^qt^xPL&eUiutm z2DMzEW-2f}E2Ae>wCRpyt}~i~cJt?(BCt`sPqyZzgAWXi(qTel7Na``$o9cZf#ScM zFeqX#4nM47YNwyFFGNo)QaSRuzw;`HKqv%{(tRTYk;v{^1J@!N1YuG*#<}+D5)68j zIW+45>*e0kR0+d2BibA#HY+MNb?XDmH*pRRyjq)Wpe$ywy=i8=Lpfo6ex=6VY%o3Re05gtz&5&HHm&R@FTZoVj1{;ucHE&IL)QMrIE}O zJjum6K4_%3%zZbontI=^lv&piw}lsoi|LR-j}11zfEKbW@#lKLT-=^7lM%Y7@egEtul6VTf)2W|NGzE6lU z1=5PaJ<0DZIuAKOsb>Urf_QiM?PwToq zIXkQ~yvwR>vZIPTlMD7~aV--a9lHg@b4dGoS~Y*8+nHZ#I}#@>X{_$6qO$OU@#JL< z=CZLsf7JfNP|ZQg71II}>4R0bKG2V}A3&8{clqsVnhS5H<4M1k3dqZasW% zS$TT?(b%^5eaz#Bc1d~I-=b0u@`y#jg5=Wl#HQswk4B$J5r0C(IlH)Y>8tFO0a@iI zAJKO}ztx6=4sv>LPm#$r3umYY&xfPZlJ7n$ugbM=*S)8tV_)CGy_Zu;9aGbn%*|Ab zc>y!G`9#>Rz8Or^z`bXu;~bEs4`UxQ<=(6p+To@iPgHM%<&(T#ddmdevCb*@xuf#y zW{az@AAaeXUSz#Z*lH%A>p2y!?oXHQVeRjJ2ZqRoeii6P5dVOkqPVlnFJtg%ZRjH~ ztl0=~0s*P}U0;PA?oYR^zIpr7gC3zqQ7=``9X(p|3|>^J-_PG&8mKzMawx}j&M?!XeL8@ z&ot`CF9k6MgjCaan&)ITy>P`P#!U+&Vl|W}sQQuNb-aG(pR)uv3Lawk@Qeb*P9|$` z0zeWSz;MlR70oOKN#hbln4<<3^H2V8f^jeh&mQFAO`zJuye13Z;rb!2{DJF?^qKe8 z2oc*vE8bJ5*p!de*Te$ICVsIUG=8O@UE+SW6aMR`Kv{=ZH^L7LW#v#|OKn);$rYEI%<-nVS!4ESsR>WJ;j^7dASqVje62d3-jL8VW%8?#OzD=LP z4u2|$>O-Nr(alo)=DsBfgFjYQ-UN@NHkNjw9bkcWog{!IIf$*9K=%NPANqtzz&&PP{OCvCG08%WEHIN zvr7MxbW*lw&pC7E2y>1nKt$XvsZGKad@=;Hh3^Rckb0^JFr%1aDH=4WzgD6~iJ!ui zv`lV@cf@px2DEr$Tt!6%t|?(PfPh#w@YH%WzEWHbKu=IN?>+0g5w^q>CGND$__vTv zpc9NNq6rh?U)Urmj4nq#=&AQskqV9|_UP%;;i$U|SfLBppDIm9=N!OJc>P|P-W;^4 z`EaY~8e*yyyCSTpX4yc08Mor84R0SHQ|&X4a&uGG^Lw*$eDE#CE5cwfaS#S|pj{)V z%`sj+-pbIyL0noXCisCGpytHs-tZ$->Gt;B_Lq&7FB&ZMlfP&lwQS&SnbO|DPgir{ z0wt#4E{LW5jlTZ)^-F2|_@1Dg4I9*;=o!gEw|RM>+5V)cXT$v16W;4rU$^9{obz04 zN%;qTW*ljj=$T5opsp4-i-j1?CcjgZ>4oLW(5lY8c^b3EE;Z<>ujnlk>Q61m6@5p! zOAa)M7_ucKyQ#Ukj9$mAm7&+J9ib$IhZN00)Eg3cH!OYU>{ez^y-*}vCnsyu&LrI7 zZ{FOQ8(UjdWx&KIKsv1~0Vht(c5sL++a6U=9jILVxn_xGA&e1=6gW3AWJ%5DVYKN=7q@Gnfs_xgoT|NAWA#C{GA)2&^#29jUMjWPoV9s+m+H-R^q<5fz3RsGz` zM2*~wF1I1-mcQoV_;Q^*za*Uf*gx}hk0^#rXD>1gu<*0Ef_+>w1@ zY57uP*<;WVDVgF;&;{Ds$&WL$3TCp=`N$?_X62R}dGob~S!Zb@G|aY~YZw+`)iK;jek>*tZL^OCvpVtNqOyBJDm6&-WkK+0l4 zXv9+%i`(bKvZ%|OL=~tL_KU|_eZjmkfBwNR4J|%Mp=)%J+_Ax(6}@k*QVQ6|dc;|& zLU8bn!*cEIUqs{%9KMChflbYh6~lYrEei_2tmP=ciDYDCa8|?2qnK+I5#-dw_;|jL zK3kYIwO5y?&iOoX-#SCk2i=nw%%fmuFWkCyl9th6$wf2E@36m0_oC_yQ1#*M#nJgU zohtU2H}+0eZ5L;i@kbQVtjm?FZ*E_`dXOS?p{c33&H)uP3;@MKae*&gU#!V_^C$xH zm1E;;y)$feqYAk1$2)7K7*+}_xuveOMSvt4Wx&aJvSj7ut*WveH`M&MD@H~YJSIQZ z(MT|QbXOaj_l`~(SE`*W^YTmq^=f^)Qq2l~MrJ&0JV@a`jLtb0qmzT?R@g7Tx^KhL z-Uod61#W~>9y#(}QBmUDfZE^mA531ozOuaB*vg77j$2U=Hah)V_J}}mpOVt0Sx=Ia z-ZX7q;tk=zXA3-jJdaG-|Lc^iAcgvRF`a}(1HkY^g?I@roYUK7OZK@8+Y8CbZ|laL zerlBSrTaBi?P&8Tw&>BXxp&HOrx9ks=`qq7U$Pd2l69>rtDV<0udVDq+;MBlS*}-r zFJa=;C5p?$cbzM1YHI9Nr;{(yCdqyDZN`$nkU+UpTiNv$N0~ZQq*5(jAN;q zc^kOpH@fu|VO%EODrK$=u0Js{hL{{9J=4z$@OcgLitO~Y+E~QgAwNgyjvhI(gsG5R zJXlb0LCQo$P??0=J6c`EtIX$B+CV;G{1>nMqI1FRYu5xo8osR!Y$<;l@o$Iy_lLp_ z6hib<+@AG>+3?Dr85uB45a1G91bfRIq0qw43liKros_nu(!w+}??% z%@oft7IvrVVqTp_j-K>r5YssfoRp2sRuUF3Tt1M7P+=(bGdbh*7E6dBN5~K;C^LYB z1hgL1hUa-FdcOPa^!ih#02|tAXIz{*iA1nPumyy6H7;ZNn%27Gzlxz(@7{R~Pva-V zJ$x|Eco9C-)CdlV|I@c?2QVCz83u4GTo)1&U6&Z>>zYX<^nQS!fexmbcpK?X3g>P8 zYZpLcAUc$>(e83d$$u0x0aCP?(L)kvwJy9N6>40tovyXyX@*&9z1xPu%l8o@b;$Nd z0|WJeT!;P+kXkXWFJbwI+xweV(f;rIUE6--aaLAj$;F>6qjHjA_^SETxP5&qV|dP`m3U_{YMYMF4D0?5nccFPVILv z)dY-mXLtN|fSJd}ZWMi(4JG0STzSn;@!auVe8W5-9^WnInIL!KV1gT@+)y60^^+oe zvlx?w2KwpeR_;f%FTC217(^G(Uana>iYrHgN(m>sq69SLP#lMxOmR7t;4tYMVTej7 zpBGJX6IZ>6%dw$5MVvY3`Mnc!kvcBNLo;l|hzaafYtOow)cax=i@_=oNMrufqXhkj zn&M7*SOwzdFPRs@<|(_?z@@>&31t*Z+94ukD9YM93L zi{egVN({^gt9g)I)bZlOg5420%)GGDtGF8}y900fFfr&+XMk?Xcx@*c6Nm)?Rtjdk>E3BSzFe-;RVz)=<3@5E+7>%SPSLbD2&G4u*IH9+d3hL99D zSo!NXGjL;wre;6nItn8Xc&z~qH<$v6wMhy$hBh_AhFVd$V5{7c)UfUA?aU3e==4)j zZZ{+~;KqfhLhk)=!FP}?Rj&97SWjTRGZC2~o4SbkcZ^br&TDm2bmrWbR#dpn%d{HZ z`>J_VS-aCRr61=j$qF<%Jo4}R-_xl6S`x_cN(TSV}PC4j($S-A*_&prHf~=eBp>yg5x%j)Vwzs;a}sj?JmF zLIW1Fl~I@}+M$faB`c!5k?P+4GH*w}%YBoNFsJmucSI`9TsfXASUSA?Ri414EI?+? zNNLQYj z!4hRwQA&(V{H_xxPOP0YDt#T2Lec#FSI1TxEwP^p5?-9PW2zttQEEhcB~I!abO78< znD7w8XH)5cv1cC2Q*f`R?1Ih1zW9MAq)TyeFREVmKG1$=$3gBp`PO1T!E6}NUiSWd z>!s_0FAWN6|FUPED55c5Qr9_fM=HpU>3_3NoJbj&8W$Ic9$Npfzl*5LKtcnqzg$ud z9MuYWcJ$<;SulR=u|}V>$aFX{KLV%ei}Od{5aV4hUo^d7ynN#6HeyyM7%*{r?ehK& z?l-?qBdgsXIHybKX@(nIwlnozCe&wI9mE9t0>?p~-*g!T*2+?0AK^5oI;^F%Ea%($S58=+y{pg4fc4j}3)f*vDqT(~eK3rG=Np zzqU=9n)j@(*Q1U%cG>LQbJyg+MwX#T>ByNgp80kuDcc+C zn#15p-~X`Oxg%#wWm@Lpwt?-ou648RouE}GlUB6PXQzpkOighA;fp)_+#h*6^5oKY zR~R$*4V00KujTR$=iv@XNi%PTPU+Xz)9p`zN3F2NV6}`sHp3*1{Y?Hxh6bWZC7RmQ zV5pcGlxMHbh}SD~W1y(!kg$T5MkQebd0L<-wPA*1{Ojw-Ps`}NJviUA$3L`ZR83zn zJu!!;{yG`;Kql0jun`(H15{?xr65Z3I&NK~n`?b!(qc!BbSzwRZu>*V7?)L#1L4ha zc8-afu|C40Qz+(dCdkG}&yX_7ImFmNgr?iTKz-Gf@7Lcis9q#sbUek9ZdmRu0S_Yr z|KI!ngUFm5qbpYrG`(?23y0LaGG+vSLAh8{Ed%~8jyNW%R(Egq8dQvbI zCO*k2+h4{z^=WulR19!ctiCqgZSeVsgqnHcV=hv@{ibZ#mThkfR!AwPFFU%*`M=kb z6>b&r!m?Giz4iQR#>scHkI(q`x>u;>Qlrb{ULH%|G`&$lYnq?$6DYRGgU+WD(C0S#_5Nnjj~H$Yh9NarxibV z_18CxZz^_H)PH(ysfT6k)}@CBEzyg-_1$=xhjOg;=h!Za-cyp21|>Uhw_hORE75J| zxbvN4+Nrm{-$vtuPxS74@;H!BPVs=yn90X*X_RS zv>)8}+dXryxgpBl>qnTcbvvIjNPW{6-!&f^B|_ptDt?d^@BJtDjM$b8MR z?svSc)%1>i8>gDYSsW~ylC!nq@5Z~X74i>Nx~rIu=&bnmaEeh}(W+H1t0!#>wb$v; zeMhX+Qw!-Ab~Rql9u#i0mrOp|8=9v%(lYT?P5h9x`)vCKB@TNXFk_o-#Dl^wOV?Ly zkCf$|9$Al&zh1B=gVz~q@iXS-5}7aOj&*xED)+$7%Am?>6`Lr>F*|sy<5#cUXWc15 zvTjJiu-v<~(SCA^{ybmn;*p(c-p49dJF)v6w)Ymh*f;xk4frrV;dkz|#W|7fbFFt( z#}0@{+PHDhhiw&;7f)cJ{73DO81v9PwqnyFmCypmc(~>E&!T;|u03~S?9$ujY9Dws zfs*0}KWmLeS?bj;CF`ff=uA85bVidSlJe|)|hskBG?(@CbclBk-9%nkLT8%oh zvexSOeq*VUm{FVioxV1F$FF;@D$J&}JWn<6YvHf@aN@7$#bHC-Ldh}9rarW?DOs^V z#>6t(JjT=3e0kIlv&pf|!?P~P+<&*|OwihoS1JaAK^7i!4>vNio!4RNwVxMXrX4Tm z<-f*Qhu>=%*RFR|f#af=H5n>$Lne>#G40e@vHF-(Z{HLxO>d{_2m252{~7w?FpIb{ z`djQ&ldFw)N9x8!8LRC!+o|W5lwne`Y^++cl~1u;OnofG?u43xR`aWUS{1BLIT$AV TR6iiWe+*_C>Bs0T-~E382x#XJ literal 68667 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 diff --git a/doc/source/images/plottingClasses.svg b/doc/source/images/plottingClasses.svg index 393d16d7..9d9cd902 100644 --- a/doc/source/images/plottingClasses.svg +++ b/doc/source/images/plottingClasses.svg @@ -13,7 +13,7 @@ height="268.51233" id="svg2" version="1.1" - inkscape:version="0.48.1 r9760" + inkscape:version="0.48.4 r9939" sodipodi:docname="plottingClasses.svg" inkscape:export-filename="/home/luke/work/manis_lab/code/pyqtgraph/documentation/source/images/plottingClasses.png" inkscape:export-xdpi="124.99" @@ -50,12 +50,12 @@ inkscape:cx="383.64946" inkscape:cy="21.059243" inkscape:document-units="px" - inkscape:current-layer="g3978" + inkscape:current-layer="g3891" showgrid="false" - inkscape:window-width="1400" + inkscape:window-width="1918" inkscape:window-height="1030" - inkscape:window-x="-3" - inkscape:window-y="-3" + inkscape:window-x="1" + inkscape:window-y="0" inkscape:window-maximized="1" fit-margin-top="0" fit-margin-left="0" @@ -69,7 +69,7 @@ image/svg+xml - + @@ -345,7 +345,7 @@ id="tspan3897" x="124.24876" y="376.57013" - style="font-size:18px">GraphicsLayoutItem(GraphicsItem) + style="font-size:18px">GraphicsLayout(GraphicsItem) Date: Sun, 9 Feb 2014 09:42:04 -0500 Subject: [PATCH 101/268] Added a few new examples --- examples/BarGraphItem.py | 41 +++++++++++ examples/CustomGraphItem.py | 136 ++++++++++++++++++++++++++++++++++++ examples/FillBetweenItem.py | 50 +++++++++++++ examples/ViewBoxFeatures.py | 90 ++++++++++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 examples/BarGraphItem.py create mode 100644 examples/CustomGraphItem.py create mode 100644 examples/FillBetweenItem.py create mode 100644 examples/ViewBoxFeatures.py diff --git a/examples/BarGraphItem.py b/examples/BarGraphItem.py new file mode 100644 index 00000000..c93a1966 --- /dev/null +++ b/examples/BarGraphItem.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +Simple example using BarGraphItem +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +win = pg.plot() +win.setWindowTitle('pyqtgraph example: BarGraphItem') + +x = np.arange(10) +y1 = np.sin(x) +y2 = 1.1 * np.sin(x+1) +y3 = 1.2 * np.sin(x+2) + +bg1 = pg.BarGraphItem(x=x, height=y1, width=0.3, brush='r') +bg2 = pg.BarGraphItem(x=x+0.33, height=y2, width=0.3, brush='g') +bg3 = pg.BarGraphItem(x=x+0.66, height=y3, width=0.3, brush='b') + +win.addItem(bg1) +win.addItem(bg2) +win.addItem(bg3) + + +# Final example shows how to handle mouse clicks: +class BarGraph(pg.BarGraphItem): + def mouseClickEvent(self, event): + print "clicked" + + +bg = BarGraph(x=x, y=y1*0.3+2, height=0.4+y1*0.2, width=0.8) +win.addItem(bg) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/CustomGraphItem.py b/examples/CustomGraphItem.py new file mode 100644 index 00000000..695768e2 --- /dev/null +++ b/examples/CustomGraphItem.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +""" +Simple example of subclassing GraphItem. +""" + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +# Enable antialiasing for prettier plots +pg.setConfigOptions(antialias=True) + +w = pg.GraphicsWindow() +w.setWindowTitle('pyqtgraph example: CustomGraphItem') +v = w.addViewBox() +v.setAspectLocked() + +class Graph(pg.GraphItem): + def __init__(self): + self.dragPoint = None + self.dragOffset = None + self.textItems = [] + pg.GraphItem.__init__(self) + self.scatter.sigClicked.connect(self.clicked) + + def setData(self, **kwds): + self.text = kwds.pop('text', []) + self.data = kwds + if 'pos' in self.data: + npts = self.data['pos'].shape[0] + self.data['data'] = np.empty(npts, dtype=[('index', int)]) + self.data['data']['index'] = np.arange(npts) + self.setTexts(self.text) + self.updateGraph() + + def setTexts(self, text): + for i in self.textItems: + i.scene().removeItem(i) + self.textItems = [] + for t in text: + item = pg.TextItem(t) + self.textItems.append(item) + item.setParentItem(self) + + def updateGraph(self): + pg.GraphItem.setData(self, **self.data) + for i,item in enumerate(self.textItems): + item.setPos(*self.data['pos'][i]) + + + def mouseDragEvent(self, ev): + if ev.button() != QtCore.Qt.LeftButton: + ev.ignore() + return + + if ev.isStart(): + # We are already one step into the drag. + # Find the point(s) at the mouse cursor when the button was first + # pressed: + pos = ev.buttonDownPos() + pts = self.scatter.pointsAt(pos) + if len(pts) == 0: + ev.ignore() + return + self.dragPoint = pts[0] + ind = pts[0].data()[0] + self.dragOffset = self.data['pos'][ind] - pos + elif ev.isFinish(): + self.dragPoint = None + return + else: + if self.dragPoint is None: + ev.ignore() + return + + ind = self.dragPoint.data()[0] + self.data['pos'][ind] = ev.pos() + self.dragOffset + self.updateGraph() + ev.accept() + + def clicked(self, pts): + print("clicked: %s" % pts) + + +g = Graph() +v.addItem(g) + +## Define positions of nodes +pos = np.array([ + [0,0], + [10,0], + [0,10], + [10,10], + [5,5], + [15,5] + ], dtype=float) + +## Define the set of connections in the graph +adj = np.array([ + [0,1], + [1,3], + [3,2], + [2,0], + [1,5], + [3,5], + ]) + +## Define the symbol to use for each node (this is optional) +symbols = ['o','o','o','o','t','+'] + +## Define the line style for each connection (this is optional) +lines = np.array([ + (255,0,0,255,1), + (255,0,255,255,2), + (255,0,255,255,3), + (255,255,0,255,2), + (255,0,0,255,1), + (255,255,255,255,4), + ], dtype=[('red',np.ubyte),('green',np.ubyte),('blue',np.ubyte),('alpha',np.ubyte),('width',float)]) + +## Define text to show next to each symbol +texts = ["Point %d" % i for i in range(6)] + +## Update the graph +g.setData(pos=pos, adj=adj, pen=lines, size=1, symbol=symbols, pxMode=False, text=texts) + + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/FillBetweenItem.py b/examples/FillBetweenItem.py new file mode 100644 index 00000000..74dd89bc --- /dev/null +++ b/examples/FillBetweenItem.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" +Demonstrates use of FillBetweenItem to fill the space between two plot curves. +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np + +win = pg.plot() +win.setWindowTitle('pyqtgraph example: FillBetweenItem') +win.setXRange(-10, 10) +win.setYRange(-10, 10) + +N = 200 +x = np.linspace(-10, 10, N) +gauss = np.exp(-x**2 / 20.) +mn = mx = np.zeros(len(x)) +curves = [win.plot(x=x, y=np.zeros(len(x)), pen='k') for i in range(4)] +brushes = [0.5, (100, 100, 255), 0.5] +fills = [pg.FillBetweenItem(curves[i], curves[i+1], brushes[i]) for i in range(3)] +for f in fills: + win.addItem(f) + +def update(): + global mx, mn, curves, gauss, x + a = 5 / abs(np.random.normal(loc=1, scale=0.2)) + y1 = -np.abs(a*gauss + np.random.normal(size=len(x))) + y2 = np.abs(a*gauss + np.random.normal(size=len(x))) + + s = 0.01 + mn = np.where(y1mx, y2, mx) * (1-s) + y2 * s + curves[0].setData(x, mn) + curves[1].setData(x, y1) + curves[2].setData(x, y2) + curves[3].setData(x, mx) + + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(30) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/ViewBoxFeatures.py b/examples/ViewBoxFeatures.py new file mode 100644 index 00000000..6388e41b --- /dev/null +++ b/examples/ViewBoxFeatures.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +""" +ViewBox is the general-purpose graphical container that allows the user to +zoom / pan to inspect any area of a 2D coordinate system. + +This example demonstrates many of the features ViewBox provides. +""" + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +x = np.arange(1000, dtype=float) +y = np.random.normal(size=1000) +y += 5 * np.sin(x/100) + +win = pg.GraphicsWindow() +win.setWindowTitle('pyqtgraph example: ____') +win.resize(1000, 800) +win.ci.setBorder((50, 50, 100)) + +sub1 = win.addLayout() +sub1.addLabel("Standard mouse interaction:
left-drag to pan, right-drag to zoom.") +sub1.nextRow() +v1 = sub1.addViewBox() +l1 = pg.PlotDataItem(y) +v1.addItem(l1) + + +sub2 = win.addLayout() +sub2.addLabel("One-button mouse interaction:
left-drag zoom to box, wheel to zoom out.") +sub2.nextRow() +v2 = sub2.addViewBox() +v2.setMouseMode(v2.RectMode) +l2 = pg.PlotDataItem(y) +v2.addItem(l2) + +win.nextRow() + +sub3 = win.addLayout() +sub3.addLabel("Locked aspect ratio when zooming.") +sub3.nextRow() +v3 = sub3.addViewBox() +v3.setAspectLocked(1.0) +l3 = pg.PlotDataItem(y) +v3.addItem(l3) + +sub4 = win.addLayout() +sub4.addLabel("View limits:
prevent panning or zooming past limits.") +sub4.nextRow() +v4 = sub4.addViewBox() +v4.setLimits(xMin=-100, xMax=1100, + minXRange=20, maxXRange=500, + yMin=-10, yMax=10, + minYRange=1, maxYRange=10) +l4 = pg.PlotDataItem(y) +v4.addItem(l4) + +win.nextRow() + +sub5 = win.addLayout() +sub5.addLabel("Linked axes: Data in this plot is always X-aligned to
the plot above.") +sub5.nextRow() +v5 = sub5.addViewBox() +v5.setXLink(v3) +l5 = pg.PlotDataItem(y) +v5.addItem(l5) + +sub6 = win.addLayout() +sub6.addLabel("Disable mouse: Per-axis control over mouse input.
" + "Auto-scale-visible: Automatically fit *visible* data within view
" + "(try panning left-right).") +sub6.nextRow() +v6 = sub6.addViewBox() +v6.setMouseEnabled(x=True, y=False) +v6.enableAutoRange(x=False, y=True) +v6.setXRange(300, 450) +v6.setAutoVisible(x=False, y=True) +l6 = pg.PlotDataItem(y) +v6.addItem(l6) + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() From 5488f9ec849f7b7779afdfcc118870d78600fead Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 9 Feb 2014 10:38:29 -0500 Subject: [PATCH 102/268] Added BarGraphItem.shape() to allow better mouse interaction --- CHANGELOG | 1 + examples/BarGraphItem.py | 2 +- pyqtgraph/GraphicsScene/GraphicsScene.py | 8 ++------ pyqtgraph/graphicsItems/BarGraphItem.py | 25 ++++++++++++++++++++---- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ec558564..40bf39a2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -31,6 +31,7 @@ pyqtgraph-0.9.9 [unreleased] - Added FillBetweenItem.setCurves() - MultiPlotWidget now has setMinimumPlotHeight method and displays scroll bar when plots do not fit inside the widget. + - Added BarGraphItem.shape() to allow better mouse interaction Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/examples/BarGraphItem.py b/examples/BarGraphItem.py index c93a1966..6caa8862 100644 --- a/examples/BarGraphItem.py +++ b/examples/BarGraphItem.py @@ -28,7 +28,7 @@ win.addItem(bg3) # Final example shows how to handle mouse clicks: class BarGraph(pg.BarGraphItem): def mouseClickEvent(self, event): - print "clicked" + print("clicked") bg = BarGraph(x=x, y=y1*0.3+2, height=0.4+y1*0.2, width=0.8) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index d61a1fa4..a57cca34 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -92,15 +92,11 @@ class GraphicsScene(QtGui.QGraphicsScene): self.clickEvents = [] self.dragButtons = [] - self.prepItems = weakref.WeakKeyDictionary() ## set of items with prepareForPaintMethods self.mouseGrabber = None self.dragItem = None self.lastDrag = None self.hoverItems = weakref.WeakKeyDictionary() self.lastHoverEvent = None - #self.searchRect = QtGui.QGraphicsRectItem() - #self.searchRect.setPen(fn.mkPen(200,0,0)) - #self.addItem(self.searchRect) self.contextMenu = [QtGui.QAction("Export...", self)] self.contextMenu[0].triggered.connect(self.showExportDialog) @@ -437,10 +433,10 @@ class GraphicsScene(QtGui.QGraphicsScene): for item in items: if hoverable and not hasattr(item, 'hoverEvent'): continue - shape = item.shape() + shape = item.shape() # Note: default shape() returns boundingRect() if shape is None: continue - if item.mapToScene(shape).contains(point): + if shape.contains(item.mapFromScene(point)): items2.append(item) ## Sort by descending Z-order (don't trust scene.itms() to do this either) diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index 9f9dbcde..a1d5d029 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -47,16 +47,20 @@ class BarGraphItem(GraphicsObject): pens=None, brushes=None, ) + self._shape = None + self.picture = None self.setOpts(**opts) def setOpts(self, **opts): self.opts.update(opts) self.picture = None + self._shape = None self.update() self.informViewBoundsChanged() def drawPicture(self): self.picture = QtGui.QPicture() + self._shape = QtGui.QPainterPath() p = QtGui.QPainter(self.picture) pen = self.opts['pen'] @@ -122,6 +126,10 @@ class BarGraphItem(GraphicsObject): if brushes is not None: p.setBrush(fn.mkBrush(brushes[i])) + if np.isscalar(x0): + x = x0 + else: + x = x0[i] if np.isscalar(y0): y = y0 else: @@ -130,9 +138,15 @@ class BarGraphItem(GraphicsObject): w = width else: w = width[i] - - p.drawRect(QtCore.QRectF(x0[i], y, w, height[i])) - + if np.isscalar(height): + h = height + else: + h = height[i] + + + rect = QtCore.QRectF(x, y, w, h) + p.drawRect(rect) + self._shape.addRect(rect) p.end() self.prepareGeometryChange() @@ -148,4 +162,7 @@ class BarGraphItem(GraphicsObject): self.drawPicture() return QtCore.QRectF(self.picture.boundingRect()) - \ No newline at end of file + def shape(self): + if self.picture is None: + self.drawPicture() + return self._shape From c8ee4a86be7b2d3a6c5075e6657531153ae685d8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 9 Feb 2014 10:39:20 -0500 Subject: [PATCH 103/268] Added support for GL_LINES in GLLinePlotItem --- pyqtgraph/opengl/items/GLLinePlotItem.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py index a578dd1d..459d701e 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -16,6 +16,7 @@ class GLLinePlotItem(GLGraphicsItem): glopts = kwds.pop('glOptions', 'additive') self.setGLOptions(glopts) self.pos = None + self.mode = 'line_strip' self.width = 1. self.color = (1.0,1.0,1.0,1.0) self.setData(**kwds) @@ -35,9 +36,13 @@ class GLLinePlotItem(GLGraphicsItem): a single color for the entire item. width float specifying line width antialias enables smooth line drawing + mode 'lines': Each pair of vertexes draws a single line + segment. + 'line_strip': All vertexes are drawn as a + continuous set of line segments. ==================== ================================================== """ - args = ['pos', 'color', 'width', 'connected', 'antialias'] + args = ['pos', 'color', 'width', 'mode', 'antialias'] for k in kwds.keys(): if k not in args: raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args))) @@ -93,7 +98,13 @@ class GLLinePlotItem(GLGraphicsItem): glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); - glDrawArrays(GL_LINE_STRIP, 0, int(self.pos.size / self.pos.shape[-1])) + if self.mode == 'line_strip': + glDrawArrays(GL_LINE_STRIP, 0, int(self.pos.size / self.pos.shape[-1])) + elif self.mode == 'lines': + glDrawArrays(GL_LINES, 0, int(self.pos.size / self.pos.shape[-1])) + else: + raise Exception("Unknown line mode '%s'. (must be 'lines' or 'line_strip')" % self.mode) + finally: glDisableClientState(GL_COLOR_ARRAY) glDisableClientState(GL_VERTEX_ARRAY) From 032c6c625d9447131cc4c8253eae3dcc4a4a4775 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 10 Feb 2014 20:51:17 -0500 Subject: [PATCH 104/268] GLViewWidget.itemsAt() now measures y from top of widget. --- CHANGELOG | 2 ++ pyqtgraph/opengl/GLViewWidget.py | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 40bf39a2..f2c01af7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,8 @@ pyqtgraph-0.9.9 [unreleased] - Renamed GraphicsView signals to avoid collision with ViewBox signals that are wrapped in PlotWidget: sigRangeChanged => sigDeviceRangeChanged and sigTransformChanged => sigDeviceTransformChanged. + - GLViewWidget.itemsAt() now measures y from top of widget to match mouse + event position. New Features: - Added ViewBox.setLimits() method diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 0516bf08..99a43da9 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -129,6 +129,12 @@ class GLViewWidget(QtOpenGL.QGLWidget): return tr def itemsAt(self, region=None): + """ + Return a list of the items displayed in the region (x, y, w, h) + relative to the widget. + """ + region = (region[0], self.height()-(region[1]+region[3]), region[2], region[3]) + #buf = np.zeros(100000, dtype=np.uint) buf = glSelectBuffer(100000) try: @@ -140,12 +146,12 @@ class GLViewWidget(QtOpenGL.QGLWidget): finally: hits = glRenderMode(GL_RENDER) - + items = [(h.near, h.names[0]) for h in hits] items.sort(key=lambda i: i[0]) return [self._itemNames[i[1]] for i in items] - + def paintGL(self, region=None, viewport=None, useItemNames=False): """ viewport specifies the arguments to glViewport. If None, then we use self.opts['viewport'] @@ -294,6 +300,17 @@ class GLViewWidget(QtOpenGL.QGLWidget): def mouseReleaseEvent(self, ev): pass + # Example item selection code: + #region = (ev.pos().x()-5, ev.pos().y()-5, 10, 10) + #print(self.itemsAt(region)) + + ## debugging code: draw the picking region + #glViewport(*self.getViewport()) + #glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) + #region = (region[0], self.height()-(region[1]+region[3]), region[2], region[3]) + #self.paintGL(region=region) + #self.swapBuffers() + def wheelEvent(self, ev): if (ev.modifiers() & QtCore.Qt.ControlModifier): From 79af643955fad84c968d6e74d5235f0f5d4b44c5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 11 Feb 2014 13:59:09 -0500 Subject: [PATCH 105/268] Added Vector.angle method Inverted MeshData.cylinder normals --- pyqtgraph/Vector.py | 15 +++++++++++++++ pyqtgraph/opengl/MeshData.py | 8 ++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/Vector.py b/pyqtgraph/Vector.py index 4b4fb02f..b18b3091 100644 --- a/pyqtgraph/Vector.py +++ b/pyqtgraph/Vector.py @@ -67,4 +67,19 @@ class Vector(QtGui.QVector3D): yield(self.x()) yield(self.y()) yield(self.z()) + + def angle(self, a): + """Returns the angle in degrees between this vector and the vector a.""" + n1 = self.length() + n2 = a.length() + if n1 == 0. or n2 == 0.: + return None + ## Probably this should be done with arctan2 instead.. + ang = np.arccos(np.clip(QtGui.QVector3D.dotProduct(self, a) / (n1 * n2), -1.0, 1.0)) ### in radians +# c = self.crossProduct(a) +# if c > 0: +# ang *= -1. + return ang * 180. / np.pi + + \ No newline at end of file diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index ae0fa4ca..ab2dfa41 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -242,7 +242,6 @@ class MeshData(object): v = self.vertexes(indexed='faces') self._faceNormals = np.cross(v[:,1]-v[:,0], v[:,2]-v[:,0]) - if indexed is None: return self._faceNormals elif indexed == 'faces': @@ -519,20 +518,17 @@ class MeshData(object): return MeshData(vertexes=verts, faces=faces) @staticmethod - def cylinder(rows, cols, radius=[1.0, 1.0], length=1.0, offset=False, ends=False): + def cylinder(rows, cols, radius=[1.0, 1.0], length=1.0, offset=False): """ Return a MeshData instance with vertexes and faces computed for a cylindrical surface. The cylinder may be tapered with different radii at each end (truncated cone) - ends are open if ends = False - No closed ends implemented yet... - The easiest way may be to add a vertex at the top and bottom in the center of the face? """ verts = np.empty((rows+1, cols, 3), dtype=float) if isinstance(radius, int): radius = [radius, radius] # convert to list ## compute vertexes - th = ((np.arange(cols) * 2 * np.pi / cols).reshape(1, cols)) # angle around + th = np.linspace(2 * np.pi, 0, cols).reshape(1, cols) r = (np.linspace(radius[0],radius[1],num=rows+1, endpoint=True)).reshape(rows+1, 1) # radius as a function of z verts[...,2] = np.linspace(-length/2.0, length/2.0, num=rows+1, endpoint=True).reshape(rows+1, 1) # z if offset: From 283a568693736287a7f000f7f0122602b2463fb0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 11 Feb 2014 23:13:31 -0500 Subject: [PATCH 106/268] Cylinder base moved to z=0 --- pyqtgraph/opengl/MeshData.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index ab2dfa41..9da43019 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -529,8 +529,8 @@ class MeshData(object): radius = [radius, radius] # convert to list ## compute vertexes th = np.linspace(2 * np.pi, 0, cols).reshape(1, cols) - r = (np.linspace(radius[0],radius[1],num=rows+1, endpoint=True)).reshape(rows+1, 1) # radius as a function of z - verts[...,2] = np.linspace(-length/2.0, length/2.0, num=rows+1, endpoint=True).reshape(rows+1, 1) # z + r = np.linspace(radius[0],radius[1],num=rows+1, endpoint=True).reshape(rows+1, 1) # radius as a function of z + verts[...,2] = np.linspace(0, length, num=rows+1, endpoint=True).reshape(rows+1, 1) # z if offset: th = th + ((np.pi / cols) * np.arange(rows+1).reshape(rows+1,1)) ## rotate each row by 1/2 column verts[...,0] = r * np.cos(th) # x = r cos(th) From 9677b1492b945f98fadc61763c2b36fa3b18a00c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 12 Feb 2014 02:16:00 -0500 Subject: [PATCH 107/268] Added ViewBox.setBackgroundColor() --- CHANGELOG | 1 + pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 024abc52..cf7a4efd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -35,6 +35,7 @@ pyqtgraph-0.9.9 [unreleased] when plots do not fit inside the widget. - Added BarGraphItem.shape() to allow better mouse interaction - Added MeshData.cylinder + - Added ViewBox.setBackgroundColor() Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 06e0ec1f..adc089a7 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -285,6 +285,15 @@ class ViewBox(GraphicsWidget): self.updateViewRange() self.sigStateChanged.emit(self) + def setBackgroundColor(self, color): + """ + Set the background color of the ViewBox. + + If color is None, then no background will be drawn. + """ + self.background.setVisible(color is not None) + self.state['background'] = color + self.updateBackground() def setMouseMode(self, mode): """ From 1ecceeaa9248c180133b8a52edf7ada29b43931c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 12 Feb 2014 03:02:09 -0500 Subject: [PATCH 108/268] Fixed unicode titles in Dock --- CHANGELOG | 1 + pyqtgraph/dockarea/Dock.py | 3 ++- pyqtgraph/dockarea/tests/test_dock.py | 16 ++++++++++++++++ pyqtgraph/python2_3.py | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 pyqtgraph/dockarea/tests/test_dock.py diff --git a/CHANGELOG b/CHANGELOG index cf7a4efd..faabf931 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -61,6 +61,7 @@ pyqtgraph-0.9.9 [unreleased] - Fixed ImageItem exception building histogram when image has only one value - Fixed MeshData exception caused when vertexes have no matching faces - Fixed GLViewWidget exception handler + - Fixed unicode support in Dock pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index f83397c7..d3cfcbb6 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -2,6 +2,7 @@ from ..Qt import QtCore, QtGui from .DockDrop import * from ..widgets.VerticalLabel import VerticalLabel +from ..python2_3 import asUnicode class Dock(QtGui.QWidget, DockDrop): @@ -167,7 +168,7 @@ class Dock(QtGui.QWidget, DockDrop): self.resizeOverlay(self.size()) def name(self): - return str(self.label.text()) + return asUnicode(self.label.text()) def container(self): return self._container diff --git a/pyqtgraph/dockarea/tests/test_dock.py b/pyqtgraph/dockarea/tests/test_dock.py new file mode 100644 index 00000000..949f3f0e --- /dev/null +++ b/pyqtgraph/dockarea/tests/test_dock.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +#import sip +#sip.setapi('QString', 1) + +import pyqtgraph as pg +pg.mkQApp() + +import pyqtgraph.dockarea as da + +def test_dock(): + name = pg.asUnicode("évènts_zàhéér") + dock = da.Dock(name=name) + # make sure unicode names work correctly + assert dock.name() == name + # no surprises in return type. + assert type(dock.name()) == type(name) diff --git a/pyqtgraph/python2_3.py b/pyqtgraph/python2_3.py index 2182d3a1..b1c46f26 100644 --- a/pyqtgraph/python2_3.py +++ b/pyqtgraph/python2_3.py @@ -1,5 +1,5 @@ """ -Helper functions which smooth out the differences between python 2 and 3. +Helper functions that smooth out the differences between python 2 and 3. """ import sys From 24cf9ada2c92708fbf6bae944d60dacb9972044f Mon Sep 17 00:00:00 2001 From: Sol Simpson Date: Wed, 12 Feb 2014 04:04:06 -0500 Subject: [PATCH 109/268] BF: unicode fix for TableWidget Setting a table item value to a unicode value with non ascii chars would cause the tablewidget contents to not be displayed (no exception thrown either). Changed all instances of str(..) or map(str,...) to use asUnicode and issue seems to be fixed --- pyqtgraph/widgets/TableWidget.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 03392648..d28d07c3 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -103,19 +103,19 @@ class TableWidget(QtGui.QTableWidget): if isinstance(data, list) or isinstance(data, tuple): return lambda d: d.__iter__(), None elif isinstance(data, dict): - return lambda d: iter(d.values()), list(map(str, data.keys())) + return lambda d: iter(d.values()), list(map(asUnicode, data.keys())) elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): if data.axisHasColumns(0): - header = [str(data.columnName(0, i)) for i in range(data.shape[0])] + header = [asUnicode(data.columnName(0, i)) for i in range(data.shape[0])] elif data.axisHasValues(0): - header = list(map(str, data.xvals(0))) + header = list(map(asUnicode, data.xvals(0))) else: header = None return self.iterFirstAxis, header elif isinstance(data, np.ndarray): return self.iterFirstAxis, None elif isinstance(data, np.void): - return self.iterate, list(map(str, data.dtype.names)) + return self.iterate, list(map(asUnicode, data.dtype.names)) elif data is None: return (None,None) else: @@ -239,7 +239,7 @@ class TableWidgetItem(QtGui.QTableWidgetItem): if isinstance(val, float) or isinstance(val, np.floating): s = "%0.3g" % val else: - s = str(val) + s = asUnicode(val) QtGui.QTableWidgetItem.__init__(self, s) self.value = val flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled From 210d07027e8c303f967d9478b88d6c7b0687be2d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 12 Feb 2014 11:31:58 -0500 Subject: [PATCH 110/268] ImageView updates to improve subclassing flexibility: - Allow non-ndarray image data - Make quickMinMax a normal method --- pyqtgraph/imageview/ImageView.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index c50a54c0..bf415bb3 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -196,7 +196,12 @@ class ImageView(QtGui.QWidget): img = img.asarray() if not isinstance(img, np.ndarray): - raise Exception("Image must be specified as ndarray.") + required = ['dtype', 'max', 'min', 'ndim', 'shape', 'size'] + if not all([hasattr(img, attr) for attr in required]): + raise TypeError("Image must be NumPy array or any object " + "that provides compatible attributes/methods:\n" + " %s" % str(required)) + self.image = img self.imageDisp = None @@ -319,11 +324,10 @@ class ImageView(QtGui.QWidget): if self.imageDisp is None: image = self.normalize(self.image) self.imageDisp = image - self.levelMin, self.levelMax = list(map(float, ImageView.quickMinMax(self.imageDisp))) + self.levelMin, self.levelMax = list(map(float, self.quickMinMax(self.imageDisp))) return self.imageDisp - def close(self): """Closes the widget nicely, making sure to clear the graphics scene and release memory.""" self.ui.roiPlot.close() @@ -375,7 +379,6 @@ class ImageView(QtGui.QWidget): else: QtGui.QWidget.keyReleaseEvent(self, ev) - def evalKeyState(self): if len(self.keysPressed) == 1: key = list(self.keysPressed.keys())[0] @@ -399,16 +402,13 @@ class ImageView(QtGui.QWidget): else: self.play(0) - def timeout(self): now = ptime.time() dt = now - self.lastPlayTime if dt < 0: return n = int(self.playRate * dt) - #print n, dt if n != 0: - #print n, dt, self.lastPlayTime self.lastPlayTime += (float(n)/self.playRate) if self.currentIndex+n > self.image.shape[0]: self.play(0) @@ -433,17 +433,14 @@ class ImageView(QtGui.QWidget): self.autoLevels() self.roiChanged() self.sigProcessingChanged.emit(self) - def updateNorm(self): if self.ui.normTimeRangeCheck.isChecked(): - #print "show!" self.normRgn.show() else: self.normRgn.hide() if self.ui.normROICheck.isChecked(): - #print "show!" self.normRoi.show() else: self.normRoi.hide() @@ -519,12 +516,11 @@ class ImageView(QtGui.QWidget): coords = coords - coords[:,0,np.newaxis] xvals = (coords**2).sum(axis=0) ** 0.5 self.roiCurve.setData(y=data, x=xvals) - - #self.ui.roiPlot.replot() - - @staticmethod - def quickMinMax(data): + def quickMinMax(self, data): + """ + Estimate the min/max values of *data* by subsampling. + """ while data.size > 1e6: ax = np.argmax(data.shape) sl = [slice(None)] * data.ndim @@ -533,7 +529,12 @@ class ImageView(QtGui.QWidget): return data.min(), data.max() def normalize(self, image): + """ + Process *image* using the normalization options configured in the + control panel. + This can be repurposed to process any data through the same filter. + """ if self.ui.normOffRadio.isChecked(): return image From ddaa07afb15ed75750e3099b07872ede74c8d97d Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Wed, 12 Feb 2014 20:49:53 +0100 Subject: [PATCH 111/268] "Arguments" added again (copy/paste issue) --- pyqtgraph/graphicsItems/ArrowItem.py | 2 +- pyqtgraph/graphicsItems/LabelItem.py | 2 +- pyqtgraph/graphicsItems/ScatterPlotItem.py | 4 ++-- pyqtgraph/parametertree/Parameter.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index 74066fd7..7f6abbed 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -48,7 +48,7 @@ class ArrowItem(QtGui.QGraphicsPathItem): All arguments are optional: ====================== ================================================= - **Keyword** + **Keyword Arguments** angle Orientation of the arrow in degrees. Default is 0; arrow pointing to the left. headLen Length of the arrow head, from tip to base. diff --git a/pyqtgraph/graphicsItems/LabelItem.py b/pyqtgraph/graphicsItems/LabelItem.py index 57127a07..37980ee3 100644 --- a/pyqtgraph/graphicsItems/LabelItem.py +++ b/pyqtgraph/graphicsItems/LabelItem.py @@ -38,7 +38,7 @@ class LabelItem(GraphicsWidget, GraphicsWidgetAnchor): a CSS style string: ==================== ============================== - **Style** + **Style Arguments:** color (str) example: 'CCFF00' size (str) example: '8pt' bold (bool) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 5310cf4d..1c11fcf9 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -253,13 +253,13 @@ class ScatterPlotItem(GraphicsObject): def setData(self, *args, **kargs): """ - **Ordered** + **Ordered Arguments:** * If there is only one unnamed argument, it will be interpreted like the 'spots' argument. * If there are two unnamed arguments, they will be interpreted as sequences of x and y values. ====================== =============================================================================================== - **Keyword** + **Keyword Arguments:** *spots* Optional list of dicts. Each dict specifies parameters for a single spot: {'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}. This is just an alternate method of passing in data for the corresponding arguments. diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index f7cb42a0..1c75c333 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -108,7 +108,7 @@ class Parameter(QtCore.QObject): by most Parameter subclasses. ======================= ========================================================= - **Keyword** + **Keyword Arguments:** name The name to give this Parameter. This is the name that will appear in the left-most column of a ParameterTree for this Parameter. From ce0fb140e8289f0fc2d23f0a548148fc0b1d8c6f Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Wed, 12 Feb 2014 21:03:39 +0100 Subject: [PATCH 112/268] ":" added again (copy/paste issue) --- pyqtgraph/graphicsItems/ArrowItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index 7f6abbed..77e6195f 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -48,7 +48,7 @@ class ArrowItem(QtGui.QGraphicsPathItem): All arguments are optional: ====================== ================================================= - **Keyword Arguments** + **Keyword Arguments:** angle Orientation of the arrow in degrees. Default is 0; arrow pointing to the left. headLen Length of the arrow head, from tip to base. From 9f5954641dc75ccc98b858d6cd7a5ae060893ac6 Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Wed, 12 Feb 2014 21:06:54 +0100 Subject: [PATCH 113/268] ":" added --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index b035bce7..4f5a3d8f 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -83,7 +83,7 @@ class PlotItem(GraphicsWidget): The ViewBox itself can be accessed by calling :func:`getViewBox() ` ==================== ======================================================================= - **Signals** + **Signals:** sigYRangeChanged wrapped from :class:`ViewBox ` sigXRangeChanged wrapped from :class:`ViewBox ` sigRangeChanged wrapped from :class:`ViewBox ` From af106e324587ec2d15412116077e6d23453877c4 Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Wed, 12 Feb 2014 21:18:36 +0100 Subject: [PATCH 114/268] Minor Arguments list alignment in setDownSampling --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 4f5a3d8f..16529fef 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -931,7 +931,7 @@ class PlotItem(GraphicsWidget): def setDownsampling(self, ds=None, auto=None, mode=None): """Change the default downsampling mode for all PlotDataItems managed by this plot. - ============== ================================================================= + =============== ================================================================= **Arguments:** ds (int) Reduce visible plot samples by this factor, or (bool) To enable/disable downsampling without changing the value. @@ -942,7 +942,7 @@ class PlotItem(GraphicsWidget): 'peak': Downsample by drawing a saw wave that follows the min and max of the original data. This method produces the best visual representation of the data but is slower. - ============= ================================================================= + =============== ================================================================= """ if ds is not None: if ds is False: From 17fdd51b5580e13ab03460402b2fdf7755545c3f Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Wed, 12 Feb 2014 21:19:29 +0100 Subject: [PATCH 115/268] malformed Signal list of class ROI --- pyqtgraph/graphicsItems/ROI.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index b99465b5..ff74eed3 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -38,9 +38,9 @@ def rectStr(r): class ROI(GraphicsObject): """Generic region-of-interest widget. Can be used for implementing many types of selection box with rotate/translate/scale handles. - - Signals - ----------------------- ---------------------------------------------------- + + ======================= ==================================================== + **Signals:** sigRegionChangeFinished Emitted when the user stops dragging the ROI (or one of its handles) or if the ROI is changed programatically. @@ -58,7 +58,7 @@ class ROI(GraphicsObject): details. sigRemoveRequested Emitted when the user selects 'remove' from the ROI's context menu (if available). - ----------------------- ---------------------------------------------------- + ======================= ==================================================== """ sigRegionChangeFinished = QtCore.Signal(object) From 78d92b383f63167a9a98afb84c8300dc38c055c8 Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Wed, 12 Feb 2014 21:25:31 +0100 Subject: [PATCH 116/268] Aditional ":" added, minor list alignments --- examples/GraphicsLayout.py | 2 ++ pyqtgraph/graphicsItems/GradientEditorItem.py | 2 +- pyqtgraph/graphicsItems/InfiniteLine.py | 6 +++--- pyqtgraph/widgets/ColorButton.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/GraphicsLayout.py b/examples/GraphicsLayout.py index 70da7e5c..6917e54e 100644 --- a/examples/GraphicsLayout.py +++ b/examples/GraphicsLayout.py @@ -20,6 +20,8 @@ view.show() view.setWindowTitle('pyqtgraph example: GraphicsLayout') view.resize(800,600) + + ## Title at top text = """ This example demonstrates the use of GraphicsLayout to arrange items in a grid.
diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index 92a4a672..e16370f5 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -349,7 +349,7 @@ class GradientEditorItem(TickSliderItem): with a GradientEditorItem that can be added to a GUI. ================================ =========================================================== - **Signals** + **Signals:** sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal is emitted in real time while ticks are being dragged or colors are being changed. diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index df1d5bda..08a55f83 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -15,7 +15,7 @@ class InfiniteLine(GraphicsObject): This line may be dragged to indicate a position in data coordinates. =============================== =================================================== - **Signals** + **Signals:** sigDragged(self) sigPositionChangeFinished(self) sigPositionChanged(self) @@ -28,7 +28,7 @@ class InfiniteLine(GraphicsObject): def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None): """ - ============== ================================================================== + =============== ================================================================== **Arguments:** pos Position of the line. This can be a QPointF or a single value for vertical/horizontal lines. @@ -39,7 +39,7 @@ class InfiniteLine(GraphicsObject): movable If True, the line can be dragged to a new position by the user. bounds Optional [min, max] bounding values. Bounds are only valid if the line is vertical or horizontal. - ============= ================================================================== + =============== ================================================================== """ GraphicsObject.__init__(self) diff --git a/pyqtgraph/widgets/ColorButton.py b/pyqtgraph/widgets/ColorButton.py index 40f6740f..a0bb0c8e 100644 --- a/pyqtgraph/widgets/ColorButton.py +++ b/pyqtgraph/widgets/ColorButton.py @@ -11,7 +11,7 @@ class ColorButton(QtGui.QPushButton): Button displaying a color and allowing the user to select a new color. ====================== ============================================================ - **Signals**: + **Signals:** sigColorChanging(self) emitted whenever a new color is picked in the color dialog sigColorChanged(self) emitted when the selected color is accepted (user clicks OK) ====================== ============================================================ From 57c36a953d4a79faa02ceee01cbb7c98a2405f20 Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Sat, 15 Feb 2014 11:26:24 +0100 Subject: [PATCH 117/268] malformed Signal list of function setData --- pyqtgraph/opengl/items/GLScatterPlotItem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index 123ff0e3..bb2c89a3 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -29,7 +29,6 @@ class GLScatterPlotItem(GLGraphicsItem): ==================== ================================================== **Arguments:** - ------------------------------------------------------------------------ pos (N,3) array of floats specifying point locations. color (N,4) array of floats (0.0-1.0) specifying spot colors OR a tuple of floats specifying From da4bb3df23e8b8d8573a257242937226ea05f0aa Mon Sep 17 00:00:00 2001 From: tommy3001 Date: Sat, 15 Feb 2014 11:42:36 +0100 Subject: [PATCH 118/268] List alignments class WidgetParameterItem --- pyqtgraph/parametertree/parameterTypes.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index f58145dd..92eca90f 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -18,16 +18,16 @@ class WidgetParameterItem(ParameterItem): * simple widget for editing value (displayed instead of label when item is selected) * button that resets value to default - ================= ============================================================= - Registered Types: - int Displays a :class:`SpinBox ` in integer - mode. - float Displays a :class:`SpinBox `. - bool Displays a QCheckBox - str Displays a QLineEdit - color Displays a :class:`ColorButton ` - colormap Displays a :class:`GradientWidget ` - ================= ============================================================= + ========================== ============================================================= + **Registered Types:** + int Displays a :class:`SpinBox ` in integer + mode. + float Displays a :class:`SpinBox `. + bool Displays a QCheckBox + str Displays a QLineEdit + color Displays a :class:`ColorButton ` + colormap Displays a :class:`GradientWidget ` + ========================== ============================================================= This class can be subclassed by overriding makeWidget() to provide a custom widget. """ From d67464af72166ed50828272ad6a804474f4ffcbb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 17 Feb 2014 20:02:03 -0500 Subject: [PATCH 119/268] Update imageToArray to support Py2.6 + Qt 4.10 --- pyqtgraph/functions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 99c45606..7687814f 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -993,6 +993,10 @@ def imageToArray(img, copy=False, transpose=True): else: ptr.setsize(img.byteCount()) arr = np.asarray(ptr) + if img.byteCount() != arr.size * arr.itemsize: + # Required for Python 2.6, PyQt 4.10 + # If this works on all platforms, then there is no need to use np.asarray.. + arr = np.frombuffer(ptr, np.ubyte, img.byteCount()) if fmt == img.Format_RGB32: arr = arr.reshape(img.height(), img.width(), 3) From 036cadb44c837fae5b3e0a6075778cedc067f2c7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 17 Feb 2014 20:02:42 -0500 Subject: [PATCH 120/268] pg.dbg() now returns pointer to console widget --- pyqtgraph/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 588de0cd..5f42e64f 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -393,6 +393,7 @@ def dbg(*args, **kwds): consoles.append(c) except NameError: consoles = [c] + return c def mkQApp(): From 5b6bc6715c5c0d777057771487ab3dcb2b716a4b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Feb 2014 08:54:33 -0500 Subject: [PATCH 121/268] Added GLViewWidget.setBackgroundColor() --- pyqtgraph/opengl/GLViewWidget.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 99a43da9..c71bb3c9 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -36,6 +36,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): ## (rotation around z-axis 0 points along x-axis) 'viewport': None, ## glViewport params; None == whole widget } + self.setBackgroundColor('k') self.items = [] self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown] self.keysPressed = {} @@ -64,9 +65,16 @@ class GLViewWidget(QtOpenGL.QGLWidget): def initializeGL(self): - glClearColor(0.0, 0.0, 0.0, 0.0) self.resizeGL(self.width(), self.height()) + def setBackgroundColor(self, *args, **kwds): + """ + Set the background color of the widget. Accepts the same arguments as + pg.mkColor(). + """ + self.opts['bgcolor'] = fn.mkColor(*args, **kwds) + self.update() + def getViewport(self): vp = self.opts['viewport'] if vp is None: @@ -164,6 +172,8 @@ class GLViewWidget(QtOpenGL.QGLWidget): glViewport(*viewport) self.setProjection(region=region) self.setModelview() + bgcolor = self.opts['bgcolor'] + glClearColor(bgcolor.red(), bgcolor.green(), bgcolor.blue(), 1.0) glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) self.drawItemTree(useItemNames=useItemNames) From ab0729bb04bb143148e53807f8657e54adea7f6c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Feb 2014 08:55:30 -0500 Subject: [PATCH 122/268] Add check for even array length when using arrayToQPath(connect='pairs') --- pyqtgraph/functions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 64dad102..db22f8a3 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1157,6 +1157,8 @@ def arrayToQPath(x, y, connect='all'): # decide which points are connected by lines if connect == 'pairs': connect = np.empty((n/2,2), dtype=np.int32) + if connect.size != n: + raise Exception("x,y array lengths must be multiple of 2 to use connect='pairs'") connect[:,0] = 1 connect[:,1] = 0 connect = connect.flatten() From 37adecc06e1ceaa4b4110aa85b1a6c1c5795b268 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Feb 2014 09:54:12 -0500 Subject: [PATCH 123/268] udpate changelog --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 79c9d102..8b6efc67 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,7 +36,7 @@ pyqtgraph-0.9.9 [unreleased] when plots do not fit inside the widget. - Added BarGraphItem.shape() to allow better mouse interaction - Added MeshData.cylinder - - Added ViewBox.setBackgroundColor() + - Added ViewBox.setBackgroundColor() and GLViewWidget.setBackgroundColor() Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px From 6255dca99cd8ed3dc94ff086ec65df3f45b20c65 Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Wed, 26 Feb 2014 10:20:24 -0500 Subject: [PATCH 124/268] MouseClickEvent: sometimes __repr__ could cause an exception This happens during debugging session in an IDE (eric5) when debugger inspects variables but self._scenePos is not yet initialized. --- pyqtgraph/GraphicsScene/mouseEvents.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/GraphicsScene/mouseEvents.py b/pyqtgraph/GraphicsScene/mouseEvents.py index f337a657..fa9bc36d 100644 --- a/pyqtgraph/GraphicsScene/mouseEvents.py +++ b/pyqtgraph/GraphicsScene/mouseEvents.py @@ -221,9 +221,12 @@ class MouseClickEvent(object): return self._modifiers def __repr__(self): - p = self.pos() - return "" % (p.x(), p.y(), int(self.button())) - + try: + p = self.pos() + return "" % (p.x(), p.y(), int(self.button())) + except: + return "" % (int(self.button())) + def time(self): return self._time From 953b9e412915ee7096b94f46d6ff2a3410f02265 Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Fri, 28 Feb 2014 16:16:13 -0500 Subject: [PATCH 125/268] Make signature of the setPen method consistent --- pyqtgraph/flowchart/Node.py | 4 ++-- pyqtgraph/graphicsItems/AxisItem.py | 6 +++--- pyqtgraph/graphicsItems/GraphItem.py | 4 ++-- pyqtgraph/graphicsItems/InfiniteLine.py | 4 ++-- pyqtgraph/graphicsItems/ROI.py | 4 ++-- pyqtgraph/widgets/PathButton.py | 7 +++---- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py index b6ed1e0f..da130c8c 100644 --- a/pyqtgraph/flowchart/Node.py +++ b/pyqtgraph/flowchart/Node.py @@ -501,8 +501,8 @@ class NodeGraphicsItem(GraphicsObject): bounds = self.boundingRect() self.nameItem.setPos(bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0) - def setPen(self, pen): - self.pen = pen + def setPen(self, *args, **kwargs): + self.pen = fn.mkPen(*args, **kwargs) self.update() def setBrush(self, brush): diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 66efeda5..a2176f8b 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -271,16 +271,16 @@ class AxisItem(GraphicsWidget): return fn.mkPen(getConfigOption('foreground')) return fn.mkPen(self._pen) - def setPen(self, pen): + def setPen(self, *args, **kwargs): """ Set the pen used for drawing text, axes, ticks, and grid lines. if pen == None, the default will be used (see :func:`setConfigOption `) """ self.picture = None - if pen is None: + if not (args or kwargs): pen = getConfigOption('foreground') - self._pen = fn.mkPen(pen) + self._pen = fn.mkPen(*args, **kwargs) self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6] self.setLabel() self.update() diff --git a/pyqtgraph/graphicsItems/GraphItem.py b/pyqtgraph/graphicsItems/GraphItem.py index 97759522..1e64fd52 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -67,8 +67,8 @@ class GraphItem(GraphicsObject): self.scatter.setData(**kwds) self.informViewBoundsChanged() - def setPen(self, pen): - self.pen = pen + def setPen(self, *args, **kwargs): + self.pen = fn.mkPen(*args, **kwargs) self.picture = None def generatePicture(self): diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index edf6b19e..d86883b0 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -73,10 +73,10 @@ class InfiniteLine(GraphicsObject): self.maxRange = bounds self.setValue(self.value()) - def setPen(self, pen): + def setPen(self, *args, **kwargs): """Set the pen for drawing the line. Allowable arguments are any that are valid for :func:`mkPen `.""" - self.pen = fn.mkPen(pen) + self.pen = fn.mkPen(*args, **kwargs) self.currentPen = self.pen self.update() diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index b99465b5..27fb7110 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -137,8 +137,8 @@ class ROI(GraphicsObject): def parentBounds(self): return self.mapToParent(self.boundingRect()).boundingRect() - def setPen(self, pen): - self.pen = fn.mkPen(pen) + def setPen(self, *args, **kwargs): + self.pen = fn.mkPen(*args, **kwargs) self.currentPen = self.pen self.update() diff --git a/pyqtgraph/widgets/PathButton.py b/pyqtgraph/widgets/PathButton.py index 0c62bb1b..52c60e20 100644 --- a/pyqtgraph/widgets/PathButton.py +++ b/pyqtgraph/widgets/PathButton.py @@ -23,8 +23,8 @@ class PathButton(QtGui.QPushButton): def setBrush(self, brush): self.brush = fn.mkBrush(brush) - def setPen(self, pen): - self.pen = fn.mkPen(pen) + def setPen(self, *args, **kwargs): + self.pen = fn.mkPen(*args, **kwargs) def setPath(self, path): self.path = path @@ -46,6 +46,5 @@ class PathButton(QtGui.QPushButton): p.setBrush(self.brush) p.drawPath(self.path) p.end() - + - \ No newline at end of file From 43ec2bcd2ce804d214453e9128b6380d204a7597 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Feb 2014 18:24:01 -0500 Subject: [PATCH 126/268] Expanded ROI documentation --- pyqtgraph/graphicsItems/ROI.py | 410 ++++++++++++++++++++++++++++----- 1 file changed, 348 insertions(+), 62 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index b99465b5..ef2f329b 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -36,8 +36,15 @@ def rectStr(r): return "[%f, %f] + [%f, %f]" % (r.x(), r.y(), r.width(), r.height()) class ROI(GraphicsObject): - """Generic region-of-interest widget. - Can be used for implementing many types of selection box with rotate/translate/scale handles. + """ + Generic region-of-interest widget. + + Can be used for implementing many types of selection box with + rotate/translate/scale handles. + ROIs can be customized to have a variety of shapes (by subclassing or using + any of the built-in subclasses) and any combination of draggable handles + that allow the user to manibulate the ROI. + Signals ----------------------- ---------------------------------------------------- @@ -59,6 +66,42 @@ class ROI(GraphicsObject): sigRemoveRequested Emitted when the user selects 'remove' from the ROI's context menu (if available). ----------------------- ---------------------------------------------------- + + + Arguments + ---------------- ----------------------------------------------------------- + pos (length-2 sequence) Indicates the position of the ROI's + origin. For most ROIs, this is the lower-left corner of + its bounding rectangle. + size (length-2 sequence) Indicates the width and height of the + ROI. + angle (float) The rotation of the ROI in degrees. Default is 0. + invertible (bool) If True, the user may resize the ROI to have + negative width or height (assuming the ROI has scale + handles). Default is False. + maxBounds (QRect, QRectF, or None) Specifies boundaries that the ROI + cannot be dragged outside of by the user. Default is None. + snapSize (float) The spacing of snap positions used when *scaleSnap* + or *translateSnap* are enabled. Default is 1.0. + scaleSnap (bool) If True, the width and height of the ROI are forced + to be integer multiples of *snapSize* when being resized + by the user. Default is False. + translateSnap (bool) If True, the x and y positions of the ROI are forced + to be integer multiples of *snapSize* when being resized + by the user. Default is False. + rotateSnap (bool) If True, the ROI angle is forced to a multiple of + 15 degrees when rotated by the user. Default is False. + parent (QGraphicsItem) The graphics item parent of this ROI. It + is generally not necessary to specify the parent. + pen (QPen or argument to pg.mkPen) The pen to use when drawing + the shape of the ROI. + movable (bool) If True, the ROI can be moved by dragging anywhere + inside the ROI. Default is True. + removable (bool) If True, the ROI will be given a context menu with + an option to remove the ROI. The ROI emits + sigRemoveRequested when this menu action is selected. + Default is False. + ---------------- ----------------------------------------------------------- """ sigRegionChangeFinished = QtCore.Signal(object) @@ -117,7 +160,11 @@ class ROI(GraphicsObject): return sc def saveState(self): - """Return the state of the widget in a format suitable for storing to disk. (Points are converted to tuple)""" + """Return the state of the widget in a format suitable for storing to + disk. (Points are converted to tuple) + + Combined with setState(), this allows ROIs to be easily saved and + restored.""" state = {} state['pos'] = tuple(self.state['pos']) state['size'] = tuple(self.state['size']) @@ -125,6 +172,10 @@ class ROI(GraphicsObject): return state def setState(self, state, update=True): + """ + Set the state of the ROI from a structure generated by saveState() or + getState(). + """ self.setPos(state['pos'], update=False) self.setSize(state['size'], update=False) self.setAngle(state['angle'], update=update) @@ -135,20 +186,31 @@ class ROI(GraphicsObject): h['item'].setZValue(z+1) def parentBounds(self): + """ + Return the bounding rectangle of this ROI in the coordinate system + of its parent. + """ return self.mapToParent(self.boundingRect()).boundingRect() def setPen(self, pen): + """ + Set the pen to use when drawing the ROI shape. + """ self.pen = fn.mkPen(pen) self.currentPen = self.pen self.update() def size(self): + """Return the size (w,h) of the ROI.""" return self.getState()['size'] def pos(self): + """Return the position (x,y) of the ROI's origin. + For most ROIs, this will be the lower-left corner.""" return self.getState()['pos'] def angle(self): + """Return the angle of the ROI in degrees.""" return self.getState()['angle'] def setPos(self, pos, update=True, finish=True): @@ -264,21 +326,86 @@ class ROI(GraphicsObject): #self.stateChanged() def rotate(self, angle, update=True, finish=True): + """ + Rotate the ROI by *angle* degrees. + + Also accepts *update* and *finish* arguments (see setPos() for a + description of these). + """ self.setAngle(self.angle()+angle, update=update, finish=finish) def handleMoveStarted(self): self.preMoveState = self.getState() def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None): + """ + Add a new translation handle to the ROI. Dragging the handle will move + the entire ROI without changing its angle or shape. + + Note that, by default, ROIs may be moved by dragging anywhere inside the + ROI. However, for larger ROIs it may be desirable to disable this and + instead provide one or more translation handles. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ pos = Point(pos) return self.addHandle({'name': name, 'type': 't', 'pos': pos, 'item': item}, index=index) def addFreeHandle(self, pos=None, axes=None, item=None, name=None, index=None): + """ + Add a new free handle to the ROI. Dragging free handles has no effect + on the position or shape of the ROI. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ if pos is not None: pos = Point(pos) return self.addHandle({'name': name, 'type': 'f', 'pos': pos, 'item': item}, index=index) def addScaleHandle(self, pos, center, axes=None, item=None, name=None, lockAspect=False, index=None): + """ + Add a new scale handle to the ROI. Dragging a scale handle allows the + user to change the height and/or width of the ROI. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + center (length-2 sequence) The center point around which + scaling takes place. If the center point has the + same x or y value as the handle position, then + scaling will be disabled for that axis. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ pos = Point(pos) center = Point(center) info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item, 'lockAspect': lockAspect} @@ -289,11 +416,51 @@ class ROI(GraphicsObject): return self.addHandle(info, index=index) def addRotateHandle(self, pos, center, item=None, name=None, index=None): + """ + Add a new rotation handle to the ROI. Dragging a rotation handle allows + the user to change the angle of the ROI. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + center (length-2 sequence) The center point around which + rotation takes place. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ pos = Point(pos) center = Point(center) return self.addHandle({'name': name, 'type': 'r', 'center': center, 'pos': pos, 'item': item}, index=index) def addScaleRotateHandle(self, pos, center, item=None, name=None, index=None): + """ + Add a new scale+rotation handle to the ROI. When dragging a handle of + this type, the user can simultaneously rotate the ROI around an + arbitrary center point as well as scale the ROI by dragging the handle + toward or away from the center point. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + center (length-2 sequence) The center point around which + scaling and rotation take place. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ pos = Point(pos) center = Point(center) if pos[0] != center[0] and pos[1] != center[1]: @@ -301,6 +468,27 @@ class ROI(GraphicsObject): return self.addHandle({'name': name, 'type': 'sr', 'center': center, 'pos': pos, 'item': item}, index=index) def addRotateFreeHandle(self, pos, center, axes=None, item=None, name=None, index=None): + """ + Add a new rotation+free handle to the ROI. When dragging a handle of + this type, the user can rotate the ROI around an + arbitrary center point, while moving toward or away from the center + point has no effect on the shape of the ROI. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + center (length-2 sequence) The center point around which + rotation takes place. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ pos = Point(pos) center = Point(center) return self.addHandle({'name': name, 'type': 'rf', 'center': center, 'pos': pos, 'item': item}, index=index) @@ -329,6 +517,9 @@ class ROI(GraphicsObject): return h def indexOfHandle(self, handle): + """ + Return the index of *handle* in the list of this ROI's handles. + """ if isinstance(handle, Handle): index = [i for i, info in enumerate(self.handles) if info['item'] is handle] if len(index) == 0: @@ -338,7 +529,8 @@ class ROI(GraphicsObject): return handle def removeHandle(self, handle): - """Remove a handle from this ROI. Argument may be either a Handle instance or the integer index of the handle.""" + """Remove a handle from this ROI. Argument may be either a Handle + instance or the integer index of the handle.""" index = self.indexOfHandle(handle) handle = self.handles[index]['item'] @@ -349,20 +541,17 @@ class ROI(GraphicsObject): self.stateChanged() def replaceHandle(self, oldHandle, newHandle): - """Replace one handle in the ROI for another. This is useful when connecting multiple ROIs together. - *oldHandle* may be a Handle instance or the index of a handle.""" - #print "=========================" - #print "replace", oldHandle, newHandle - #print self - #print self.handles - #print "-----------------" + """Replace one handle in the ROI for another. This is useful when + connecting multiple ROIs together. + + *oldHandle* may be a Handle instance or the index of a handle to be + replaced.""" index = self.indexOfHandle(oldHandle) info = self.handles[index] self.removeHandle(index) info['item'] = newHandle info['pos'] = newHandle.pos() self.addHandle(info, index=index) - #print self.handles def checkRemoveHandle(self, handle): ## This is used when displaying a Handle's context menu to determine @@ -373,7 +562,10 @@ class ROI(GraphicsObject): def getLocalHandlePositions(self, index=None): - """Returns the position of a handle in ROI coordinates""" + """Returns the position of handles in the ROI's coordinate system. + + The format returned is a list of (name, pos) tuples. + """ if index == None: positions = [] for h in self.handles: @@ -383,6 +575,10 @@ class ROI(GraphicsObject): return (self.handles[index]['name'], self.handles[index]['pos']) def getSceneHandlePositions(self, index=None): + """Returns the position of handles in the scene coordinate system. + + The format returned is a list of (name, pos) tuples. + """ if index == None: positions = [] for h in self.handles: @@ -392,6 +588,9 @@ class ROI(GraphicsObject): return (self.handles[index]['name'], self.handles[index]['item'].scenePos()) def getHandles(self): + """ + Return a list of this ROI's Handles. + """ return [h['item'] for h in self.handles] def mapSceneToParent(self, pt): @@ -467,8 +666,6 @@ class ROI(GraphicsObject): self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self)) self.removeTimer.start(0) - - def mouseDragEvent(self, ev): if ev.isStart(): #p = ev.pos() @@ -510,56 +707,16 @@ class ROI(GraphicsObject): self.sigClicked.emit(self, ev) else: ev.ignore() - - - def cancelMove(self): self.isMoving = False self.setState(self.preMoveState) - - #def pointDragEvent(self, pt, ev): - ### just for handling drag start/stop. - ### drag moves are handled through movePoint() - - #if ev.isStart(): - #self.isMoving = True - #self.preMoveState = self.getState() - - #self.sigRegionChangeStarted.emit(self) - #elif ev.isFinish(): - #self.isMoving = False - #self.sigRegionChangeFinished.emit(self) - #return - - - #def pointPressEvent(self, pt, ev): - ##print "press" - #self.isMoving = True - #self.preMoveState = self.getState() - - ##self.emit(QtCore.SIGNAL('regionChangeStarted'), self) - #self.sigRegionChangeStarted.emit(self) - ##self.pressPos = self.mapFromScene(ev.scenePos()) - ##self.pressHandlePos = self.handles[pt]['item'].pos() - - #def pointReleaseEvent(self, pt, ev): - ##print "release" - #self.isMoving = False - ##self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - #self.sigRegionChangeFinished.emit(self) - - #def pointMoveEvent(self, pt, ev): - #self.movePoint(pt, ev.scenePos(), ev.modifiers()) - - def checkPointMove(self, handle, pos, modifiers): """When handles move, they must ask the ROI if the move is acceptable. By default, this always returns True. Subclasses may wish override. """ return True - def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True, coords='parent'): ## called by Handles when they are moved. @@ -804,7 +961,6 @@ class ROI(GraphicsObject): round(pos[1] / snap[1]) * snap[1] ) - def boundingRect(self): return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() @@ -871,7 +1027,25 @@ class ROI(GraphicsObject): return bounds, tr def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): - """Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array. + """Use the position and orientation of this ROI relative to an imageItem + to pull a slice from an array. + + **Arguments** + ------------------- ---------------------------------------------------- + data The array to slice from. Note that this array does + *not* have to be the same data that is represented + in *img*. + img (ImageItem or other suitable QGraphicsItem) + Used to determine the relationship between the + ROI and the boundaries of *data*. + axes (length-2 tuple) Specifies the axes in *data* that + correspond to the x and y axes of *img*. + returnMappedCoords (bool) If True, the array slice is returned along + with a corresponding array of coordinates that were + used to extract data from the original array. + **kwds All keyword arguments are passed to + :func:`affineSlice `. + ------------------- ---------------------------------------------------- This method uses :func:`affineSlice ` to generate the slice from *data* and uses :func:`getAffineSliceParams ` to determine the parameters to @@ -1088,7 +1262,18 @@ class ROI(GraphicsObject): class Handle(UIGraphicsItem): + """ + Handle represents a single user-interactable point attached to an ROI. They + are usually created by a call to one of the ROI.add___Handle() methods. + Handles are represented as a square, diamond, or circle, and are drawn with + fixed pixel size regardless of the scaling of the view they are displayed in. + + Handles may be dragged to change the position, size, orientation, or other + properties of the ROI they are attached to. + + + """ types = { ## defines number of sides, start angle for each handle type 't': (4, np.pi/4), 'f': (4, np.pi/4), @@ -1360,6 +1545,22 @@ class TestROI(ROI): class RectROI(ROI): + """ + Rectangular ROI subclass with a single scale handle at the top-right corner. + + **Arguments** + -------------- ------------------------------------------------------------- + pos (length-2 sequence) The position of the ROI origin. + See ROI(). + size (length-2 sequence) The size of the ROI. See ROI(). + centered (bool) If True, scale handles affect the ROI relative to its + center, rather than its origin. + sideScalers (bool) If True, extra scale handles are added at the top and + right edges. + **args All extra keyword arguments are passed to ROI() + -------------- ------------------------------------------------------------- + + """ def __init__(self, pos, size, centered=False, sideScalers=False, **args): #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) ROI.__init__(self, pos, size, **args) @@ -1375,6 +1576,22 @@ class RectROI(ROI): self.addScaleHandle([0.5, 1], [0.5, center[1]]) class LineROI(ROI): + """ + Rectangular ROI subclass with scale-rotate handles on either side. This + allows the ROI to be positioned as if moving the ends of a line segment. + A third handle controls the width of the ROI orthogonal to its "line" axis. + + **Arguments** + -------------- ------------------------------------------------------------- + pos1 (length-2 sequence) The position of the center of the ROI's + left edge. + pos2 (length-2 sequence) The position of the center of the ROI's + right edge. + width (float) The width of the ROI. + **args All extra keyword arguments are passed to ROI() + -------------- ------------------------------------------------------------- + + """ def __init__(self, pos1, pos2, width, **args): pos1 = Point(pos1) pos2 = Point(pos2) @@ -1399,6 +1616,13 @@ class MultiRectROI(QtGui.QGraphicsObject): This is generally used to mark a curved path through an image similarly to PolyLineROI. It differs in that each segment of the chain is rectangular instead of linear and thus has width. + + **Arguments** + -------------- ------------------------------------------------------------- + points (list of length-2 sequences) The list of points in the path. + width (float) The width of the ROIs orthogonal to the path. + **args All extra keyword arguments are passed to ROI() + -------------- ------------------------------------------------------------- """ sigRegionChangeFinished = QtCore.Signal(object) sigRegionChangeStarted = QtCore.Signal(object) @@ -1523,6 +1747,18 @@ class MultiLineROI(MultiRectROI): print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)") class EllipseROI(ROI): + """ + Elliptical ROI subclass with one scale handle and one rotation handle. + + + **Arguments** + -------------- ------------------------------------------------------------- + pos (length-2 sequence) The position of the ROI's origin. + size (length-2 sequence) The size of the ROI's bounding rectangle. + **args All extra keyword arguments are passed to ROI() + -------------- ------------------------------------------------------------- + + """ def __init__(self, pos, size, **args): #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) ROI.__init__(self, pos, size, **args) @@ -1540,6 +1776,10 @@ class EllipseROI(ROI): p.drawEllipse(r) def getArrayRegion(self, arr, img=None): + """ + Return the result of ROI.getArrayRegion() masked by the elliptical shape + of the ROI. Regions outside the ellipse are set to 0. + """ arr = ROI.getArrayRegion(self, arr, img) if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0: return None @@ -1557,12 +1797,25 @@ class EllipseROI(ROI): class CircleROI(EllipseROI): + """ + Circular ROI subclass. Behaves exactly as EllipseROI, but may only be scaled + proportionally to maintain its aspect ratio. + + **Arguments** + -------------- ------------------------------------------------------------- + pos (length-2 sequence) The position of the ROI's origin. + size (length-2 sequence) The size of the ROI's bounding rectangle. + **args All extra keyword arguments are passed to ROI() + -------------- ------------------------------------------------------------- + + """ def __init__(self, pos, size, **args): ROI.__init__(self, pos, size, **args) self.aspectLocked = True #self.addTranslateHandle([0.5, 0.5]) self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) - + + class PolygonROI(ROI): ## deprecated. Use PloyLineROI instead. @@ -1616,8 +1869,24 @@ class PolygonROI(ROI): return sc class PolyLineROI(ROI): - """Container class for multiple connected LineSegmentROIs. Responsible for adding new - line segments, and for translation/(rotation?) of multiple lines together.""" + """ + Container class for multiple connected LineSegmentROIs. + + This class allows the user to draw paths of multiple line segments. + + **Arguments** + -------------- ------------------------------------------------------------- + positions (list of length-2 sequences) The list of points in the path. + Note that, unlike the handle positions specified in other + ROIs, these positions must be expressed in the normal + coordinate system of the ROI, rather than (0 to 1) relative + to the size of the ROI. + closed (bool) if True, an extra LineSegmentROI is added connecting + the beginning and end points. + **args All extra keyword arguments are passed to ROI() + -------------- ------------------------------------------------------------- + + """ def __init__(self, positions, closed=False, pos=None, **args): if pos is None: @@ -1730,6 +1999,10 @@ class PolyLineROI(ROI): return p def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): + """ + Return the result of ROI.getArrayRegion(), masked by the shape of the + ROI. Values outside the ROI shape are set to 0. + """ sl = self.getArraySlice(data, img, axes=(0,1)) if sl is None: return None @@ -1758,6 +2031,14 @@ class PolyLineROI(ROI): class LineSegmentROI(ROI): """ ROI subclass with two freely-moving handles defining a line. + + **Arguments** + -------------- ------------------------------------------------------------- + positions (list of two length-2 sequences) The endpoints of the line + segment. Note that, unlike the handle positions specified in + other ROIs, these positions must be expressed in the normal + coordinate system of the ROI, rather than (0 to 1) relative + to the size of the ROI. """ def __init__(self, positions=(None, None), pos=None, handles=(None,None), **args): @@ -1810,8 +2091,13 @@ class LineSegmentROI(ROI): def getArrayRegion(self, data, img, axes=(0,1)): """ - Use the position of this ROI relative to an imageItem to pull a slice from an array. - Since this pulls 1D data from a 2D coordinate system, the return value will have ndim = data.ndim-1 + Use the position of this ROI relative to an imageItem to pull a slice + from an array. + + Since this pulls 1D data from a 2D coordinate system, the return value + will have ndim = data.ndim-1 + + See ROI.getArrayRegion() for a description of the arguments. """ imgPts = [self.mapToItem(img, h['item'].pos()) for h in self.handles] From 912f1f13c26186d27c9b7aa94a7cb03b5a3deccb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Feb 2014 20:27:22 -0500 Subject: [PATCH 127/268] documentation fixes --- doc/source/graphicsItems/index.rst | 2 +- doc/source/graphicsItems/roi.rst | 22 ++++- pyqtgraph/graphicsItems/AxisItem.py | 2 +- pyqtgraph/graphicsItems/PlotDataItem.py | 38 +++++---- pyqtgraph/graphicsItems/ROI.py | 107 +++++++++++++----------- 5 files changed, 100 insertions(+), 71 deletions(-) diff --git a/doc/source/graphicsItems/index.rst b/doc/source/graphicsItems/index.rst index b15c205c..970e9500 100644 --- a/doc/source/graphicsItems/index.rst +++ b/doc/source/graphicsItems/index.rst @@ -1,4 +1,4 @@ -Pyqtgraph's Graphics Items +PyQtGraph's Graphics Items ========================== Since pyqtgraph relies on Qt's GraphicsView framework, most of its graphics functionality is implemented as QGraphicsItem subclasses. This has two important consequences: 1) virtually anything you want to draw can be easily accomplished using the functionality provided by Qt. 2) Many of pyqtgraph's GraphicsItem classes can be used in any normal QGraphicsScene. diff --git a/doc/source/graphicsItems/roi.rst b/doc/source/graphicsItems/roi.rst index 22945ade..f4f4346d 100644 --- a/doc/source/graphicsItems/roi.rst +++ b/doc/source/graphicsItems/roi.rst @@ -4,5 +4,25 @@ ROI .. autoclass:: pyqtgraph.ROI :members: - .. automethod:: pyqtgraph.ROI.__init__ +.. autoclass:: pyqtgraph.RectROI + :members: + +.. autoclass:: pyqtgraph.EllipseROI + :members: + +.. autoclass:: pyqtgraph.CircleROI + :members: + +.. autoclass:: pyqtgraph.LineSegmentROI + :members: + +.. autoclass:: pyqtgraph.PolyLineROI + :members: + +.. autoclass:: pyqtgraph.LineROI + :members: + +.. autoclass:: pyqtgraph.MultiRectROI + :members: + diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 66efeda5..69ad695c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -164,7 +164,7 @@ class AxisItem(GraphicsWidget): without any scaling prefix (eg, 'V' instead of 'mV'). The scaling prefix will be automatically prepended based on the range of data displayed. - **args All extra keyword arguments become CSS style options for + \**args All extra keyword arguments become CSS style options for the tag which will surround the axis label and units. ============= ============================================================= diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 8baab719..29d48db6 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -56,7 +56,7 @@ class PlotDataItem(GraphicsObject): =========================== ========================================= **Line style keyword arguments:** - ========== ================================================ + ========== ============================================================================== connect Specifies how / whether vertexes should be connected. See :func:`arrayToQPath() ` pen Pen to use for drawing line between points. @@ -67,21 +67,25 @@ class PlotDataItem(GraphicsObject): fillLevel Fill the area between the curve and fillLevel fillBrush Fill to use when fillLevel is specified. May be any single argument accepted by :func:`mkBrush() ` - ========== ================================================ + ========== ============================================================================== **Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() ` for more information) - ============ ================================================ - symbol Symbol to use for drawing points OR list of symbols, one per point. Default is no symbol. + ============ ===================================================== + symbol Symbol to use for drawing points OR list of symbols, + one per point. Default is no symbol. Options are o, s, t, d, +, or any QPainterPath - symbolPen Outline pen for drawing points OR list of pens, one per point. - May be any single argument accepted by :func:`mkPen() ` - symbolBrush Brush for filling points OR list of brushes, one per point. - May be any single argument accepted by :func:`mkBrush() ` + symbolPen Outline pen for drawing points OR list of pens, one + per point. May be any single argument accepted by + :func:`mkPen() ` + symbolBrush Brush for filling points OR list of brushes, one per + point. May be any single argument accepted by + :func:`mkBrush() ` symbolSize Diameter of symbols OR list of diameters. - pxMode (bool) If True, then symbolSize is specified in pixels. If False, then symbolSize is + pxMode (bool) If True, then symbolSize is specified in + pixels. If False, then symbolSize is specified in data coordinates. - ============ ================================================ + ============ ===================================================== **Optimization keyword arguments:** @@ -92,11 +96,11 @@ class PlotDataItem(GraphicsObject): decimate deprecated. downsample (int) Reduce the number of samples displayed by this value downsampleMethod 'subsample': Downsample by taking the first of N samples. - This method is fastest and least accurate. + This method is fastest and least accurate. 'mean': Downsample by taking the mean of N samples. 'peak': Downsample by drawing a saw wave that follows the min - and max of the original data. This method produces the best - visual representation of the data but is slower. + and max of the original data. This method produces the best + visual representation of the data but is slower. autoDownsample (bool) If True, resample the data before plotting to avoid plotting multiple line segments per pixel. This can improve performance when viewing very high-density data, but increases the initial overhead @@ -294,13 +298,13 @@ class PlotDataItem(GraphicsObject): Arguments ds (int) Reduce visible plot samples by this factor. To disable, set ds=1. - auto (bool) If True, automatically pick *ds* based on visible range + auto (bool) If True, automatically pick *ds* based on visible range. mode 'subsample': Downsample by taking the first of N samples. - This method is fastest and least accurate. + This method is fastest and least accurate. 'mean': Downsample by taking the mean of N samples. 'peak': Downsample by drawing a saw wave that follows the min - and max of the original data. This method produces the best - visual representation of the data but is slower. + and max of the original data. This method produces the best + visual representation of the data but is slower. =========== ================================================================= """ changed = False diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index ef2f329b..bea0d730 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -46,8 +46,8 @@ class ROI(GraphicsObject): that allow the user to manibulate the ROI. - Signals - ----------------------- ---------------------------------------------------- + ======================= ==================================================== + **Signals** sigRegionChangeFinished Emitted when the user stops dragging the ROI (or one of its handles) or if the ROI is changed programatically. @@ -65,11 +65,11 @@ class ROI(GraphicsObject): details. sigRemoveRequested Emitted when the user selects 'remove' from the ROI's context menu (if available). - ----------------------- ---------------------------------------------------- + ======================= ==================================================== - Arguments - ---------------- ----------------------------------------------------------- + ================ =========================================================== + **Arguments** pos (length-2 sequence) Indicates the position of the ROI's origin. For most ROIs, this is the lower-left corner of its bounding rectangle. @@ -101,7 +101,7 @@ class ROI(GraphicsObject): an option to remove the ROI. The ROI emits sigRemoveRequested when this menu action is selected. Default is False. - ---------------- ----------------------------------------------------------- + ================ =========================================================== """ sigRegionChangeFinished = QtCore.Signal(object) @@ -276,11 +276,14 @@ class ROI(GraphicsObject): If the ROI is bounded and the move would exceed boundaries, then the ROI is moved to the nearest acceptable position instead. - snap can be: - None (default): use self.translateSnap and self.snapSize to determine whether/how to snap - False: do not snap - Point(w,h) snap to rectangular grid with spacing (w,h) - True: snap using self.snapSize (and ignoring self.translateSnap) + *snap* can be: + + =============== ========================================================================== + None (default) use self.translateSnap and self.snapSize to determine whether/how to snap + False do not snap + Point(w,h) snap to rectangular grid with spacing (w,h) + True snap using self.snapSize (and ignoring self.translateSnap) + =============== ========================================================================== Also accepts *update* and *finish* arguments (see setPos() for a description of these). """ @@ -346,8 +349,8 @@ class ROI(GraphicsObject): ROI. However, for larger ROIs it may be desirable to disable this and instead provide one or more translation handles. - Arguments: - ------------------- ---------------------------------------------------- + =================== ==================================================== + **Arguments** pos (length-2 sequence) The position of the handle relative to the shape of the ROI. A value of (0,0) indicates the origin, whereas (1, 1) indicates the @@ -357,7 +360,7 @@ class ROI(GraphicsObject): name The name of this handle (optional). Handles are identified by name when calling getLocalHandlePositions and getSceneHandlePositions. - ------------------- ---------------------------------------------------- + =================== ==================================================== """ pos = Point(pos) return self.addHandle({'name': name, 'type': 't', 'pos': pos, 'item': item}, index=index) @@ -367,8 +370,8 @@ class ROI(GraphicsObject): Add a new free handle to the ROI. Dragging free handles has no effect on the position or shape of the ROI. - Arguments: - ------------------- ---------------------------------------------------- + =================== ==================================================== + **Arguments** pos (length-2 sequence) The position of the handle relative to the shape of the ROI. A value of (0,0) indicates the origin, whereas (1, 1) indicates the @@ -378,7 +381,7 @@ class ROI(GraphicsObject): name The name of this handle (optional). Handles are identified by name when calling getLocalHandlePositions and getSceneHandlePositions. - ------------------- ---------------------------------------------------- + =================== ==================================================== """ if pos is not None: pos = Point(pos) @@ -389,8 +392,8 @@ class ROI(GraphicsObject): Add a new scale handle to the ROI. Dragging a scale handle allows the user to change the height and/or width of the ROI. - Arguments: - ------------------- ---------------------------------------------------- + =================== ==================================================== + **Arguments** pos (length-2 sequence) The position of the handle relative to the shape of the ROI. A value of (0,0) indicates the origin, whereas (1, 1) indicates the @@ -404,7 +407,7 @@ class ROI(GraphicsObject): name The name of this handle (optional). Handles are identified by name when calling getLocalHandlePositions and getSceneHandlePositions. - ------------------- ---------------------------------------------------- + =================== ==================================================== """ pos = Point(pos) center = Point(center) @@ -420,8 +423,8 @@ class ROI(GraphicsObject): Add a new rotation handle to the ROI. Dragging a rotation handle allows the user to change the angle of the ROI. - Arguments: - ------------------- ---------------------------------------------------- + =================== ==================================================== + **Arguments** pos (length-2 sequence) The position of the handle relative to the shape of the ROI. A value of (0,0) indicates the origin, whereas (1, 1) indicates the @@ -433,7 +436,7 @@ class ROI(GraphicsObject): name The name of this handle (optional). Handles are identified by name when calling getLocalHandlePositions and getSceneHandlePositions. - ------------------- ---------------------------------------------------- + =================== ==================================================== """ pos = Point(pos) center = Point(center) @@ -446,8 +449,8 @@ class ROI(GraphicsObject): arbitrary center point as well as scale the ROI by dragging the handle toward or away from the center point. - Arguments: - ------------------- ---------------------------------------------------- + =================== ==================================================== + **Arguments** pos (length-2 sequence) The position of the handle relative to the shape of the ROI. A value of (0,0) indicates the origin, whereas (1, 1) indicates the @@ -459,7 +462,7 @@ class ROI(GraphicsObject): name The name of this handle (optional). Handles are identified by name when calling getLocalHandlePositions and getSceneHandlePositions. - ------------------- ---------------------------------------------------- + =================== ==================================================== """ pos = Point(pos) center = Point(center) @@ -474,8 +477,8 @@ class ROI(GraphicsObject): arbitrary center point, while moving toward or away from the center point has no effect on the shape of the ROI. - Arguments: - ------------------- ---------------------------------------------------- + =================== ==================================================== + **Arguments** pos (length-2 sequence) The position of the handle relative to the shape of the ROI. A value of (0,0) indicates the origin, whereas (1, 1) indicates the @@ -487,7 +490,7 @@ class ROI(GraphicsObject): name The name of this handle (optional). Handles are identified by name when calling getLocalHandlePositions and getSceneHandlePositions. - ------------------- ---------------------------------------------------- + =================== ==================================================== """ pos = Point(pos) center = Point(center) @@ -1030,8 +1033,8 @@ class ROI(GraphicsObject): """Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array. + =================== ==================================================== **Arguments** - ------------------- ---------------------------------------------------- data The array to slice from. Note that this array does *not* have to be the same data that is represented in *img*. @@ -1043,9 +1046,9 @@ class ROI(GraphicsObject): returnMappedCoords (bool) If True, the array slice is returned along with a corresponding array of coordinates that were used to extract data from the original array. - **kwds All keyword arguments are passed to + \**kwds All keyword arguments are passed to :func:`affineSlice `. - ------------------- ---------------------------------------------------- + =================== ==================================================== This method uses :func:`affineSlice ` to generate the slice from *data* and uses :func:`getAffineSliceParams ` to determine the parameters to @@ -1548,8 +1551,8 @@ class RectROI(ROI): """ Rectangular ROI subclass with a single scale handle at the top-right corner. + ============== ============================================================= **Arguments** - -------------- ------------------------------------------------------------- pos (length-2 sequence) The position of the ROI origin. See ROI(). size (length-2 sequence) The size of the ROI. See ROI(). @@ -1557,8 +1560,8 @@ class RectROI(ROI): center, rather than its origin. sideScalers (bool) If True, extra scale handles are added at the top and right edges. - **args All extra keyword arguments are passed to ROI() - -------------- ------------------------------------------------------------- + \**args All extra keyword arguments are passed to ROI() + ============== ============================================================= """ def __init__(self, pos, size, centered=False, sideScalers=False, **args): @@ -1581,15 +1584,15 @@ class LineROI(ROI): allows the ROI to be positioned as if moving the ends of a line segment. A third handle controls the width of the ROI orthogonal to its "line" axis. + ============== ============================================================= **Arguments** - -------------- ------------------------------------------------------------- pos1 (length-2 sequence) The position of the center of the ROI's left edge. pos2 (length-2 sequence) The position of the center of the ROI's right edge. width (float) The width of the ROI. - **args All extra keyword arguments are passed to ROI() - -------------- ------------------------------------------------------------- + \**args All extra keyword arguments are passed to ROI() + ============== ============================================================= """ def __init__(self, pos1, pos2, width, **args): @@ -1617,12 +1620,12 @@ class MultiRectROI(QtGui.QGraphicsObject): an image similarly to PolyLineROI. It differs in that each segment of the chain is rectangular instead of linear and thus has width. + ============== ============================================================= **Arguments** - -------------- ------------------------------------------------------------- points (list of length-2 sequences) The list of points in the path. width (float) The width of the ROIs orthogonal to the path. - **args All extra keyword arguments are passed to ROI() - -------------- ------------------------------------------------------------- + \**args All extra keyword arguments are passed to ROI() + ============== ============================================================= """ sigRegionChangeFinished = QtCore.Signal(object) sigRegionChangeStarted = QtCore.Signal(object) @@ -1751,12 +1754,12 @@ class EllipseROI(ROI): Elliptical ROI subclass with one scale handle and one rotation handle. + ============== ============================================================= **Arguments** - -------------- ------------------------------------------------------------- pos (length-2 sequence) The position of the ROI's origin. size (length-2 sequence) The size of the ROI's bounding rectangle. - **args All extra keyword arguments are passed to ROI() - -------------- ------------------------------------------------------------- + \**args All extra keyword arguments are passed to ROI() + ============== ============================================================= """ def __init__(self, pos, size, **args): @@ -1801,12 +1804,12 @@ class CircleROI(EllipseROI): Circular ROI subclass. Behaves exactly as EllipseROI, but may only be scaled proportionally to maintain its aspect ratio. + ============== ============================================================= **Arguments** - -------------- ------------------------------------------------------------- pos (length-2 sequence) The position of the ROI's origin. size (length-2 sequence) The size of the ROI's bounding rectangle. - **args All extra keyword arguments are passed to ROI() - -------------- ------------------------------------------------------------- + \**args All extra keyword arguments are passed to ROI() + ============== ============================================================= """ def __init__(self, pos, size, **args): @@ -1874,8 +1877,8 @@ class PolyLineROI(ROI): This class allows the user to draw paths of multiple line segments. + ============== ============================================================= **Arguments** - -------------- ------------------------------------------------------------- positions (list of length-2 sequences) The list of points in the path. Note that, unlike the handle positions specified in other ROIs, these positions must be expressed in the normal @@ -1883,8 +1886,8 @@ class PolyLineROI(ROI): to the size of the ROI. closed (bool) if True, an extra LineSegmentROI is added connecting the beginning and end points. - **args All extra keyword arguments are passed to ROI() - -------------- ------------------------------------------------------------- + \**args All extra keyword arguments are passed to ROI() + ============== ============================================================= """ def __init__(self, positions, closed=False, pos=None, **args): @@ -2032,13 +2035,15 @@ class LineSegmentROI(ROI): """ ROI subclass with two freely-moving handles defining a line. + ============== ============================================================= **Arguments** - -------------- ------------------------------------------------------------- positions (list of two length-2 sequences) The endpoints of the line segment. Note that, unlike the handle positions specified in other ROIs, these positions must be expressed in the normal coordinate system of the ROI, rather than (0 to 1) relative to the size of the ROI. + \**args All extra keyword arguments are passed to ROI() + ============== ============================================================= """ def __init__(self, positions=(None, None), pos=None, handles=(None,None), **args): From 250ae84149a5c0688a02b556b013a6a76512abba Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Feb 2014 21:09:03 -0500 Subject: [PATCH 128/268] doc corrections update contributors list --- README.md | 1 + doc/source/3dgraphics.rst | 2 +- doc/source/3dgraphics/index.rst | 2 +- doc/source/functions.rst | 2 +- doc/source/how_to_use.rst | 4 ++-- doc/source/images.rst | 2 +- doc/source/installation.rst | 2 +- doc/source/introduction.rst | 6 +++--- doc/source/parametertree/index.rst | 2 +- doc/source/plotting.rst | 2 +- doc/source/prototyping.rst | 6 +++--- doc/source/qtcrashcourse.rst | 4 ++-- doc/source/region_of_interest.rst | 2 +- 13 files changed, 19 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 47377410..2fcc7a2d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Contributors * Guillaume Poulin * Antony Lee * Mattias Põldaru + * Thomas S. Requirements ------------ diff --git a/doc/source/3dgraphics.rst b/doc/source/3dgraphics.rst index effa288d..0a0a0210 100644 --- a/doc/source/3dgraphics.rst +++ b/doc/source/3dgraphics.rst @@ -1,7 +1,7 @@ 3D Graphics =========== -Pyqtgraph uses OpenGL to provide a 3D scenegraph system. This system is functional but still early in development. +PyQtGraph uses OpenGL to provide a 3D scenegraph system. This system is functional but still early in development. Current capabilities include: * 3D view widget with zoom/rotate controls (mouse drag and wheel) diff --git a/doc/source/3dgraphics/index.rst b/doc/source/3dgraphics/index.rst index d025a4c7..c8b5bb46 100644 --- a/doc/source/3dgraphics/index.rst +++ b/doc/source/3dgraphics/index.rst @@ -1,4 +1,4 @@ -Pyqtgraph's 3D Graphics System +PyQtGraph's 3D Graphics System ============================== The 3D graphics system in pyqtgraph is composed of a :class:`view widget ` and diff --git a/doc/source/functions.rst b/doc/source/functions.rst index ef11a4c1..5d328ad9 100644 --- a/doc/source/functions.rst +++ b/doc/source/functions.rst @@ -13,7 +13,7 @@ Simple Data Display Functions Color, Pen, and Brush Functions ------------------------------- -Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and fill shapes. These classes are highly capable but somewhat awkward to use. Pyqtgraph offers the functions :func:`~pyqtgraph.mkColor`, :func:`~pyqtgraph.mkPen`, and :func:`~pyqtgraph.mkBrush` to simplify the process of creating these classes. In most cases, however, it will be unnecessary to call these functions directly--any function or method that accepts *pen* or *brush* arguments will make use of these functions for you. For example, the following three lines all have the same effect:: +Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and fill shapes. These classes are highly capable but somewhat awkward to use. PyQtGraph offers the functions :func:`~pyqtgraph.mkColor`, :func:`~pyqtgraph.mkPen`, and :func:`~pyqtgraph.mkBrush` to simplify the process of creating these classes. In most cases, however, it will be unnecessary to call these functions directly--any function or method that accepts *pen* or *brush* arguments will make use of these functions for you. For example, the following three lines all have the same effect:: pg.plot(xdata, ydata, pen='r') pg.plot(xdata, ydata, pen=pg.mkPen('r')) diff --git a/doc/source/how_to_use.rst b/doc/source/how_to_use.rst index e4ad3cdd..c8b5b22b 100644 --- a/doc/source/how_to_use.rst +++ b/doc/source/how_to_use.rst @@ -12,7 +12,7 @@ There are a few suggested ways to use pyqtgraph: Command-line use ---------------- -Pyqtgraph makes it very easy to visualize data from the command line. Observe:: +PyQtGraph makes it very easy to visualize data from the command line. Observe:: import pyqtgraph as pg pg.plot(data) # data can be a list of values or a numpy array @@ -43,7 +43,7 @@ While I consider this approach somewhat lazy, it is often the case that 'lazy' i Embedding widgets inside PyQt applications ------------------------------------------ -For the serious application developer, all of the functionality in pyqtgraph is available via :ref:`widgets ` that can be embedded just like any other Qt widgets. Most importantly, see: :class:`PlotWidget `, :class:`ImageView `, :class:`GraphicsLayoutWidget `, and :class:`GraphicsView `. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality: +For the serious application developer, all of the functionality in pyqtgraph is available via :ref:`widgets ` that can be embedded just like any other Qt widgets. Most importantly, see: :class:`PlotWidget `, :class:`ImageView `, :class:`GraphicsLayoutWidget `, and :class:`GraphicsView `. PyQtGraph's widgets can be included in Designer's ui files via the "Promote To..." functionality: #. In Designer, create a QGraphicsView widget ("Graphics View" under the "Display Widgets" category). #. Right-click on the QGraphicsView and select "Promote To...". diff --git a/doc/source/images.rst b/doc/source/images.rst index 00d45650..0a4ac147 100644 --- a/doc/source/images.rst +++ b/doc/source/images.rst @@ -1,7 +1,7 @@ Displaying images and video =========================== -Pyqtgraph displays 2D numpy arrays as images and provides tools for determining how to translate between the numpy data type and RGB values on the screen. If you want to display data from common image and video file formats, you will need to load the data first using another library (PIL works well for images and built-in numpy conversion). +PyQtGraph displays 2D numpy arrays as images and provides tools for determining how to translate between the numpy data type and RGB values on the screen. If you want to display data from common image and video file formats, you will need to load the data first using another library (PIL works well for images and built-in numpy conversion). The easiest way to display 2D or 3D data is using the :func:`pyqtgraph.image` function:: diff --git a/doc/source/installation.rst b/doc/source/installation.rst index 2d48fead..e2bf0f8d 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -1,7 +1,7 @@ Installation ============ -Pyqtgraph does not really require any installation scripts. All that is needed is for the pyqtgraph folder to be placed someplace importable. Most people will prefer to simply place this folder within a larger project folder. If you want to make pyqtgraph available system-wide, use one of the methods listed below: +PyQtGraph does not really require any installation scripts. All that is needed is for the pyqtgraph folder to be placed someplace importable. Most people will prefer to simply place this folder within a larger project folder. If you want to make pyqtgraph available system-wide, use one of the methods listed below: * **Debian, Ubuntu, and similar Linux:** Download the .deb file linked at the top of the pyqtgraph web page or install using apt by putting "deb http://luke.campagnola.me/debian dev/" in your /etc/apt/sources.list file and install the python-pyqtgraph package. * **Arch Linux:** Looks like someone has posted unofficial packages for Arch (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577) diff --git a/doc/source/introduction.rst b/doc/source/introduction.rst index 44a498bc..043ee6ba 100644 --- a/doc/source/introduction.rst +++ b/doc/source/introduction.rst @@ -6,9 +6,9 @@ Introduction What is pyqtgraph? ------------------ -Pyqtgraph is a graphics and user interface library for Python that provides functionality commonly required in engineering and science applications. Its primary goals are 1) to provide fast, interactive graphics for displaying data (plots, video, etc.) and 2) to provide tools to aid in rapid application development (for example, property trees such as used in Qt Designer). +PyQtGraph is a graphics and user interface library for Python that provides functionality commonly required in engineering and science applications. Its primary goals are 1) to provide fast, interactive graphics for displaying data (plots, video, etc.) and 2) to provide tools to aid in rapid application development (for example, property trees such as used in Qt Designer). -Pyqtgraph makes heavy use of the Qt GUI platform (via PyQt or PySide) for its high-performance graphics and numpy for heavy number crunching. In particular, pyqtgraph uses Qt's GraphicsView framework which is a highly capable graphics system on its own; we bring optimized and simplified primitives to this framework to allow data visualization with minimal effort. +PyQtGraph makes heavy use of the Qt GUI platform (via PyQt or PySide) for its high-performance graphics and numpy for heavy number crunching. In particular, pyqtgraph uses Qt's GraphicsView framework which is a highly capable graphics system on its own; we bring optimized and simplified primitives to this framework to allow data visualization with minimal effort. It is known to run on Linux, Windows, and OSX @@ -33,7 +33,7 @@ Amongst the core features of pyqtgraph are: Examples -------- -Pyqtgraph includes an extensive set of examples that can be accessed by running:: +PyQtGraph includes an extensive set of examples that can be accessed by running:: import pyqtgraph.examples pyqtgraph.examples.run() diff --git a/doc/source/parametertree/index.rst b/doc/source/parametertree/index.rst index ee33b19c..94f590c3 100644 --- a/doc/source/parametertree/index.rst +++ b/doc/source/parametertree/index.rst @@ -8,7 +8,7 @@ This feature is commonly seen, for example, in user interface design application Parameters generally have a name, a data type (int, float, string, color, etc), and a value matching the data type. Parameters may be grouped and nested to form hierarchies and may be subclassed to provide custom behavior and display widgets. -Pyqtgraph's parameter tree system works similarly to the model-view architecture used by some components of Qt: Parameters are purely data-handling classes +PyQtGraph's parameter tree system works similarly to the model-view architecture used by some components of Qt: Parameters are purely data-handling classes that exist independent of any graphical interface. A ParameterTree is a widget that automatically generates a graphical interface which represents the state of a haierarchy of Parameter objects and allows the user to edit the values within that hierarchy. This separation of data (model) and graphical interface (view) allows the same data to be represented multiple times and in a variety of different ways. diff --git a/doc/source/plotting.rst b/doc/source/plotting.rst index 20956957..8a99663a 100644 --- a/doc/source/plotting.rst +++ b/doc/source/plotting.rst @@ -28,7 +28,7 @@ All of the above functions also return handles to the objects that are created, Organization of Plotting Classes -------------------------------- -There are several classes invloved in displaying plot data. Most of these classes are instantiated automatically, but it is useful to understand how they are organized and relate to each other. Pyqtgraph is based heavily on Qt's GraphicsView framework--if you are not already familiar with this, it's worth reading about (but not essential). Most importantly: 1) Qt GUIs are composed of QWidgets, 2) A special widget called QGraphicsView is used for displaying complex graphics, and 3) QGraphicsItems define the objects that are displayed within a QGraphicsView. +There are several classes invloved in displaying plot data. Most of these classes are instantiated automatically, but it is useful to understand how they are organized and relate to each other. PyQtGraph is based heavily on Qt's GraphicsView framework--if you are not already familiar with this, it's worth reading about (but not essential). Most importantly: 1) Qt GUIs are composed of QWidgets, 2) A special widget called QGraphicsView is used for displaying complex graphics, and 3) QGraphicsItems define the objects that are displayed within a QGraphicsView. * Data Classes (all subclasses of QGraphicsItem) * :class:`PlotCurveItem ` - Displays a plot line given x,y data diff --git a/doc/source/prototyping.rst b/doc/source/prototyping.rst index e8dffb66..71dcd4ce 100644 --- a/doc/source/prototyping.rst +++ b/doc/source/prototyping.rst @@ -3,7 +3,7 @@ Rapid GUI prototyping [Just an overview; documentation is not complete yet] -Pyqtgraph offers several powerful features which are commonly used in engineering and scientific applications. +PyQtGraph offers several powerful features which are commonly used in engineering and scientific applications. Parameter Trees --------------- @@ -16,7 +16,7 @@ See the `parametertree documentation `_ for more information. Visual Programming Flowcharts ----------------------------- -Pyqtgraph's flowcharts provide a visual programming environment similar in concept to LabView--functional modules are added to a flowchart and connected by wires to define a more complex and arbitrarily configurable algorithm. A small number of predefined modules (called Nodes) are included with pyqtgraph, but most flowchart developers will want to define their own library of Nodes. At their core, the Nodes are little more than 1) a Python function 2) a list of input/output terminals, and 3) an optional widget providing a control panel for the Node. Nodes may transmit/receive any type of Python object via their terminals. +PyQtGraph's flowcharts provide a visual programming environment similar in concept to LabView--functional modules are added to a flowchart and connected by wires to define a more complex and arbitrarily configurable algorithm. A small number of predefined modules (called Nodes) are included with pyqtgraph, but most flowchart developers will want to define their own library of Nodes. At their core, the Nodes are little more than 1) a Python function 2) a list of input/output terminals, and 3) an optional widget providing a control panel for the Node. Nodes may transmit/receive any type of Python object via their terminals. See the `flowchart documentation `_ and the flowchart examples for more information. @@ -30,6 +30,6 @@ The Canvas is a system designed to allow the user to add/remove items to a 2D ca Dockable Widgets ---------------- -The dockarea system allows the design of user interfaces which can be rearranged by the user at runtime. Docks can be moved, resized, stacked, and torn out of the main window. This is similar in principle to the docking system built into Qt, but offers a more deterministic dock placement API (in Qt it is very difficult to programatically generate complex dock arrangements). Additionally, Qt's docks are designed to be used as small panels around the outer edge of a window. Pyqtgraph's docks were created with the notion that the entire window (or any portion of it) would consist of dockable components. +The dockarea system allows the design of user interfaces which can be rearranged by the user at runtime. Docks can be moved, resized, stacked, and torn out of the main window. This is similar in principle to the docking system built into Qt, but offers a more deterministic dock placement API (in Qt it is very difficult to programatically generate complex dock arrangements). Additionally, Qt's docks are designed to be used as small panels around the outer edge of a window. PyQtGraph's docks were created with the notion that the entire window (or any portion of it) would consist of dockable components. diff --git a/doc/source/qtcrashcourse.rst b/doc/source/qtcrashcourse.rst index f117bb7f..083a78ee 100644 --- a/doc/source/qtcrashcourse.rst +++ b/doc/source/qtcrashcourse.rst @@ -1,7 +1,7 @@ Qt Crash Course =============== -Pyqtgraph makes extensive use of Qt for generating nearly all of its visual output and interfaces. Qt's documentation is very well written and we encourage all pyqtgraph developers to familiarize themselves with it. The purpose of this section is to provide an introduction to programming with Qt (using either PyQt or PySide) for the pyqtgraph developer. +PyQtGraph makes extensive use of Qt for generating nearly all of its visual output and interfaces. Qt's documentation is very well written and we encourage all pyqtgraph developers to familiarize themselves with it. The purpose of this section is to provide an introduction to programming with Qt (using either PyQt or PySide) for the pyqtgraph developer. QWidgets and Layouts -------------------- @@ -12,7 +12,7 @@ A Qt GUI is almost always composed of a few basic components: * Multiple QWidget instances such as QPushButton, QLabel, QComboBox, etc. * QLayout instances (optional, but strongly encouraged) which automatically manage the positioning of widgets to allow the GUI to resize in a usable way. -Pyqtgraph fits into this scheme by providing its own QWidget subclasses to be inserted into your GUI. +PyQtGraph fits into this scheme by providing its own QWidget subclasses to be inserted into your GUI. Example:: diff --git a/doc/source/region_of_interest.rst b/doc/source/region_of_interest.rst index eda9cacc..e972cae8 100644 --- a/doc/source/region_of_interest.rst +++ b/doc/source/region_of_interest.rst @@ -1,7 +1,7 @@ Interactive Data Selection Controls =================================== -Pyqtgraph includes graphics items which allow the user to select and mark regions of data. +PyQtGraph includes graphics items which allow the user to select and mark regions of data. Linear Selection and Marking ---------------------------- From fccae7d72c2bdc3c60515878cfb711a2f439c7a5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Feb 2014 21:33:48 -0500 Subject: [PATCH 129/268] Added note about opengl and vispy --- doc/source/3dgraphics/index.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/source/3dgraphics/index.rst b/doc/source/3dgraphics/index.rst index c8b5bb46..08202d31 100644 --- a/doc/source/3dgraphics/index.rst +++ b/doc/source/3dgraphics/index.rst @@ -5,7 +5,10 @@ The 3D graphics system in pyqtgraph is composed of a :class:`view widget `) which can be added to a view widget. -**Note:** use of this system requires python-opengl bindings. Linux users should install the python-opengl +**Note 1:** pyqtgraph.opengl is based on the deprecated OpenGL fixed-function pipeline. Although it is +currently a functioning system, it is likely to be superceded in the future by `VisPy `_. + +**Note 2:** use of this system requires python-opengl bindings. Linux users should install the python-opengl packages from their distribution. Windows/OSX users can download from ``_. Contents: From 05b70e9e49964105fab28285349c94e4199b1086 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Feb 2014 21:44:48 -0500 Subject: [PATCH 130/268] Added ConsoleWidget documentation --- doc/source/widgets/consolewidget.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/source/widgets/consolewidget.rst diff --git a/doc/source/widgets/consolewidget.rst b/doc/source/widgets/consolewidget.rst new file mode 100644 index 00000000..a85327f9 --- /dev/null +++ b/doc/source/widgets/consolewidget.rst @@ -0,0 +1,6 @@ +ConsoleWidget +============= + +.. autoclass:: pyqtgraph.console.ConsoleWidget + :members: + \ No newline at end of file From c4880863b99a12a2666b0e201cbf0aabdebc3498 Mon Sep 17 00:00:00 2001 From: fabioz Date: Mon, 3 Mar 2014 09:32:14 -0300 Subject: [PATCH 131/268] Fixed crash when collecting items on ViewBox and fixed exception which could happen when dragging when mouse handling is disabled. --- pyqtgraph/graphicsItems/GraphicsObject.py | 11 ++++- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 57 ++++++++++++++++++++-- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsObject.py b/pyqtgraph/graphicsItems/GraphicsObject.py index 1ea9a08b..015a78c6 100644 --- a/pyqtgraph/graphicsItems/GraphicsObject.py +++ b/pyqtgraph/graphicsItems/GraphicsObject.py @@ -21,8 +21,15 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): ret = QtGui.QGraphicsObject.itemChange(self, change, value) if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]: self.parentChanged() - if self.__inform_view_on_changes and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: - self.informViewBoundsChanged() + try: + inform_view_on_change = self.__inform_view_on_changes + except AttributeError: + # It's possible that the attribute was already collected when the itemChange happened + # (if it was triggered during the gc of the object). + pass + else: + if inform_view_on_change and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: + self.informViewBoundsChanged() ## workaround for pyqt bug: ## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 6bff9c65..c99713bb 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -13,20 +13,66 @@ from ... import getConfigOption __all__ = ['ViewBox'] +class WeakList(object): + + def __init__(self): + self._items = [] + + def append(self, obj): + #Add backwards to iterate backwards (to make iterating more efficient on removal). + self._items.insert(0, weakref.ref(obj)) + + def __iter__(self): + i = len(self._items)-1 + while i >= 0: + ref = self._items[i] + d = ref() + if d is None: + del self._items[i] + else: + yield d + i -= 1 class ChildGroup(ItemGroup): - sigItemsChanged = QtCore.Signal() def __init__(self, parent): ItemGroup.__init__(self, parent) + # Changed from the signal to a listener-weak-list because it was Crashing with PySide 1.2.1 + # + # Seems to be related to the fact that sigItemsChanged is there but in the hierarchy it + # starts with 'object' and not 'QObject', as it's a + # pyqtgraph.graphicsItems.GraphicsItem.GraphicsItem. + # + # Crash (gotten with faulthandler): + # Current thread 0x00001adc: + # File "X:\pyqtgraph\pyqtgraph\graphicsItems\ViewBox\ViewBox.py", line 35 in itemChange + # File "X:\pyqtgraph\pyqtgraph\WidgetGroup.py", line 197 in acceptsType + # File "X:\pyqtgraph\pyqtgraph\WidgetGroup.py", line 187 in autoAdd + # File "X:\pyqtgraph\pyqtgraph\WidgetGroup.py", line 194 in autoAdd + # File "X:\pyqtgraph\pyqtgraph\WidgetGroup.py", line 132 in __init__ + # File "X:\pyqtgraph\pyqtgraph\graphicsItems\ViewBox\ViewBoxMenu.py", line 41 in __init__ + # File "X:\pyqtgraph\pyqtgraph\graphicsItems\ViewBox\ViewBox.py", line 185 in __init__ + # + # Could not reproduce crash after changing to weak list (and note that crash didn't always + # happen even with signals, but when it did happen, it was always in the same place). + + self.itemsChangedListeners = WeakList() + # excempt from telling view when transform changes self._GraphicsObject__inform_view_on_change = False def itemChange(self, change, value): ret = ItemGroup.itemChange(self, change, value) if change == self.ItemChildAddedChange or change == self.ItemChildRemovedChange: - self.sigItemsChanged.emit() - + try: + itemsChangedListeners = self.itemsChangedListeners + except AttributeError: + # It's possible that the attribute was already collected when the itemChange happened + # (if it was triggered during the gc of the object). + pass + else: + for listener in itemsChangedListeners: + listener.itemsChanged() return ret @@ -140,7 +186,7 @@ class ViewBox(GraphicsWidget): ## this is a workaround for a Qt + OpenGL bug that causes improper clipping ## https://bugreports.qt.nokia.com/browse/QTBUG-23723 self.childGroup = ChildGroup(self) - self.childGroup.sigItemsChanged.connect(self.itemsChanged) + self.childGroup.itemsChangedListeners.append(self) self.background = QtGui.QGraphicsRectItem(self.rect()) self.background.setParentItem(self) @@ -1187,7 +1233,8 @@ class ViewBox(GraphicsWidget): y = tr.y() if mask[1] == 1 else None self._resetTarget() - self.translateBy(x=x, y=y) + if x is not None or y is not None: + self.translateBy(x=x, y=y) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) elif ev.button() & QtCore.Qt.RightButton: #print "vb.rightDrag" From ad11ff3950a8d25ca13f5b7c9383bf3f31695121 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 3 Mar 2014 12:56:15 -0500 Subject: [PATCH 132/268] Minor cleanups for GraphItem --- pyqtgraph/graphicsItems/GraphItem.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphItem.py b/pyqtgraph/graphicsItems/GraphItem.py index 413fd79a..c80138fb 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -45,7 +45,8 @@ class GraphItem(GraphicsObject): * None (to disable connection drawing) * 'default' to use the default foreground color. - symbolPen The pen used for drawing nodes. + symbolPen The pen(s) used for drawing nodes. + symbolBrush The brush(es) used for drawing nodes. ``**opts`` All other keyword arguments are given to :func:`ScatterPlotItem.setData() ` to affect the appearance of nodes (symbol, size, brush, @@ -54,19 +55,28 @@ class GraphItem(GraphicsObject): """ if 'adj' in kwds: self.adjacency = kwds.pop('adj') - assert self.adjacency.dtype.kind in 'iu' - self.picture = None + if self.adjacency.dtype.kind not in 'iu': + raise Exception("adjacency array must have int or unsigned type.") + self._update() if 'pos' in kwds: self.pos = kwds['pos'] - self.picture = None + self._update() if 'pen' in kwds: self.setPen(kwds.pop('pen')) - self.picture = None + self._update() + if 'symbolPen' in kwds: kwds['pen'] = kwds.pop('symbolPen') + if 'symbolBrush' in kwds: + kwds['brush'] = kwds.pop('symbolBrush') self.scatter.setData(**kwds) self.informViewBoundsChanged() + def _update(self): + self.picture = None + self.prepareGeometryChange() + self.update() + def setPen(self, *args, **kwargs): """ Set the pen used to draw graph lines. @@ -83,6 +93,7 @@ class GraphItem(GraphicsObject): else: self.pen = fn.mkPen(*args, **kwargs) self.picture = None + self.update() def generatePicture(self): self.picture = QtGui.QPicture() From 3e764b00c2f0a07f4f2ae3c6c8f29b4024ab5357 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 3 Mar 2014 13:48:31 -0500 Subject: [PATCH 133/268] Minor edits --- CHANGELOG | 1 + README.md | 1 + pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 26 ++++++---------------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 00bea8fe..9ef3f921 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -64,6 +64,7 @@ pyqtgraph-0.9.9 [unreleased] - Fixed MeshData exception caused when vertexes have no matching faces - Fixed GLViewWidget exception handler - Fixed unicode support in Dock + - Fixed PySide crash caused by emitting signal from GraphicsObject.itemChange pyqtgraph-0.9.8 2013-11-24 diff --git a/README.md b/README.md index c2be3fdd..cb79c995 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Contributors * Mattias Põldaru * Thomas S. * Mikhail Terekhov + * fabioz Requirements ------------ diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index c99713bb..58b2aeba 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -37,25 +37,13 @@ class ChildGroup(ItemGroup): def __init__(self, parent): ItemGroup.__init__(self, parent) - # Changed from the signal to a listener-weak-list because it was Crashing with PySide 1.2.1 - # - # Seems to be related to the fact that sigItemsChanged is there but in the hierarchy it - # starts with 'object' and not 'QObject', as it's a - # pyqtgraph.graphicsItems.GraphicsItem.GraphicsItem. - # - # Crash (gotten with faulthandler): - # Current thread 0x00001adc: - # File "X:\pyqtgraph\pyqtgraph\graphicsItems\ViewBox\ViewBox.py", line 35 in itemChange - # File "X:\pyqtgraph\pyqtgraph\WidgetGroup.py", line 197 in acceptsType - # File "X:\pyqtgraph\pyqtgraph\WidgetGroup.py", line 187 in autoAdd - # File "X:\pyqtgraph\pyqtgraph\WidgetGroup.py", line 194 in autoAdd - # File "X:\pyqtgraph\pyqtgraph\WidgetGroup.py", line 132 in __init__ - # File "X:\pyqtgraph\pyqtgraph\graphicsItems\ViewBox\ViewBoxMenu.py", line 41 in __init__ - # File "X:\pyqtgraph\pyqtgraph\graphicsItems\ViewBox\ViewBox.py", line 185 in __init__ - # - # Could not reproduce crash after changing to weak list (and note that crash didn't always - # happen even with signals, but when it did happen, it was always in the same place). - + + # Used as callback to inform ViewBox when items are added/removed from + # the group. + # Note 1: We would prefer to override itemChange directly on the + # ViewBox, but this causes crashes on PySide. + # Note 2: We might also like to use a signal rather than this callback + # mechanism, but this causes a different PySide crash. self.itemsChangedListeners = WeakList() # excempt from telling view when transform changes From e7cd4012bc5ea4e4f2c03d5f7beeb58bddd98588 Mon Sep 17 00:00:00 2001 From: fabioz Date: Tue, 4 Mar 2014 10:03:20 -0300 Subject: [PATCH 134/268] Changed FiniteCache which had a bug where calling items() would make it recurse forever with a new LRUCache implementation. --- pyqtgraph/graphicsItems/GraphicsItem.py | 26 +----- pyqtgraph/lru_cache.py | 116 ++++++++++++++++++++++++ tests/test.py | 52 ++++++++++- 3 files changed, 170 insertions(+), 24 deletions(-) create mode 100644 pyqtgraph/lru_cache.py diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index e34086bd..5c941dae 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -3,29 +3,9 @@ from ..GraphicsScene import GraphicsScene from ..Point import Point from .. import functions as fn import weakref -from ..pgcollections import OrderedDict -import operator, sys +import operator +from pyqtgraph.lru_cache import LRUCache -class FiniteCache(OrderedDict): - """Caches a finite number of objects, removing - least-frequently used items.""" - def __init__(self, length): - self._length = length - OrderedDict.__init__(self) - - def __setitem__(self, item, val): - self.pop(item, None) # make sure item is added to end - OrderedDict.__setitem__(self, item, val) - while len(self) > self._length: - del self[list(self.keys())[0]] - - def __getitem__(self, item): - val = OrderedDict.__getitem__(self, item) - del self[item] - self[item] = val ## promote this key - return val - - class GraphicsItem(object): """ @@ -38,7 +18,7 @@ class GraphicsItem(object): The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task. """ - _pixelVectorGlobalCache = FiniteCache(100) + _pixelVectorGlobalCache = LRUCache(100, 70) def __init__(self, register=True): if not hasattr(self, '_qtBaseClass'): diff --git a/pyqtgraph/lru_cache.py b/pyqtgraph/lru_cache.py new file mode 100644 index 00000000..862e956a --- /dev/null +++ b/pyqtgraph/lru_cache.py @@ -0,0 +1,116 @@ +import operator +import sys +import itertools + + +_IS_PY3 = sys.version_info[0] == 3 + +class LRUCache(object): + ''' + This LRU cache should be reasonable for short collections (until around 100 items), as it does a + sort on the items if the collection would become too big (so, it is very fast for getting and + setting but when its size would become higher than the max size it does one sort based on the + internal time to decide which items should be removed -- which should be Ok if the resize_to + isn't too close to the max_size so that it becomes an operation that doesn't happen all the + time). + ''' + + def __init__(self, max_size=100, resize_to=70): + ''' + :param int max_size: + This is the maximum size of the cache. When some item is added and the cache would become + bigger than this, it's resized to the value passed on resize_to. + + :param int resize_to: + When a resize operation happens, this is the size of the final cache. + ''' + assert resize_to < max_size + self.max_size = max_size + self.resize_to = resize_to + self._counter = 0 + self._dict = {} + if _IS_PY3: + self._next_time = itertools.count(0).__next__ + else: + self._next_time = itertools.count(0).next + + def __getitem__(self, key): + item = self._dict[key] + item[2] = self._next_time() + return item[1] + + def __len__(self): + return len(self._dict) + + def __setitem__(self, key, value): + item = self._dict.get(key) + if item is None: + if len(self._dict) + 1 > self.max_size: + self._resize_to() + + item = [key, value, self._next_time()] + self._dict[key] = item + else: + item[1] = value + item[2] = self._next_time() + + def __delitem__(self, key): + del self._dict[key] + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def clear(self): + self._dict.clear() + + if _IS_PY3: + def values(self): + return [i[1] for i in self._dict.values()] + + def keys(self): + return [x[0] for x in self._dict.values()] + + def _resize_to(self): + ordered = sorted(self._dict.values(), key=operator.itemgetter(2))[:self.resize_to] + for i in ordered: + del self._dict[i[0]] + + def iteritems(self, access_time=False): + ''' + :param bool access_time: + If True sorts the returned items by the internal access time. + ''' + if access_time: + for x in sorted(self._dict.values(), key=operator.itemgetter(2)): + yield x[0], x[1] + else: + for x in self._dict.items(): + yield x[0], x[1] + + else: + def values(self): + return [i[1] for i in self._dict.itervalues()] + + def keys(self): + return [x[0] for x in self._dict.itervalues()] + + + def _resize_to(self): + ordered = sorted(self._dict.itervalues(), key=operator.itemgetter(2))[:self.resize_to] + for i in ordered: + del self._dict[i[0]] + + def iteritems(self, access_time=False): + ''' + :param bool access_time: + If True sorts the returned items by the internal access time. + ''' + if access_time: + for x in sorted(self._dict.itervalues(), key=operator.itemgetter(2)): + yield x[0], x[1] + else: + for x in self._dict.iteritems(): + yield x[0], x[1] diff --git a/tests/test.py b/tests/test.py index f24a7d42..9821f821 100644 --- a/tests/test.py +++ b/tests/test.py @@ -5,4 +5,54 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..') ## all tests should be defined with this class so we have the option to tweak it later. class TestCase(unittest.TestCase): - pass \ No newline at end of file + + def testLRU(self): + from pyqtgraph.lru_cache import LRUCache + lru = LRUCache(2, 1) + + def CheckLru(): + lru[1] = 1 + lru[2] = 2 + lru[3] = 3 + + self.assertEqual(2, len(lru)) + self.assertSetEqual(set([2, 3]), set(lru.keys())) + self.assertSetEqual(set([2, 3]), set(lru.values())) + + lru[2] = 2 + self.assertSetEqual(set([2, 3]), set(lru.values())) + + lru[1] = 1 + self.assertSetEqual(set([2, 1]), set(lru.values())) + + #Iterates from the used in the last access to others based on access time. + self.assertEqual([(2, 2), (1, 1)], list(lru.iteritems(access_time=True))) + lru[2] = 2 + self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) + + del lru[2] + self.assertEqual([(1, 1), ], list(lru.iteritems(access_time=True))) + + lru[2] = 2 + self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) + + _a = lru[1] + self.assertEqual([(2, 2), (1, 1)], list(lru.iteritems(access_time=True))) + + _a = lru[2] + self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) + + self.assertEqual(lru.get(2), 2) + self.assertEqual(lru.get(3), None) + self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) + + lru.clear() + self.assertEqual([], list(lru.iteritems())) + + CheckLru() + + # Check it twice... + CheckLru() + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From dad5d8f733f09942c703daf9a6e102803e3dbfc6 Mon Sep 17 00:00:00 2001 From: Pietro Zambelli Date: Tue, 4 Mar 2014 16:56:56 +0000 Subject: [PATCH 135/268] Detect range of the image using bottleneck if available --- pyqtgraph/imageview/ImageView.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index bf415bb3..c7c3206e 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -33,6 +33,11 @@ from .. import debug as debug from ..SignalProxy import SignalProxy +try: + from bottleneck import nanmin, nanmax +except ImportError: + from numpy import nanmin, nanmax + #try: #from .. import metaarray as metaarray #HAVE_METAARRAY = True @@ -526,7 +531,7 @@ class ImageView(QtGui.QWidget): sl = [slice(None)] * data.ndim sl[ax] = slice(None, None, 2) data = data[sl] - return data.min(), data.max() + return nanmin(data), nanmax(data) def normalize(self, image): """ From eb33970274b6b2558b8911c6889e79571f21e555 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 5 Mar 2014 09:11:53 -0500 Subject: [PATCH 136/268] Correct to unix line endings --- pyqtgraph/lru_cache.py | 232 ++++++++++++++++++++--------------------- 1 file changed, 116 insertions(+), 116 deletions(-) diff --git a/pyqtgraph/lru_cache.py b/pyqtgraph/lru_cache.py index 862e956a..2ce2e372 100644 --- a/pyqtgraph/lru_cache.py +++ b/pyqtgraph/lru_cache.py @@ -1,116 +1,116 @@ -import operator -import sys -import itertools - - -_IS_PY3 = sys.version_info[0] == 3 - -class LRUCache(object): - ''' - This LRU cache should be reasonable for short collections (until around 100 items), as it does a - sort on the items if the collection would become too big (so, it is very fast for getting and - setting but when its size would become higher than the max size it does one sort based on the - internal time to decide which items should be removed -- which should be Ok if the resize_to - isn't too close to the max_size so that it becomes an operation that doesn't happen all the - time). - ''' - - def __init__(self, max_size=100, resize_to=70): - ''' - :param int max_size: - This is the maximum size of the cache. When some item is added and the cache would become - bigger than this, it's resized to the value passed on resize_to. - - :param int resize_to: - When a resize operation happens, this is the size of the final cache. - ''' - assert resize_to < max_size - self.max_size = max_size - self.resize_to = resize_to - self._counter = 0 - self._dict = {} - if _IS_PY3: - self._next_time = itertools.count(0).__next__ - else: - self._next_time = itertools.count(0).next - - def __getitem__(self, key): - item = self._dict[key] - item[2] = self._next_time() - return item[1] - - def __len__(self): - return len(self._dict) - - def __setitem__(self, key, value): - item = self._dict.get(key) - if item is None: - if len(self._dict) + 1 > self.max_size: - self._resize_to() - - item = [key, value, self._next_time()] - self._dict[key] = item - else: - item[1] = value - item[2] = self._next_time() - - def __delitem__(self, key): - del self._dict[key] - - def get(self, key, default=None): - try: - return self[key] - except KeyError: - return default - - def clear(self): - self._dict.clear() - - if _IS_PY3: - def values(self): - return [i[1] for i in self._dict.values()] - - def keys(self): - return [x[0] for x in self._dict.values()] - - def _resize_to(self): - ordered = sorted(self._dict.values(), key=operator.itemgetter(2))[:self.resize_to] - for i in ordered: - del self._dict[i[0]] - - def iteritems(self, access_time=False): - ''' - :param bool access_time: - If True sorts the returned items by the internal access time. - ''' - if access_time: - for x in sorted(self._dict.values(), key=operator.itemgetter(2)): - yield x[0], x[1] - else: - for x in self._dict.items(): - yield x[0], x[1] - - else: - def values(self): - return [i[1] for i in self._dict.itervalues()] - - def keys(self): - return [x[0] for x in self._dict.itervalues()] - - - def _resize_to(self): - ordered = sorted(self._dict.itervalues(), key=operator.itemgetter(2))[:self.resize_to] - for i in ordered: - del self._dict[i[0]] - - def iteritems(self, access_time=False): - ''' - :param bool access_time: - If True sorts the returned items by the internal access time. - ''' - if access_time: - for x in sorted(self._dict.itervalues(), key=operator.itemgetter(2)): - yield x[0], x[1] - else: - for x in self._dict.iteritems(): - yield x[0], x[1] +import operator +import sys +import itertools + + +_IS_PY3 = sys.version_info[0] == 3 + +class LRUCache(object): + ''' + This LRU cache should be reasonable for short collections (until around 100 items), as it does a + sort on the items if the collection would become too big (so, it is very fast for getting and + setting but when its size would become higher than the max size it does one sort based on the + internal time to decide which items should be removed -- which should be Ok if the resize_to + isn't too close to the max_size so that it becomes an operation that doesn't happen all the + time). + ''' + + def __init__(self, max_size=100, resize_to=70): + ''' + :param int max_size: + This is the maximum size of the cache. When some item is added and the cache would become + bigger than this, it's resized to the value passed on resize_to. + + :param int resize_to: + When a resize operation happens, this is the size of the final cache. + ''' + assert resize_to < max_size + self.max_size = max_size + self.resize_to = resize_to + self._counter = 0 + self._dict = {} + if _IS_PY3: + self._next_time = itertools.count(0).__next__ + else: + self._next_time = itertools.count(0).next + + def __getitem__(self, key): + item = self._dict[key] + item[2] = self._next_time() + return item[1] + + def __len__(self): + return len(self._dict) + + def __setitem__(self, key, value): + item = self._dict.get(key) + if item is None: + if len(self._dict) + 1 > self.max_size: + self._resize_to() + + item = [key, value, self._next_time()] + self._dict[key] = item + else: + item[1] = value + item[2] = self._next_time() + + def __delitem__(self, key): + del self._dict[key] + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def clear(self): + self._dict.clear() + + if _IS_PY3: + def values(self): + return [i[1] for i in self._dict.values()] + + def keys(self): + return [x[0] for x in self._dict.values()] + + def _resize_to(self): + ordered = sorted(self._dict.values(), key=operator.itemgetter(2))[:self.resize_to] + for i in ordered: + del self._dict[i[0]] + + def iteritems(self, access_time=False): + ''' + :param bool access_time: + If True sorts the returned items by the internal access time. + ''' + if access_time: + for x in sorted(self._dict.values(), key=operator.itemgetter(2)): + yield x[0], x[1] + else: + for x in self._dict.items(): + yield x[0], x[1] + + else: + def values(self): + return [i[1] for i in self._dict.itervalues()] + + def keys(self): + return [x[0] for x in self._dict.itervalues()] + + + def _resize_to(self): + ordered = sorted(self._dict.itervalues(), key=operator.itemgetter(2))[:self.resize_to] + for i in ordered: + del self._dict[i[0]] + + def iteritems(self, access_time=False): + ''' + :param bool access_time: + If True sorts the returned items by the internal access time. + ''' + if access_time: + for x in sorted(self._dict.itervalues(), key=operator.itemgetter(2)): + yield x[0], x[1] + else: + for x in self._dict.iteritems(): + yield x[0], x[1] From dad001b9d42501aa0a64a558c5bad49fa0c019c3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 5 Mar 2014 09:12:23 -0500 Subject: [PATCH 137/268] Style corrections --- pyqtgraph/lru_cache.py | 65 +++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/pyqtgraph/lru_cache.py b/pyqtgraph/lru_cache.py index 2ce2e372..9c04abf3 100644 --- a/pyqtgraph/lru_cache.py +++ b/pyqtgraph/lru_cache.py @@ -10,33 +10,35 @@ class LRUCache(object): This LRU cache should be reasonable for short collections (until around 100 items), as it does a sort on the items if the collection would become too big (so, it is very fast for getting and setting but when its size would become higher than the max size it does one sort based on the - internal time to decide which items should be removed -- which should be Ok if the resize_to - isn't too close to the max_size so that it becomes an operation that doesn't happen all the + internal time to decide which items should be removed -- which should be Ok if the resizeTo + isn't too close to the maxSize so that it becomes an operation that doesn't happen all the time). ''' - def __init__(self, max_size=100, resize_to=70): + def __init__(self, maxSize=100, resizeTo=70): ''' - :param int max_size: - This is the maximum size of the cache. When some item is added and the cache would become - bigger than this, it's resized to the value passed on resize_to. - - :param int resize_to: - When a resize operation happens, this is the size of the final cache. + ============== ========================================================= + **Arguments:** + maxSize (int) This is the maximum size of the cache. When some + item is added and the cache would become bigger than + this, it's resized to the value passed on resizeTo. + resizeTo (int) When a resize operation happens, this is the size + of the final cache. + ============== ========================================================= ''' - assert resize_to < max_size - self.max_size = max_size - self.resize_to = resize_to + assert resizeTo < maxSize + self.maxSize = maxSize + self.resizeTo = resizeTo self._counter = 0 self._dict = {} if _IS_PY3: - self._next_time = itertools.count(0).__next__ + self._nextTime = itertools.count(0).__next__ else: - self._next_time = itertools.count(0).next + self._nextTime = itertools.count(0).next def __getitem__(self, key): item = self._dict[key] - item[2] = self._next_time() + item[2] = self._nextTime() return item[1] def __len__(self): @@ -45,14 +47,14 @@ class LRUCache(object): def __setitem__(self, key, value): item = self._dict.get(key) if item is None: - if len(self._dict) + 1 > self.max_size: - self._resize_to() + if len(self._dict) + 1 > self.maxSize: + self._resizeTo() - item = [key, value, self._next_time()] + item = [key, value, self._nextTime()] self._dict[key] = item else: item[1] = value - item[2] = self._next_time() + item[2] = self._nextTime() def __delitem__(self, key): del self._dict[key] @@ -73,17 +75,17 @@ class LRUCache(object): def keys(self): return [x[0] for x in self._dict.values()] - def _resize_to(self): - ordered = sorted(self._dict.values(), key=operator.itemgetter(2))[:self.resize_to] + def _resizeTo(self): + ordered = sorted(self._dict.values(), key=operator.itemgetter(2))[:self.resizeTo] for i in ordered: del self._dict[i[0]] - def iteritems(self, access_time=False): + def iteritems(self, accessTime=False): ''' - :param bool access_time: + :param bool accessTime: If True sorts the returned items by the internal access time. ''' - if access_time: + if accessTime: for x in sorted(self._dict.values(), key=operator.itemgetter(2)): yield x[0], x[1] else: @@ -98,17 +100,20 @@ class LRUCache(object): return [x[0] for x in self._dict.itervalues()] - def _resize_to(self): - ordered = sorted(self._dict.itervalues(), key=operator.itemgetter(2))[:self.resize_to] + def _resizeTo(self): + ordered = sorted(self._dict.itervalues(), key=operator.itemgetter(2))[:self.resizeTo] for i in ordered: del self._dict[i[0]] - def iteritems(self, access_time=False): + def iteritems(self, accessTime=False): ''' - :param bool access_time: - If True sorts the returned items by the internal access time. + ============= ====================================================== + **Arguments** + accessTime (bool) If True sorts the returned items by the + internal access time. + ============= ====================================================== ''' - if access_time: + if accessTime: for x in sorted(self._dict.itervalues(), key=operator.itemgetter(2)): yield x[0], x[1] else: From dcb2c421796ffc86f340f3965ef2c7772e4f0907 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 5 Mar 2014 09:16:53 -0500 Subject: [PATCH 138/268] Moved lru_cache to util, test to util/tests --- pyqtgraph/util/__init__.py | 0 pyqtgraph/{ => util}/lru_cache.py | 0 pyqtgraph/util/tests/test_lru_cache.py | 50 ++++++++++++++++++++++ tests/test.py | 58 -------------------------- 4 files changed, 50 insertions(+), 58 deletions(-) create mode 100644 pyqtgraph/util/__init__.py rename pyqtgraph/{ => util}/lru_cache.py (100%) create mode 100644 pyqtgraph/util/tests/test_lru_cache.py delete mode 100644 tests/test.py diff --git a/pyqtgraph/util/__init__.py b/pyqtgraph/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyqtgraph/lru_cache.py b/pyqtgraph/util/lru_cache.py similarity index 100% rename from pyqtgraph/lru_cache.py rename to pyqtgraph/util/lru_cache.py diff --git a/pyqtgraph/util/tests/test_lru_cache.py b/pyqtgraph/util/tests/test_lru_cache.py new file mode 100644 index 00000000..c0cf9f8a --- /dev/null +++ b/pyqtgraph/util/tests/test_lru_cache.py @@ -0,0 +1,50 @@ +from pyqtgraph.util.lru_cache import LRUCache + +def testLRU(): + lru = LRUCache(2, 1) + # check twice + checkLru(lru) + checkLru(lru) + +def checkLru(lru): + lru[1] = 1 + lru[2] = 2 + lru[3] = 3 + + assert len(lru) == 2 + assert set([2, 3]) == set(lru.keys()) + assert set([2, 3]) == set(lru.values()) + + lru[2] = 2 + assert set([2, 3]) == set(lru.values()) + + lru[1] = 1 + set([2, 1]) == set(lru.values()) + + #Iterates from the used in the last access to others based on access time. + assert [(2, 2), (1, 1)] == list(lru.iteritems(accessTime=True)) + lru[2] = 2 + assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True)) + + del lru[2] + assert [(1, 1), ] == list(lru.iteritems(accessTime=True)) + + lru[2] = 2 + assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True)) + + _a = lru[1] + assert [(2, 2), (1, 1)] == list(lru.iteritems(accessTime=True)) + + _a = lru[2] + assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True)) + + assert lru.get(2) == 2 + assert lru.get(3) == None + assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True)) + + lru.clear() + assert [] == list(lru.iteritems()) + + +if __name__ == '__main__': + testLRU() diff --git a/tests/test.py b/tests/test.py deleted file mode 100644 index 9821f821..00000000 --- a/tests/test.py +++ /dev/null @@ -1,58 +0,0 @@ -import unittest -import os, sys -## make sure this instance of pyqtgraph gets imported first -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -## all tests should be defined with this class so we have the option to tweak it later. -class TestCase(unittest.TestCase): - - def testLRU(self): - from pyqtgraph.lru_cache import LRUCache - lru = LRUCache(2, 1) - - def CheckLru(): - lru[1] = 1 - lru[2] = 2 - lru[3] = 3 - - self.assertEqual(2, len(lru)) - self.assertSetEqual(set([2, 3]), set(lru.keys())) - self.assertSetEqual(set([2, 3]), set(lru.values())) - - lru[2] = 2 - self.assertSetEqual(set([2, 3]), set(lru.values())) - - lru[1] = 1 - self.assertSetEqual(set([2, 1]), set(lru.values())) - - #Iterates from the used in the last access to others based on access time. - self.assertEqual([(2, 2), (1, 1)], list(lru.iteritems(access_time=True))) - lru[2] = 2 - self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) - - del lru[2] - self.assertEqual([(1, 1), ], list(lru.iteritems(access_time=True))) - - lru[2] = 2 - self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) - - _a = lru[1] - self.assertEqual([(2, 2), (1, 1)], list(lru.iteritems(access_time=True))) - - _a = lru[2] - self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) - - self.assertEqual(lru.get(2), 2) - self.assertEqual(lru.get(3), None) - self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) - - lru.clear() - self.assertEqual([], list(lru.iteritems())) - - CheckLru() - - # Check it twice... - CheckLru() - -if __name__ == '__main__': - unittest.main() \ No newline at end of file From 41c3d47d4334f71b7bd2a6cf543755f06b26de22 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 5 Mar 2014 10:25:55 -0500 Subject: [PATCH 139/268] Correct GraphicsItem to use relative import of lru_cache Update MultiPlotSpeedTest to test lru_cache performance --- examples/MultiPlotSpeedTest.py | 21 +++++++++++++++------ examples/__main__.py | 1 + pyqtgraph/graphicsItems/GraphicsItem.py | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/examples/MultiPlotSpeedTest.py b/examples/MultiPlotSpeedTest.py index e38c90e2..0d0d701b 100644 --- a/examples/MultiPlotSpeedTest.py +++ b/examples/MultiPlotSpeedTest.py @@ -22,17 +22,25 @@ p.setWindowTitle('pyqtgraph example: MultiPlotSpeedTest') #p.setRange(QtCore.QRectF(0, -10, 5000, 20)) p.setLabel('bottom', 'Index', units='B') -nPlots = 10 +nPlots = 100 +nSamples = 500 #curves = [p.plot(pen=(i,nPlots*1.3)) for i in range(nPlots)] -curves = [pg.PlotCurveItem(pen=(i,nPlots*1.3)) for i in range(nPlots)] -for c in curves: +curves = [] +for i in range(nPlots): + c = pg.PlotCurveItem(pen=(i,nPlots*1.3)) p.addItem(c) + c.setPos(0,i*6) + curves.append(c) -rgn = pg.LinearRegionItem([1,100]) +p.setYRange(0, nPlots*6) +p.setXRange(0, nSamples) +p.resize(600,900) + +rgn = pg.LinearRegionItem([nSamples/5.,nSamples/3.]) p.addItem(rgn) -data = np.random.normal(size=(53,5000/nPlots)) +data = np.random.normal(size=(nPlots*23,nSamples)) ptr = 0 lastTime = time() fps = None @@ -42,7 +50,8 @@ def update(): count += 1 #print "---------", count for i in range(nPlots): - curves[i].setData(i+data[(ptr+i)%data.shape[0]]) + curves[i].setData(data[(ptr+i)%data.shape[0]]) + #print " setData done." ptr += nPlots now = time() diff --git a/examples/__main__.py b/examples/__main__.py index e7dbe5eb..efd6ea06 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -53,6 +53,7 @@ examples = OrderedDict([ ('Video speed test', 'VideoSpeedTest.py'), ('Line Plot update', 'PlotSpeedTest.py'), ('Scatter Plot update', 'ScatterPlotSpeedTest.py'), + ('Multiple plots', 'MultiPlotSpeedTest.py'), ])), ('3D Graphics', OrderedDict([ ('Volumetric', 'GLVolumeItem.py'), diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 5c941dae..2cae5d20 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -4,7 +4,7 @@ from ..Point import Point from .. import functions as fn import weakref import operator -from pyqtgraph.lru_cache import LRUCache +from ..util.lru_cache import LRUCache class GraphicsItem(object): From a82e940f7ab11fa150db8ef67aca36b2a558282f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 5 Mar 2014 11:01:53 -0500 Subject: [PATCH 140/268] Added instructions for contributing to pg --- CONTRIBUTING.txt | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 CONTRIBUTING.txt diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt new file mode 100644 index 00000000..46790bac --- /dev/null +++ b/CONTRIBUTING.txt @@ -0,0 +1,47 @@ +Contributions to pyqtgraph are welcome! + +Please use the following guidelines when preparing changes: + +* The preferred method for submitting changes is by github pull request. If + this is inconvenient, don't hesitate to submit by other means. + +* Pull requests should include only a focused and related set of changes. + Mixed features and unrelated changes (such as .gitignore) will usually be + rejected. + +* For major changes, it is recommended to discuss your plans on the mailing + list or in a github issue before putting in too much effort. + + * Along these lines, please note that pyqtgraph.opengl will be deprecated + soon and replaced with VisPy. + +* Writing proper documentation and unit tests is highly encouraged. PyQtGraph + uses nose / py.test style testing, so tests should usually be included in a + tests/ directory adjacent to the relevant code. + +* Documentation is generated with sphinx; please check that docstring changes + compile correctly. + +* Style guidelines: + + * PyQtGraph prefers PEP8 for most style issues, but this is not enforced + rigorously as long as the code is clean and readable. + + * Exception 1: All variable names should use camelCase rather than + underscore_separation. This is done for consistency with Qt + + * Exception 2: Function docstrings use ReStructuredText tables for + describing arguments: + + ``` + ============== ======================================================== + **Arguments:** + argName1 (type) Description of argument + argName2 (type) Description of argument. Longer descriptions must + be wrapped within the column guidelines defined by the + "====" header and footer. + ============== ======================================================== + ``` + + QObject subclasses that implement new signals should also describe + these in a similar table. From 82167a24880164b2e0e5adc2c89d5e2b34c2d493 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 5 Mar 2014 11:05:09 -0500 Subject: [PATCH 141/268] corrected contributors list --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37e562e9..bd54d4e2 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Contributors * Antony Lee * Mattias Põldaru * Thomas S. - * fabioz + * Fabio Zadrozny * Mikhail Terekhov Requirements From 8eb85d97e811497e95b67883cf10ef46b1196c73 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 6 Mar 2014 11:17:14 -0500 Subject: [PATCH 142/268] clarification in CONTRIB --- CONTRIBUTING.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 46790bac..f0ab3416 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -2,8 +2,9 @@ Contributions to pyqtgraph are welcome! Please use the following guidelines when preparing changes: -* The preferred method for submitting changes is by github pull request. If - this is inconvenient, don't hesitate to submit by other means. +* The preferred method for submitting changes is by github pull request + against the "develop" branch. If this is inconvenient, don't hesitate to + submit by other means. * Pull requests should include only a focused and related set of changes. Mixed features and unrelated changes (such as .gitignore) will usually be From 00418e49219aeecfbee93a1d6d4ffe13a2ca11d8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 10 Mar 2014 23:04:10 -0400 Subject: [PATCH 143/268] Allow GLMeshItem to draw edges from MeshData with face-indexed vertexes. --- CHANGELOG | 1 + examples/GLMeshItem.py | 56 +++----------- pyqtgraph/opengl/MeshData.py | 111 ++++++++------------------- pyqtgraph/opengl/items/GLMeshItem.py | 8 +- 4 files changed, 51 insertions(+), 125 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 079a8bf6..bf9112cc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -67,6 +67,7 @@ pyqtgraph-0.9.9 [unreleased] - Fixed PySide crash caused by emitting signal from GraphicsObject.itemChange - Fixed possible infinite loop from FiniteCache - Allow images with NaN in ImageView + - MeshData can generate edges from face-indexed vertexes pyqtgraph-0.9.8 2013-11-24 diff --git a/examples/GLMeshItem.py b/examples/GLMeshItem.py index f017f19b..1caa3490 100644 --- a/examples/GLMeshItem.py +++ b/examples/GLMeshItem.py @@ -53,19 +53,24 @@ m1.translate(5, 5, 0) m1.setGLOptions('additive') w.addItem(m1) + ## Example 2: ## Array of vertex positions, three per face +verts = np.empty((36, 3, 3), dtype=np.float32) +theta = np.linspace(0, 2*np.pi, 37)[:-1] +verts[:,0] = np.vstack([2*np.cos(theta), 2*np.sin(theta), [0]*36]).T +verts[:,1] = np.vstack([4*np.cos(theta+0.2), 4*np.sin(theta+0.2), [-1]*36]).T +verts[:,2] = np.vstack([4*np.cos(theta-0.2), 4*np.sin(theta-0.2), [1]*36]).T + ## Colors are specified per-vertex - -verts = verts[faces] ## Same mesh geometry as example 2, but now we are passing in 12 vertexes colors = np.random.random(size=(verts.shape[0], 3, 4)) -#colors[...,3] = 1.0 - -m2 = gl.GLMeshItem(vertexes=verts, vertexColors=colors, smooth=False, shader='balloon') +m2 = gl.GLMeshItem(vertexes=verts, vertexColors=colors, smooth=False, shader='balloon', + drawEdges=True, edgeColor=(1, 1, 0, 1)) m2.translate(-5, 5, 0) w.addItem(m2) + ## Example 3: ## sphere @@ -79,7 +84,7 @@ colors[:,1] = np.linspace(0, 1, colors.shape[0]) md.setFaceColors(colors) m3 = gl.GLMeshItem(meshdata=md, smooth=False)#, shader='balloon') -m3.translate(-5, -5, 0) +m3.translate(5, -5, 0) w.addItem(m3) @@ -114,45 +119,6 @@ w.addItem(m6) - -def psi(i, j, k, offset=(25, 25, 50)): - x = i-offset[0] - y = j-offset[1] - z = k-offset[2] - th = np.arctan2(z, (x**2+y**2)**0.5) - phi = np.arctan2(y, x) - r = (x**2 + y**2 + z **2)**0.5 - a0 = 1 - #ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th) - ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1) - - return ps - - #return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2 - - -print("Generating scalar field..") -data = np.abs(np.fromfunction(psi, (50,50,100))) - - -#data = np.fromfunction(lambda i,j,k: np.sin(0.2*((i-25)**2+(j-15)**2+k**2)**0.5), (50,50,50)); -# print("Generating isosurface..") -# verts = pg.isosurface(data, data.max()/4.) -# print dir(gl.MeshData) -# md = gl.GLMeshItem(vertexes=verts) -# -# colors = np.ones((md.vertexes(indexed='faces').shape[0], 4), dtype=float) -# colors[:,3] = 0.3 -# colors[:,2] = np.linspace(0, 1, colors.shape[0]) -# m1 = gl.GLMeshItem(meshdata=md, color=colors, smooth=False) -# -# w.addItem(m1) -# m1.translate(-25, -25, -20) -# -# m2 = gl.GLMeshItem(vertexes=verts, color=colors, smooth=True) -# -# w.addItem(m2) -# m2.translate(-25, -25, -50) diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index 74771255..34a6e3fc 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -84,64 +84,11 @@ class MeshData(object): if faceColors is not None: self.setFaceColors(faceColors) - #self.setFaces(vertexes=vertexes, faces=faces, vertexColors=vertexColors, faceColors=faceColors) - - - #def setFaces(self, vertexes=None, faces=None, vertexColors=None, faceColors=None): - #""" - #Set the faces in this data set. - #Data may be provided either as an Nx3x3 array of floats (9 float coordinate values per face):: - - #faces = [ [(x, y, z), (x, y, z), (x, y, z)], ... ] - - #or as an Nx3 array of ints (vertex integers) AND an Mx3 array of floats (3 float coordinate values per vertex):: - - #faces = [ (p1, p2, p3), ... ] - #vertexes = [ (x, y, z), ... ] - - #""" - #if not isinstance(vertexes, np.ndarray): - #vertexes = np.array(vertexes) - #if vertexes.dtype != np.float: - #vertexes = vertexes.astype(float) - #if faces is None: - #self._setIndexedFaces(vertexes, vertexColors, faceColors) - #else: - #self._setUnindexedFaces(faces, vertexes, vertexColors, faceColors) - ##print self.vertexes().shape - ##print self.faces().shape - - - #def setMeshColor(self, color): - #"""Set the color of the entire mesh. This removes any per-face or per-vertex colors.""" - #color = fn.Color(color) - #self._meshColor = color.glColor() - #self._vertexColors = None - #self._faceColors = None - - - #def __iter__(self): - #"""Iterate over all faces, yielding a list of three tuples [(position, normal, color), ...] for each face.""" - #vnorms = self.vertexNormals() - #vcolors = self.vertexColors() - #for i in range(self._faces.shape[0]): - #face = [] - #for j in [0,1,2]: - #vind = self._faces[i,j] - #pos = self._vertexes[vind] - #norm = vnorms[vind] - #if vcolors is None: - #color = self._meshColor - #else: - #color = vcolors[vind] - #face.append((pos, norm, color)) - #yield face - - #def __len__(self): - #return len(self._faces) - def faces(self): - """Return an array (Nf, 3) of vertex indexes, three per triangular face in the mesh.""" + """Return an array (Nf, 3) of vertex indexes, three per triangular face in the mesh. + + If faces have not been computed for this mesh, the function returns None. + """ return self._faces def edges(self): @@ -161,8 +108,6 @@ class MeshData(object): self.resetNormals() self._vertexColorsIndexedByFaces = None self._faceColorsIndexedByFaces = None - - def vertexes(self, indexed=None): """Return an array (N,3) of the positions of vertexes in the mesh. @@ -207,7 +152,6 @@ class MeshData(object): self._vertexNormalsIndexedByFaces = None self._faceNormals = None self._faceNormalsIndexedByFaces = None - def hasFaceIndexedData(self): """Return True if this object already has vertex positions indexed by face""" @@ -229,7 +173,6 @@ class MeshData(object): if v is not None: return True return False - def faceNormals(self, indexed=None): """ @@ -366,7 +309,6 @@ class MeshData(object): ## This is done by collapsing into a list of 'unique' vertexes (difference < 1e-14) ## I think generally this should be discouraged.. - faces = self._vertexesIndexedByFaces verts = {} ## used to remember the index of each vertex position self._faces = np.empty(faces.shape[:2], dtype=np.uint) @@ -427,22 +369,35 @@ class MeshData(object): #pass def _computeEdges(self): - ## generate self._edges from self._faces - #print self._faces - nf = len(self._faces) - edges = np.empty(nf*3, dtype=[('i', np.uint, 2)]) - edges['i'][0:nf] = self._faces[:,:2] - edges['i'][nf:2*nf] = self._faces[:,1:3] - edges['i'][-nf:,0] = self._faces[:,2] - edges['i'][-nf:,1] = self._faces[:,0] - - # sort per-edge - mask = edges['i'][:,0] > edges['i'][:,1] - edges['i'][mask] = edges['i'][mask][:,::-1] - - # remove duplicate entries - self._edges = np.unique(edges)['i'] - #print self._edges + if not self.hasFaceIndexedData: + ## generate self._edges from self._faces + nf = len(self._faces) + edges = np.empty(nf*3, dtype=[('i', np.uint, 2)]) + edges['i'][0:nf] = self._faces[:,:2] + edges['i'][nf:2*nf] = self._faces[:,1:3] + edges['i'][-nf:,0] = self._faces[:,2] + edges['i'][-nf:,1] = self._faces[:,0] + + # sort per-edge + mask = edges['i'][:,0] > edges['i'][:,1] + edges['i'][mask] = edges['i'][mask][:,::-1] + + # remove duplicate entries + self._edges = np.unique(edges)['i'] + #print self._edges + elif self._vertexesIndexedByFaces is not None: + verts = self._vertexesIndexedByFaces + edges = np.empty((verts.shape[0], 3, 2), dtype=np.uint) + nf = verts.shape[0] + edges[:,0,0] = np.arange(nf) * 3 + edges[:,0,1] = edges[:,0,0] + 1 + edges[:,1,0] = edges[:,0,1] + edges[:,1,1] = edges[:,1,0] + 1 + edges[:,2,0] = edges[:,1,1] + edges[:,2,1] = edges[:,0,0] + self._edges = edges + else: + raise Exception("MeshData cannot generate edges--no faces in this data.") def save(self): diff --git a/pyqtgraph/opengl/items/GLMeshItem.py b/pyqtgraph/opengl/items/GLMeshItem.py index c80fd488..55e75942 100644 --- a/pyqtgraph/opengl/items/GLMeshItem.py +++ b/pyqtgraph/opengl/items/GLMeshItem.py @@ -153,8 +153,12 @@ class GLMeshItem(GLGraphicsItem): self.colors = md.faceColors(indexed='faces') if self.opts['drawEdges']: - self.edges = md.edges() - self.edgeVerts = md.vertexes() + if not md.hasFaceIndexedData(): + self.edges = md.edges() + self.edgeVerts = md.vertexes() + else: + self.edges = md.edges() + self.edgeVerts = md.vertexes(indexed='faces') return def paint(self): From 816069c020430c5fd1356ef4357ac9ae82f71753 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 23 Nov 2013 14:35:09 -0500 Subject: [PATCH 144/268] All scipy imports in the library are now optional (need to test each of these changes) Several examples still require scipy. --- examples/ViewBox.py | 1 - pyqtgraph/SRTTransform3D.py | 4 +- pyqtgraph/colormap.py | 6 +- pyqtgraph/flowchart/library/Filters.py | 19 +++-- pyqtgraph/flowchart/library/functions.py | 21 ++++- pyqtgraph/functions.py | 100 +++++------------------ pyqtgraph/graphicsItems/PlotDataItem.py | 5 +- pyqtgraph/graphicsItems/ROI.py | 5 +- 8 files changed, 68 insertions(+), 93 deletions(-) diff --git a/examples/ViewBox.py b/examples/ViewBox.py index 2dcbb758..3a66afe3 100644 --- a/examples/ViewBox.py +++ b/examples/ViewBox.py @@ -14,7 +14,6 @@ import initExample ## This example uses a ViewBox to create a PlotWidget-like interface -#from scipy import random import numpy as np from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg diff --git a/pyqtgraph/SRTTransform3D.py b/pyqtgraph/SRTTransform3D.py index 417190e1..7da3c1bd 100644 --- a/pyqtgraph/SRTTransform3D.py +++ b/pyqtgraph/SRTTransform3D.py @@ -4,7 +4,6 @@ from .Vector import Vector from .Transform3D import Transform3D from .Vector import Vector import numpy as np -import scipy.linalg class SRTTransform3D(Transform3D): """4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate @@ -118,6 +117,7 @@ class SRTTransform3D(Transform3D): The input matrix must be affine AND have no shear, otherwise the conversion will most likely fail. """ + import numpy.linalg for i in range(4): self.setRow(i, m.row(i)) m = self.matrix().reshape(4,4) @@ -134,7 +134,7 @@ class SRTTransform3D(Transform3D): ## rotation axis is the eigenvector with eigenvalue=1 r = m[:3, :3] / scale[:, np.newaxis] try: - evals, evecs = scipy.linalg.eig(r) + evals, evecs = numpy.linalg.eig(r) except: print("Rotation matrix: %s" % str(r)) print("Scale: %s" % str(scale)) diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index 38c12097..559e6688 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -1,5 +1,4 @@ import numpy as np -import scipy.interpolate from .Qt import QtGui, QtCore class ColorMap(object): @@ -84,6 +83,11 @@ class ColorMap(object): qcolor Values are returned as an array of QColor objects. =========== =============================================================== """ + try: + import scipy.interpolate + except: + raise Exception("Colormap.map() requires the package scipy.interpolate, but it could not be imported.") + if isinstance(mode, basestring): mode = self.enumMap[mode.lower()] diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py index 518c8c18..e5bb2453 100644 --- a/pyqtgraph/flowchart/library/Filters.py +++ b/pyqtgraph/flowchart/library/Filters.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- from ...Qt import QtCore, QtGui from ..Node import Node -from scipy.signal import detrend -from scipy.ndimage import median_filter, gaussian_filter -#from ...SignalProxy import SignalProxy from . import functions from .common import * import numpy as np @@ -119,7 +116,11 @@ class Median(CtrlNode): @metaArrayWrapper def processData(self, data): - return median_filter(data, self.ctrls['n'].value()) + try: + import scipy.ndimage + except ImportError: + raise Exception("MedianFilter node requires the package scipy.ndimage.") + return scipy.ndimage.median_filter(data, self.ctrls['n'].value()) class Mode(CtrlNode): """Filters data by taking the mode (histogram-based) of a sliding window""" @@ -156,7 +157,11 @@ class Gaussian(CtrlNode): @metaArrayWrapper def processData(self, data): - return gaussian_filter(data, self.ctrls['sigma'].value()) + try: + import scipy.ndimage + except ImportError: + raise Exception("GaussianFilter node requires the package scipy.ndimage.") + return scipy.ndimage.gaussian_filter(data, self.ctrls['sigma'].value()) class Derivative(CtrlNode): @@ -189,6 +194,10 @@ class Detrend(CtrlNode): @metaArrayWrapper def processData(self, data): + try: + from scipy.signal import detrend + except ImportError: + raise Exception("DetrendFilter node requires the package scipy.signal.") return detrend(data) diff --git a/pyqtgraph/flowchart/library/functions.py b/pyqtgraph/flowchart/library/functions.py index 9efb8f36..027e1386 100644 --- a/pyqtgraph/flowchart/library/functions.py +++ b/pyqtgraph/flowchart/library/functions.py @@ -1,4 +1,3 @@ -import scipy import numpy as np from ...metaarray import MetaArray @@ -47,6 +46,11 @@ def downsample(data, n, axis=0, xvals='subsample'): def applyFilter(data, b, a, padding=100, bidir=True): """Apply a linear filter with coefficients a, b. Optionally pad the data before filtering and/or run the filter in both directions.""" + try: + import scipy.signal + except ImportError: + raise Exception("applyFilter() requires the package scipy.signal.") + d1 = data.view(np.ndarray) if padding > 0: @@ -67,6 +71,11 @@ def applyFilter(data, b, a, padding=100, bidir=True): def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True): """return data passed through bessel filter""" + try: + import scipy.signal + except ImportError: + raise Exception("besselFilter() requires the package scipy.signal.") + if dt is None: try: tvals = data.xvals('Time') @@ -85,6 +94,11 @@ def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True): def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, dt=None, btype='low', bidir=True): """return data passed through bessel filter""" + try: + import scipy.signal + except ImportError: + raise Exception("butterworthFilter() requires the package scipy.signal.") + if dt is None: try: tvals = data.xvals('Time') @@ -175,6 +189,11 @@ def denoise(data, radius=2, threshold=4): def adaptiveDetrend(data, x=None, threshold=3.0): """Return the signal with baseline removed. Discards outliers from baseline measurement.""" + try: + import scipy.signal + except ImportError: + raise Exception("adaptiveDetrend() requires the package scipy.signal.") + if x is None: x = data.xvals(0) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 427fb01d..62df1ce3 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -34,17 +34,6 @@ import decimal, re import ctypes import sys, struct -try: - import scipy.ndimage - HAVE_SCIPY = True - if getConfigOption('useWeave'): - try: - import scipy.weave - except ImportError: - setConfigOptions(useWeave=False) -except ImportError: - HAVE_SCIPY = False - from . import debug def siScale(x, minVal=1e-25, allowUnicode=True): @@ -422,7 +411,9 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) """ - if not HAVE_SCIPY: + try: + import scipy.ndimage + except ImportError: raise Exception("This function requires the scipy library, but it does not appear to be importable.") # sanity check @@ -579,15 +570,14 @@ def solve3DTransform(points1, points2): Find a 3D transformation matrix that maps points1 onto points2. Points must be specified as a list of 4 Vectors. """ - if not HAVE_SCIPY: - raise Exception("This function depends on the scipy library, but it does not appear to be importable.") + import numpy.linalg A = np.array([[points1[i].x(), points1[i].y(), points1[i].z(), 1] for i in range(4)]) B = np.array([[points2[i].x(), points2[i].y(), points2[i].z(), 1] for i in range(4)]) ## solve 3 sets of linear equations to determine transformation matrix elements matrix = np.zeros((4,4)) for i in range(3): - matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix + matrix[i] = numpy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix return matrix @@ -600,8 +590,7 @@ def solveBilinearTransform(points1, points2): mapped = np.dot(matrix, [x*y, x, y, 1]) """ - if not HAVE_SCIPY: - raise Exception("This function depends on the scipy library, but it does not appear to be importable.") + import numpy.linalg ## A is 4 rows (points) x 4 columns (xy, x, y, 1) ## B is 4 rows (points) x 2 columns (x, y) A = np.array([[points1[i].x()*points1[i].y(), points1[i].x(), points1[i].y(), 1] for i in range(4)]) @@ -610,7 +599,7 @@ def solveBilinearTransform(points1, points2): ## solve 2 sets of linear equations to determine transformation matrix elements matrix = np.zeros((2,4)) for i in range(2): - matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix + matrix[i] = numpy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix return matrix @@ -629,6 +618,10 @@ def rescaleData(data, scale, offset, dtype=None): try: if not getConfigOption('useWeave'): raise Exception('Weave is disabled; falling back to slower version.') + try: + import scipy.weave + except ImportError: + raise Exception('scipy.weave is not importable; falling back to slower version.') ## require native dtype when using weave if not data.dtype.isnative: @@ -671,68 +664,13 @@ def applyLookupTable(data, lut): Uses values in *data* as indexes to select values from *lut*. The returned data has shape data.shape + lut.shape[1:] - Uses scipy.weave to improve performance if it is available. Note: color gradient lookup tables can be generated using GradientWidget. """ if data.dtype.kind not in ('i', 'u'): data = data.astype(int) - ## using np.take appears to be faster than even the scipy.weave method and takes care of clipping as well. return np.take(lut, data, axis=0, mode='clip') - ### old methods: - #data = np.clip(data, 0, lut.shape[0]-1) - - #try: - #if not USE_WEAVE: - #raise Exception('Weave is disabled; falling back to slower version.') - - ### number of values to copy for each LUT lookup - #if lut.ndim == 1: - #ncol = 1 - #else: - #ncol = sum(lut.shape[1:]) - - ### output array - #newData = np.empty((data.size, ncol), dtype=lut.dtype) - - ### flattened input arrays - #flatData = data.flatten() - #flatLut = lut.reshape((lut.shape[0], ncol)) - - #dataSize = data.size - - ### strides for accessing each item - #newStride = newData.strides[0] / newData.dtype.itemsize - #lutStride = flatLut.strides[0] / flatLut.dtype.itemsize - #dataStride = flatData.strides[0] / flatData.dtype.itemsize - - ### strides for accessing individual values within a single LUT lookup - #newColStride = newData.strides[1] / newData.dtype.itemsize - #lutColStride = flatLut.strides[1] / flatLut.dtype.itemsize - - #code = """ - - #for( int i=0; i (abs(dx[0]) / 1000.)) if not uniform: - import scipy.interpolate as interp + try: + import scipy.interpolate as interp + except: + raise Exception('Fourier transform of irregularly-sampled data requires the package scipy.interpolate.') x2 = np.linspace(x[0], x[-1], len(x)) y = interp.griddata(x, y, x2, method='linear') x = x2 diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index c0f9008c..79be91f8 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -13,11 +13,8 @@ of how to build an ROI at the bottom of the file. """ from ..Qt import QtCore, QtGui -#if not hasattr(QtCore, 'Signal'): - #QtCore.Signal = QtCore.pyqtSignal import numpy as np -from numpy.linalg import norm -import scipy.ndimage as ndimage +#from numpy.linalg import norm from ..Point import * from ..SRTTransform import SRTTransform from math import cos, sin From b398ccd0ce9cbe5e3033ff645d67c2ae782a3aa8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 10 Mar 2014 13:06:39 -0400 Subject: [PATCH 145/268] corrected import --- examples/ImageView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ImageView.py b/examples/ImageView.py index d0bbd31b..44821f42 100644 --- a/examples/ImageView.py +++ b/examples/ImageView.py @@ -14,7 +14,7 @@ displaying and analyzing 2D and 3D data. ImageView provides: import initExample import numpy as np -import scipy +import scipy.ndimage from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph as pg From 18ddff76f0614cef70b1eabbf60f0c1ccb839303 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 10 Mar 2014 13:06:54 -0400 Subject: [PATCH 146/268] colormap no longer requires scipy.interpolate --- pyqtgraph/colormap.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index 559e6688..446044e1 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -63,8 +63,8 @@ class ColorMap(object): ignored. By default, the mode is entirely RGB. =============== ============================================================== """ - self.pos = pos - self.color = color + self.pos = np.array(pos) + self.color = np.array(color) if mode is None: mode = np.ones(len(pos)) self.mode = mode @@ -83,11 +83,6 @@ class ColorMap(object): qcolor Values are returned as an array of QColor objects. =========== =============================================================== """ - try: - import scipy.interpolate - except: - raise Exception("Colormap.map() requires the package scipy.interpolate, but it could not be imported.") - if isinstance(mode, basestring): mode = self.enumMap[mode.lower()] @@ -96,15 +91,24 @@ class ColorMap(object): else: pos, color = self.getStops(mode) - data = np.clip(data, pos.min(), pos.max()) + # don't need this--np.interp takes care of it. + #data = np.clip(data, pos.min(), pos.max()) - if not isinstance(data, np.ndarray): - interp = scipy.interpolate.griddata(pos, color, np.array([data]))[0] + # Interpolate + # TODO: is griddata faster? + # interp = scipy.interpolate.griddata(pos, color, data) + if np.isscalar(data): + interp = np.empty((color.shape[1],), dtype=color.dtype) else: - interp = scipy.interpolate.griddata(pos, color, data) - - if mode == self.QCOLOR: if not isinstance(data, np.ndarray): + data = np.array(data) + interp = np.empty(data.shape + (color.shape[1],), dtype=color.dtype) + for i in range(color.shape[1]): + interp[...,i] = np.interp(data, pos, color[:,i]) + + # Convert to QColor if requested + if mode == self.QCOLOR: + if np.isscalar(data): return QtGui.QColor(*interp) else: return [QtGui.QColor(*x) for x in interp] From 0bc923b7191076be0be9ea18c27fc10e5394a0fe Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 10 Mar 2014 15:46:55 -0400 Subject: [PATCH 147/268] Added SRTTransform3D test, corrected fromMatrix bug --- pyqtgraph/SRTTransform3D.py | 5 ++-- pyqtgraph/tests/test_srttransform3d.py | 39 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 pyqtgraph/tests/test_srttransform3d.py diff --git a/pyqtgraph/SRTTransform3D.py b/pyqtgraph/SRTTransform3D.py index 7da3c1bd..9b54843b 100644 --- a/pyqtgraph/SRTTransform3D.py +++ b/pyqtgraph/SRTTransform3D.py @@ -122,7 +122,8 @@ class SRTTransform3D(Transform3D): self.setRow(i, m.row(i)) m = self.matrix().reshape(4,4) ## translation is 4th column - self._state['pos'] = m[:3,3] + self._state['pos'] = m[:3,3] + ## scale is vector-length of first three columns scale = (m[:3,:3]**2).sum(axis=0)**0.5 ## see whether there is an inversion @@ -132,7 +133,7 @@ class SRTTransform3D(Transform3D): self._state['scale'] = scale ## rotation axis is the eigenvector with eigenvalue=1 - r = m[:3, :3] / scale[:, np.newaxis] + r = m[:3, :3] / scale[np.newaxis, :] try: evals, evecs = numpy.linalg.eig(r) except: diff --git a/pyqtgraph/tests/test_srttransform3d.py b/pyqtgraph/tests/test_srttransform3d.py new file mode 100644 index 00000000..88aa1581 --- /dev/null +++ b/pyqtgraph/tests/test_srttransform3d.py @@ -0,0 +1,39 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np +from numpy.testing import assert_array_almost_equal, assert_almost_equal + +testPoints = np.array([ + [0, 0, 0], + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [-1, -1, 0], + [0, -1, -1]]) + + +def testMatrix(): + """ + SRTTransform3D => Transform3D => SRTTransform3D + """ + tr = pg.SRTTransform3D() + tr.setRotate(45, (0, 0, 1)) + tr.setScale(0.2, 0.4, 1) + tr.setTranslate(10, 20, 40) + assert tr.getRotation() == (45, QtGui.QVector3D(0, 0, 1)) + assert tr.getScale() == QtGui.QVector3D(0.2, 0.4, 1) + assert tr.getTranslation() == QtGui.QVector3D(10, 20, 40) + + tr2 = pg.Transform3D(tr) + assert np.all(tr.matrix() == tr2.matrix()) + + # This is the most important test: + # The transition from Transform3D to SRTTransform3D is a tricky one. + tr3 = pg.SRTTransform3D(tr2) + assert_array_almost_equal(tr.matrix(), tr3.matrix()) + assert_almost_equal(tr3.getRotation()[0], tr.getRotation()[0]) + assert_array_almost_equal(tr3.getRotation()[1], tr.getRotation()[1]) + assert_array_almost_equal(tr3.getScale(), tr.getScale()) + assert_array_almost_equal(tr3.getTranslation(), tr.getTranslation()) + + From 4263379a90fae23e3e64a40a6b1bf13a7d4289ba Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 10 Mar 2014 23:45:27 -0400 Subject: [PATCH 148/268] added test for solve3DTransform --- pyqtgraph/functions.py | 17 +++++++++++++---- pyqtgraph/tests/test_functions.py | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 pyqtgraph/tests/test_functions.py diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 62df1ce3..74d1f8a5 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -568,16 +568,25 @@ def transformCoordinates(tr, coords, transpose=False): def solve3DTransform(points1, points2): """ Find a 3D transformation matrix that maps points1 onto points2. - Points must be specified as a list of 4 Vectors. + Points must be specified as either lists of 4 Vectors or + (4, 3) arrays. """ import numpy.linalg - A = np.array([[points1[i].x(), points1[i].y(), points1[i].z(), 1] for i in range(4)]) - B = np.array([[points2[i].x(), points2[i].y(), points2[i].z(), 1] for i in range(4)]) + pts = [] + for inp in (points1, points2): + if isinstance(inp, np.ndarray): + A = np.empty((4,4), dtype=float) + A[:,:3] = inp[:,:3] + A[:,3] = 1.0 + else: + A = np.array([[inp[i].x(), inp[i].y(), inp[i].z(), 1] for i in range(4)]) + pts.append(A) ## solve 3 sets of linear equations to determine transformation matrix elements matrix = np.zeros((4,4)) for i in range(3): - matrix[i] = numpy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix + ## solve Ax = B; x is one row of the desired transformation matrix + matrix[i] = numpy.linalg.solve(pts[0], pts[1][:,i]) return matrix diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py new file mode 100644 index 00000000..da9beca2 --- /dev/null +++ b/pyqtgraph/tests/test_functions.py @@ -0,0 +1,21 @@ +import pyqtgraph as pg +import numpy as np +from numpy.testing import assert_array_almost_equal, assert_almost_equal + +np.random.seed(12345) + +def testSolve3D(): + p1 = np.array([[0,0,0,1], + [1,0,0,1], + [0,1,0,1], + [0,0,1,1]], dtype=float) + + # transform points through random matrix + tr = np.random.normal(size=(4, 4)) + tr[3] = (0,0,0,1) + p2 = np.dot(tr, p1.T).T[:,:3] + + # solve to see if we can recover the transformation matrix. + tr2 = pg.solve3DTransform(p1, p2) + + assert_array_almost_equal(tr[:3], tr2[:3]) From 1eac666d02d7dd6752e33f1090b40b5363d523ad Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 10 Mar 2014 23:54:35 -0400 Subject: [PATCH 149/268] PlotDataItem._fourierTransform now uses np.interp --- pyqtgraph/graphicsItems/PlotDataItem.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 3b8d7061..5806475d 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -651,16 +651,12 @@ class PlotDataItem(GraphicsObject): def _fourierTransform(self, x, y): ## Perform fourier transform. If x values are not sampled uniformly, - ## then use interpolate.griddata to resample before taking fft. + ## then use np.interp to resample before taking fft. dx = np.diff(x) uniform = not np.any(np.abs(dx-dx[0]) > (abs(dx[0]) / 1000.)) if not uniform: - try: - import scipy.interpolate as interp - except: - raise Exception('Fourier transform of irregularly-sampled data requires the package scipy.interpolate.') x2 = np.linspace(x[0], x[-1], len(x)) - y = interp.griddata(x, y, x2, method='linear') + y = np.interp(x2, x, y) x = x2 f = np.fft.fft(y) / len(y) y = abs(f[1:len(f)/2]) From 72a3902a1865ade1df05923a748b208210dabd10 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 11 Mar 2014 02:25:05 -0400 Subject: [PATCH 150/268] Added pure-python implementation of scipy.ndimage.map_coordinates --- pyqtgraph/functions.py | 60 +++++++++++++++++-- pyqtgraph/graphicsItems/ROI.py | 99 ------------------------------- pyqtgraph/tests/test_functions.py | 21 +++++++ 3 files changed, 76 insertions(+), 104 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 74d1f8a5..d63a8bbb 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -372,7 +372,7 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, """ Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data. - The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates (see the scipy documentation for more information about this). + The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates if it is available (see the scipy documentation for more information about this). If scipy is not available, then a slower implementation of map_coordinates is used. For a graphical interface to this function, see :func:`ROI.getArrayRegion ` @@ -413,8 +413,9 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, """ try: import scipy.ndimage + have_scipy = True except ImportError: - raise Exception("This function requires the scipy library, but it does not appear to be importable.") + have_scipy = False # sanity check if len(shape) != len(vectors): @@ -436,7 +437,6 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, #print "tr1:", tr1 ## dims are now [(slice axes), (other axes)] - ## make sure vectors are arrays if not isinstance(vectors, np.ndarray): vectors = np.array(vectors) @@ -456,8 +456,10 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, output = np.empty(tuple(shape) + extraShape, dtype=data.dtype) for inds in np.ndindex(*extraShape): ind = (Ellipsis,) + inds - #print data[ind].shape, x.shape, output[ind].shape, output.shape - output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs) + if have_scipy: + output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs) + else: + output[ind] = mapCoordinates(data[ind], x.T) tr = list(range(output.ndim)) trb = [] @@ -474,6 +476,54 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, else: return output +def mapCoordinates(data, x, default=0.0): + """ + Pure-python alternative to scipy.ndimage.map_coordinates + + *data* is an array of any shape. + *x* is an array with shape[-1] == data.ndim + + Returns array of shape (x.shape[:-1] + data.shape) + """ + result = np.empty(x.shape[:-1] + data.shape, dtype=data.dtype) + nd = data.ndim + + # First we generate arrays of indexes that are needed to + # extract the data surrounding each point + fields = np.mgrid[(slice(0,2),) * nd] + xmin = np.floor(x).astype(int) + xmax = xmin + 1 + indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]]) + fieldInds = [] + totalMask = np.ones(x.shape[:-1], dtype=bool) # keep track of out-of-bound indexes + for ax in range(nd): + mask = (xmin[...,ax] >= 0) & (xmax[...,ax] < data.shape[ax]) + totalMask &= mask + axisIndex = indexes[...,ax][fields[ax]] + axisMask = mask.astype(np.ubyte).reshape((1,)*(fields.ndim-1) + mask.shape) + axisIndex *= axisMask + fieldInds.append(axisIndex) + + # Get data values surrounding each requested point + # fieldData[..., i] contains all 2**nd values needed to interpolate x[i] + fieldData = data[tuple(fieldInds)] + + ## Interpolate + s = np.empty((nd,) + fieldData.shape, dtype=float) + dx = x - xmin + # reshape fields for arithmetic against dx + for ax in range(nd): + f1 = fields[ax].reshape(fields[ax].shape + (1,)*(dx.ndim-1)) + s[ax] = f1 * dx[:,ax] + (1-f1) * (1-dx[:,ax]) + s = np.product(s, axis=0) + result = fieldData * s + for i in range(nd): + result = result.sum(axis=0) + + result[~totalMask] = default + return result + + def transformToArray(tr): """ Given a QTransform, return a 3x3 numpy array. diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 79be91f8..5494f3e3 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1082,105 +1082,6 @@ class ROI(GraphicsObject): #mapped += translate.reshape((2,1,1)) mapped = fn.transformCoordinates(img.transform(), coords) return result, mapped - - - ### transpose data so x and y are the first 2 axes - #trAx = range(0, data.ndim) - #trAx.remove(axes[0]) - #trAx.remove(axes[1]) - #tr1 = tuple(axes) + tuple(trAx) - #arr = data.transpose(tr1) - - ### Determine the minimal area of the data we will need - #(dataBounds, roiDataTransform) = self.getArraySlice(data, img, returnSlice=False, axes=axes) - - ### Pad data boundaries by 1px if possible - #dataBounds = ( - #(max(dataBounds[0][0]-1, 0), min(dataBounds[0][1]+1, arr.shape[0])), - #(max(dataBounds[1][0]-1, 0), min(dataBounds[1][1]+1, arr.shape[1])) - #) - - ### Extract minimal data from array - #arr1 = arr[dataBounds[0][0]:dataBounds[0][1], dataBounds[1][0]:dataBounds[1][1]] - - ### Update roiDataTransform to reflect this extraction - #roiDataTransform *= QtGui.QTransform().translate(-dataBounds[0][0], -dataBounds[1][0]) - #### (roiDataTransform now maps from ROI coords to extracted data coords) - - - ### Rotate array - #if abs(self.state['angle']) > 1e-5: - #arr2 = ndimage.rotate(arr1, self.state['angle'] * 180 / np.pi, order=1) - - ### update data transforms to reflect this rotation - #rot = QtGui.QTransform().rotate(self.state['angle'] * 180 / np.pi) - #roiDataTransform *= rot - - ### The rotation also causes a shift which must be accounted for: - #dataBound = QtCore.QRectF(0, 0, arr1.shape[0], arr1.shape[1]) - #rotBound = rot.mapRect(dataBound) - #roiDataTransform *= QtGui.QTransform().translate(-rotBound.left(), -rotBound.top()) - - #else: - #arr2 = arr1 - - - - #### Shift off partial pixels - ## 1. map ROI into current data space - #roiBounds = roiDataTransform.mapRect(self.boundingRect()) - - ## 2. Determine amount to shift data - #shift = (int(roiBounds.left()) - roiBounds.left(), int(roiBounds.bottom()) - roiBounds.bottom()) - #if abs(shift[0]) > 1e-6 or abs(shift[1]) > 1e-6: - ## 3. pad array with 0s before shifting - #arr2a = np.zeros((arr2.shape[0]+2, arr2.shape[1]+2) + arr2.shape[2:], dtype=arr2.dtype) - #arr2a[1:-1, 1:-1] = arr2 - - ## 4. shift array and udpate transforms - #arr3 = ndimage.shift(arr2a, shift + (0,)*(arr2.ndim-2), order=1) - #roiDataTransform *= QtGui.QTransform().translate(1+shift[0], 1+shift[1]) - #else: - #arr3 = arr2 - - - #### Extract needed region from rotated/shifted array - ## 1. map ROI into current data space (round these values off--they should be exact integer values at this point) - #roiBounds = roiDataTransform.mapRect(self.boundingRect()) - ##print self, roiBounds.height() - ##import traceback - ##traceback.print_stack() - - #roiBounds = QtCore.QRect(round(roiBounds.left()), round(roiBounds.top()), round(roiBounds.width()), round(roiBounds.height())) - - ##2. intersect ROI with data bounds - #dataBounds = roiBounds.intersect(QtCore.QRect(0, 0, arr3.shape[0], arr3.shape[1])) - - ##3. Extract data from array - #db = dataBounds - #bounds = ( - #(db.left(), db.right()+1), - #(db.top(), db.bottom()+1) - #) - #arr4 = arr3[bounds[0][0]:bounds[0][1], bounds[1][0]:bounds[1][1]] - - #### Create zero array in size of ROI - #arr5 = np.zeros((roiBounds.width(), roiBounds.height()) + arr4.shape[2:], dtype=arr4.dtype) - - ### Fill array with ROI data - #orig = Point(dataBounds.topLeft() - roiBounds.topLeft()) - #subArr = arr5[orig[0]:orig[0]+arr4.shape[0], orig[1]:orig[1]+arr4.shape[1]] - #subArr[:] = arr4[:subArr.shape[0], :subArr.shape[1]] - - - ### figure out the reverse transpose order - #tr2 = np.array(tr1) - #for i in range(0, len(tr2)): - #tr2[tr1[i]] = i - #tr2 = tuple(tr2) - - ### Untranspose array before returning - #return arr5.transpose(tr2) def getAffineSliceParams(self, data, img, axes=(0,1)): """ diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index da9beca2..86a7ff2c 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -19,3 +19,24 @@ def testSolve3D(): tr2 = pg.solve3DTransform(p1, p2) assert_array_almost_equal(tr[:3], tr2[:3]) + + +def test_mapCoordinates(): + data = np.array([[ 0., 1., 2.], + [ 2., 3., 5.], + [ 7., 7., 4.]]) + + x = np.array([[ 0.3, 0.6], + [ 1. , 1. ], + [ 0.5, 1. ], + [ 0.5, 2.5], + [ 10. , 10. ]]) + + result = pg.mapCoordinates(data, x) + + import scipy.ndimage + spresult = scipy.ndimage.map_coordinates(data, x.T, order=1) + + assert_array_almost_equal(result, spresult) + + From ff697ce4929457097baa038381556ae96453a3ad Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 11 Mar 2014 13:13:33 -0400 Subject: [PATCH 151/268] Expanded capabilities of interpolateArray function to support broadcasting --- pyqtgraph/debug.py | 66 +++++++++--------- pyqtgraph/functions.py | 112 ++++++++++++++++++++++++------ pyqtgraph/tests/test_functions.py | 34 +++++++-- 3 files changed, 151 insertions(+), 61 deletions(-) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 6b01c339..f208f3a5 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -399,7 +399,9 @@ class Profiler(object): only the initial "pyqtgraph." prefix from the module. """ - _profilers = os.environ.get("PYQTGRAPHPROFILE", "") + _profilers = os.environ.get("PYQTGRAPHPROFILE", None) + _profilers = _profilers.split(",") if _profilers is not None else [] + _depth = 0 _msgs = [] @@ -415,38 +417,36 @@ class Profiler(object): _disabledProfiler = DisabledProfiler() - if _profilers: - _profilers = _profilers.split(",") - def __new__(cls, msg=None, disabled='env', delayed=True): - """Optionally create a new profiler based on caller's qualname. - """ - if disabled is True: - return cls._disabledProfiler - - # determine the qualified name of the caller function - caller_frame = sys._getframe(1) - try: - caller_object_type = type(caller_frame.f_locals["self"]) - except KeyError: # we are in a regular function - qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1] - else: # we are in a method - qualifier = caller_object_type.__name__ - func_qualname = qualifier + "." + caller_frame.f_code.co_name - if func_qualname not in cls._profilers: # don't do anything - return cls._disabledProfiler - # create an actual profiling object - cls._depth += 1 - obj = super(Profiler, cls).__new__(cls) - obj._name = msg or func_qualname - obj._delayed = delayed - obj._markCount = 0 - obj._finished = False - obj._firstTime = obj._lastTime = ptime.time() - obj._newMsg("> Entering " + obj._name) - return obj - else: - def __new__(cls, delayed=True): - return lambda msg=None: None + def __new__(cls, msg=None, disabled='env', delayed=True): + """Optionally create a new profiler based on caller's qualname. + """ + if disabled is True or (disabled=='env' and len(cls._profilers) == 0): + return cls._disabledProfiler + + # determine the qualified name of the caller function + caller_frame = sys._getframe(1) + try: + caller_object_type = type(caller_frame.f_locals["self"]) + except KeyError: # we are in a regular function + qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1] + else: # we are in a method + qualifier = caller_object_type.__name__ + func_qualname = qualifier + "." + caller_frame.f_code.co_name + if disabled=='env' and func_qualname not in cls._profilers: # don't do anything + return cls._disabledProfiler + # create an actual profiling object + cls._depth += 1 + obj = super(Profiler, cls).__new__(cls) + obj._name = msg or func_qualname + obj._delayed = delayed + obj._markCount = 0 + obj._finished = False + obj._firstTime = obj._lastTime = ptime.time() + obj._newMsg("> Entering " + obj._name) + return obj + #else: + #def __new__(cls, delayed=True): + #return lambda msg=None: None def __call__(self, msg=None): """Register or print a new message with timing information. diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index d63a8bbb..57d9a685 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -416,6 +416,7 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, have_scipy = True except ImportError: have_scipy = False + have_scipy = False # sanity check if len(shape) != len(vectors): @@ -452,14 +453,18 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, #print "X values:" #print x ## iterate manually over unused axes since map_coordinates won't do it for us - extraShape = data.shape[len(axes):] - output = np.empty(tuple(shape) + extraShape, dtype=data.dtype) - for inds in np.ndindex(*extraShape): - ind = (Ellipsis,) + inds - if have_scipy: + if have_scipy: + extraShape = data.shape[len(axes):] + output = np.empty(tuple(shape) + extraShape, dtype=data.dtype) + for inds in np.ndindex(*extraShape): + ind = (Ellipsis,) + inds output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs) - else: - output[ind] = mapCoordinates(data[ind], x.T) + else: + # map_coordinates expects the indexes as the first axis, whereas + # interpolateArray expects indexes at the last axis. + tr = tuple(range(1,x.ndim)) + (0,) + output = interpolateArray(data, x.transpose(tr)) + tr = list(range(output.ndim)) trb = [] @@ -476,51 +481,114 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, else: return output -def mapCoordinates(data, x, default=0.0): +def interpolateArray(data, x, default=0.0): """ - Pure-python alternative to scipy.ndimage.map_coordinates + N-dimensional interpolation similar scipy.ndimage.map_coordinates. - *data* is an array of any shape. - *x* is an array with shape[-1] == data.ndim + This function returns linearly-interpolated values sampled from a regular + grid of data. + + *data* is an array of any shape containing the values to be interpolated. + *x* is an array with (shape[-1] <= data.ndim) containing the locations + within *data* to interpolate. Returns array of shape (x.shape[:-1] + data.shape) + + For example, assume we have the following 2D image data:: + + >>> data = np.array([[1, 2, 4 ], + [10, 20, 40 ], + [100, 200, 400]]) + + To compute a single interpolated point from this data:: + + >>> x = np.array([(0.5, 0.5)]) + >>> interpolateArray(data, x) + array([ 8.25]) + + To compute a 1D list of interpolated locations:: + + >>> x = np.array([(0.5, 0.5), + (1.0, 1.0), + (1.0, 2.0), + (1.5, 0.0)]) + >>> interpolateArray(data, x) + array([ 8.25, 20. , 40. , 55. ]) + + To compute a 2D array of interpolated locations:: + + >>> x = np.array([[(0.5, 0.5), (1.0, 2.0)], + [(1.0, 1.0), (1.5, 0.0)]]) + >>> interpolateArray(data, x) + array([[ 8.25, 40. ], + [ 20. , 55. ]]) + + ..and so on. The *x* argument may have any shape as long as + ```x.shape[-1] <= data.ndim```. In the case that + ```x.shape[-1] < data.ndim```, then the remaining axes are simply + broadcasted as usual. For example, we can interpolate one location + from an entire row of the data:: + + >>> x = np.array([[0.5]]) + >>> interpolateArray(data, x) + array([[ 5.5, 11. , 22. ]]) + + This is useful for interpolating from arrays of colors, vertexes, etc. """ + + prof = debug.Profiler() + result = np.empty(x.shape[:-1] + data.shape, dtype=data.dtype) nd = data.ndim + md = x.shape[-1] # First we generate arrays of indexes that are needed to # extract the data surrounding each point - fields = np.mgrid[(slice(0,2),) * nd] + fields = np.mgrid[(slice(0,2),) * md] xmin = np.floor(x).astype(int) xmax = xmin + 1 indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]]) fieldInds = [] totalMask = np.ones(x.shape[:-1], dtype=bool) # keep track of out-of-bound indexes - for ax in range(nd): - mask = (xmin[...,ax] >= 0) & (xmax[...,ax] < data.shape[ax]) - totalMask &= mask + for ax in range(md): + mask = (xmin[...,ax] >= 0) & (x[...,ax] <= data.shape[ax]-1) + # keep track of points that need to be set to default + totalMask &= mask + + # ..and keep track of indexes that are out of bounds + # (note that when x[...,ax] == data.shape[ax], then xmax[...,ax] will be out + # of bounds, but the interpolation will work anyway) + mask &= (xmax[...,ax] < data.shape[ax]) axisIndex = indexes[...,ax][fields[ax]] - axisMask = mask.astype(np.ubyte).reshape((1,)*(fields.ndim-1) + mask.shape) - axisIndex *= axisMask + #axisMask = mask.astype(np.ubyte).reshape((1,)*(fields.ndim-1) + mask.shape) + axisIndex[axisIndex < 0] = 0 + axisIndex[axisIndex >= data.shape[ax]] = 0 fieldInds.append(axisIndex) + prof() # Get data values surrounding each requested point # fieldData[..., i] contains all 2**nd values needed to interpolate x[i] fieldData = data[tuple(fieldInds)] - + prof() + ## Interpolate - s = np.empty((nd,) + fieldData.shape, dtype=float) + s = np.empty((md,) + fieldData.shape, dtype=float) dx = x - xmin # reshape fields for arithmetic against dx - for ax in range(nd): + for ax in range(md): f1 = fields[ax].reshape(fields[ax].shape + (1,)*(dx.ndim-1)) - s[ax] = f1 * dx[:,ax] + (1-f1) * (1-dx[:,ax]) + sax = f1 * dx[...,ax] + (1-f1) * (1-dx[...,ax]) + sax = sax.reshape(sax.shape + (1,) * (s.ndim-1-sax.ndim)) + s[ax] = sax s = np.product(s, axis=0) result = fieldData * s - for i in range(nd): + for i in range(md): result = result.sum(axis=0) + prof() + totalMask.shape = totalMask.shape + (1,) * (nd - md) result[~totalMask] = default + prof() return result diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index 86a7ff2c..af0dde58 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -21,10 +21,10 @@ def testSolve3D(): assert_array_almost_equal(tr[:3], tr2[:3]) -def test_mapCoordinates(): - data = np.array([[ 0., 1., 2.], - [ 2., 3., 5.], - [ 7., 7., 4.]]) +def test_interpolateArray(): + data = np.array([[ 1., 2., 4. ], + [ 10., 20., 40. ], + [ 100., 200., 400.]]) x = np.array([[ 0.3, 0.6], [ 1. , 1. ], @@ -32,11 +32,33 @@ def test_mapCoordinates(): [ 0.5, 2.5], [ 10. , 10. ]]) - result = pg.mapCoordinates(data, x) + result = pg.interpolateArray(data, x) import scipy.ndimage spresult = scipy.ndimage.map_coordinates(data, x.T, order=1) assert_array_almost_equal(result, spresult) - + # test mapping when x.shape[-1] < data.ndim + x = np.array([[ 0.3, 0], + [ 0.3, 1], + [ 0.3, 2]]) + + r1 = pg.interpolateArray(data, x) + r2 = pg.interpolateArray(data, x[0,:1]) + assert_array_almost_equal(r1, r2) + + + # test mapping 2D array of locations + x = np.array([[[0.5, 0.5], [0.5, 1.0], [0.5, 1.5]], + [[1.5, 0.5], [1.5, 1.0], [1.5, 1.5]]]) + + r1 = pg.interpolateArray(data, x) + r2 = scipy.ndimage.map_coordinates(data, x.transpose(2,0,1), order=1) + assert_array_almost_equal(r1, r2) + + + + +if __name__ == '__main__': + test_interpolateArray() \ No newline at end of file From 34802c8aeca50225a716ecd22880bd0758297ff0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 11 Mar 2014 19:01:34 -0400 Subject: [PATCH 152/268] Added pg.gaussianFilter, removed all dependency on gaussian_filter --- examples/DataSlicing.py | 1 - examples/FlowchartCustomNode.py | 7 ++--- examples/GLImageItem.py | 5 ++-- examples/GLSurfacePlot.py | 5 ++-- examples/HistogramLUT.py | 3 +- examples/ImageView.py | 3 +- examples/VideoSpeedTest.py | 8 +++-- examples/crosshair.py | 5 ++-- examples/isocurve.py | 3 +- pyqtgraph/flowchart/library/Filters.py | 3 +- pyqtgraph/functions.py | 41 +++++++++++++++++++++++++- 11 files changed, 59 insertions(+), 25 deletions(-) diff --git a/examples/DataSlicing.py b/examples/DataSlicing.py index bd201832..d766e7e3 100644 --- a/examples/DataSlicing.py +++ b/examples/DataSlicing.py @@ -11,7 +11,6 @@ a 2D plane and interpolate data along that plane to generate a slice image import initExample import numpy as np -import scipy from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph as pg diff --git a/examples/FlowchartCustomNode.py b/examples/FlowchartCustomNode.py index 25ea5c77..54c56622 100644 --- a/examples/FlowchartCustomNode.py +++ b/examples/FlowchartCustomNode.py @@ -12,7 +12,6 @@ from pyqtgraph.flowchart.library.common import CtrlNode from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import numpy as np -import scipy.ndimage app = QtGui.QApplication([]) @@ -44,7 +43,7 @@ win.show() ## generate random input data data = np.random.normal(size=(100,100)) -data = 25 * scipy.ndimage.gaussian_filter(data, (5,5)) +data = 25 * pg.gaussianFilter(data, (5,5)) data += np.random.normal(size=(100,100)) data[40:60, 40:60] += 15.0 data[30:50, 30:50] += 15.0 @@ -90,7 +89,7 @@ class ImageViewNode(Node): ## CtrlNode is just a convenience class that automatically creates its ## control widget based on a simple data structure. class UnsharpMaskNode(CtrlNode): - """Return the input data passed through scipy.ndimage.gaussian_filter.""" + """Return the input data passed through pg.gaussianFilter.""" nodeName = "UnsharpMask" uiTemplate = [ ('sigma', 'spin', {'value': 1.0, 'step': 1.0, 'range': [0.0, None]}), @@ -110,7 +109,7 @@ class UnsharpMaskNode(CtrlNode): # CtrlNode has created self.ctrls, which is a dict containing {ctrlName: widget} sigma = self.ctrls['sigma'].value() strength = self.ctrls['strength'].value() - output = dataIn - (strength * scipy.ndimage.gaussian_filter(dataIn, (sigma,sigma))) + output = dataIn - (strength * pg.gaussianFilter(dataIn, (sigma,sigma))) return {'dataOut': output} diff --git a/examples/GLImageItem.py b/examples/GLImageItem.py index dfdaad0c..581474fd 100644 --- a/examples/GLImageItem.py +++ b/examples/GLImageItem.py @@ -12,7 +12,6 @@ from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph.opengl as gl import pyqtgraph as pg import numpy as np -import scipy.ndimage as ndi app = QtGui.QApplication([]) w = gl.GLViewWidget() @@ -22,8 +21,8 @@ w.setWindowTitle('pyqtgraph example: GLImageItem') ## create volume data set to slice three images from shape = (100,100,70) -data = ndi.gaussian_filter(np.random.normal(size=shape), (4,4,4)) -data += ndi.gaussian_filter(np.random.normal(size=shape), (15,15,15))*15 +data = pg.gaussianFilter(np.random.normal(size=shape), (4,4,4)) +data += pg.gaussianFilter(np.random.normal(size=shape), (15,15,15))*15 ## slice out three planes, convert to RGBA for OpenGL texture levels = (-0.08, 0.08) diff --git a/examples/GLSurfacePlot.py b/examples/GLSurfacePlot.py index 963cf4cf..e9896e07 100644 --- a/examples/GLSurfacePlot.py +++ b/examples/GLSurfacePlot.py @@ -10,7 +10,6 @@ import initExample from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph as pg import pyqtgraph.opengl as gl -import scipy.ndimage as ndi import numpy as np ## Create a GL View widget to display data @@ -29,7 +28,7 @@ w.addItem(g) ## Simple surface plot example ## x, y values are not specified, so assumed to be 0:50 -z = ndi.gaussian_filter(np.random.normal(size=(50,50)), (1,1)) +z = pg.gaussianFilter(np.random.normal(size=(50,50)), (1,1)) p1 = gl.GLSurfacePlotItem(z=z, shader='shaded', color=(0.5, 0.5, 1, 1)) p1.scale(16./49., 16./49., 1.0) p1.translate(-18, 2, 0) @@ -46,7 +45,7 @@ w.addItem(p2) ## Manually specified colors -z = ndi.gaussian_filter(np.random.normal(size=(50,50)), (1,1)) +z = pg.gaussianFilter(np.random.normal(size=(50,50)), (1,1)) x = np.linspace(-12, 12, 50) y = np.linspace(-12, 12, 50) colors = np.ones((50,50,4), dtype=float) diff --git a/examples/HistogramLUT.py b/examples/HistogramLUT.py index 5d66cb5d..4d89dd3f 100644 --- a/examples/HistogramLUT.py +++ b/examples/HistogramLUT.py @@ -7,7 +7,6 @@ Use a HistogramLUTWidget to control the contrast / coloration of an image. import initExample import numpy as np -import scipy.ndimage as ndi from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg @@ -34,7 +33,7 @@ l.addWidget(v, 0, 0) w = pg.HistogramLUTWidget() l.addWidget(w, 0, 1) -data = ndi.gaussian_filter(np.random.normal(size=(256, 256)), (20, 20)) +data = pg.gaussianFilter(np.random.normal(size=(256, 256)), (20, 20)) for i in range(32): for j in range(32): data[i*8, j*8] += .1 diff --git a/examples/ImageView.py b/examples/ImageView.py index 44821f42..22168409 100644 --- a/examples/ImageView.py +++ b/examples/ImageView.py @@ -14,7 +14,6 @@ displaying and analyzing 2D and 3D data. ImageView provides: import initExample import numpy as np -import scipy.ndimage from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph as pg @@ -29,7 +28,7 @@ win.show() win.setWindowTitle('pyqtgraph example: ImageView') ## Create random 3D data set with noisy signals -img = scipy.ndimage.gaussian_filter(np.random.normal(size=(200, 200)), (5, 5)) * 20 + 100 +img = pg.gaussianFilter(np.random.normal(size=(200, 200)), (5, 5)) * 20 + 100 img = img[np.newaxis,:,:] decay = np.exp(-np.linspace(0,0.3,100))[:,np.newaxis,np.newaxis] data = np.random.normal(size=(100, 200, 200)) diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index 7449c2cd..19c49107 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -13,7 +13,6 @@ import initExample ## Add path to library (just for examples; you do not need th from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE import numpy as np import pyqtgraph as pg -import scipy.ndimage as ndi import pyqtgraph.ptime as ptime if USE_PYSIDE: @@ -95,10 +94,13 @@ def mkData(): if ui.rgbCheck.isChecked(): data = np.random.normal(size=(frames,width,height,3), loc=loc, scale=scale) - data = ndi.gaussian_filter(data, (0, 6, 6, 0)) + data = pg.gaussianFilter(data, (0, 6, 6, 0)) else: data = np.random.normal(size=(frames,width,height), loc=loc, scale=scale) - data = ndi.gaussian_filter(data, (0, 6, 6)) + print frames, width, height, loc, scale + data = pg.gaussianFilter(data, (0, 6, 6)) + print data[0] + pg.image(data) if dtype[0] != 'float': data = np.clip(data, 0, mx) data = data.astype(dt) diff --git a/examples/crosshair.py b/examples/crosshair.py index 67d3cc5f..076fab49 100644 --- a/examples/crosshair.py +++ b/examples/crosshair.py @@ -7,7 +7,6 @@ the mouse. import initExample ## Add path to library (just for examples; you do not need this) import numpy as np -import scipy.ndimage as ndi import pyqtgraph as pg from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Point import Point @@ -33,8 +32,8 @@ p1.setAutoVisible(y=True) #create numpy arrays #make the numbers large to show that the xrange shows data from 10000 to all the way 0 -data1 = 10000 + 15000 * ndi.gaussian_filter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000) -data2 = 15000 + 15000 * ndi.gaussian_filter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000) +data1 = 10000 + 15000 * pg.gaussianFilter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000) +data2 = 15000 + 15000 * pg.gaussianFilter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000) p1.plot(data1, pen="r") p1.plot(data2, pen="g") diff --git a/examples/isocurve.py b/examples/isocurve.py index fa451063..b401dfe1 100644 --- a/examples/isocurve.py +++ b/examples/isocurve.py @@ -10,7 +10,6 @@ import initExample ## Add path to library (just for examples; you do not need th from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg -import scipy.ndimage as ndi app = QtGui.QApplication([]) @@ -18,7 +17,7 @@ app = QtGui.QApplication([]) frames = 200 data = np.random.normal(size=(frames,30,30), loc=0, scale=100) data = np.concatenate([data, data], axis=0) -data = ndi.gaussian_filter(data, (10, 10, 10))[frames/2:frames + frames/2] +data = pg.gaussianFilter(data, (10, 10, 10))[frames/2:frames + frames/2] data[:, 15:16, 15:17] += 1 win = pg.GraphicsWindow() diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py index e5bb2453..b72fbca5 100644 --- a/pyqtgraph/flowchart/library/Filters.py +++ b/pyqtgraph/flowchart/library/Filters.py @@ -2,6 +2,7 @@ from ...Qt import QtCore, QtGui from ..Node import Node from . import functions +from ... import functions as pgfn from .common import * import numpy as np @@ -161,7 +162,7 @@ class Gaussian(CtrlNode): import scipy.ndimage except ImportError: raise Exception("GaussianFilter node requires the package scipy.ndimage.") - return scipy.ndimage.gaussian_filter(data, self.ctrls['sigma'].value()) + return pgfn.gaussianFilter(data, self.ctrls['sigma'].value()) class Derivative(CtrlNode): diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 57d9a685..f76a71c9 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1120,6 +1120,45 @@ def colorToAlpha(data, color): #raise Exception() return np.clip(output, 0, 255).astype(np.ubyte) + +def gaussianFilter(data, sigma): + """ + Drop-in replacement for scipy.ndimage.gaussian_filter. + + (note: results are only approximately equal to the output of + gaussian_filter) + """ + if np.isscalar(sigma): + sigma = (sigma,) * data.ndim + + baseline = data.mean() + filtered = data - baseline + for ax in range(data.ndim): + s = sigma[ax] + if s == 0: + continue + + # generate 1D gaussian kernel + ksize = int(s * 6) + x = np.arange(-ksize, ksize) + kernel = np.exp(-x**2 / (2*s**2)) + kshape = [1,] * data.ndim + kshape[ax] = len(kernel) + kernel = kernel.reshape(kshape) + + # convolve as product of FFTs + shape = data.shape[ax] + ksize + scale = 1.0 / (abs(s) * (2*np.pi)**0.5) + filtered = scale * np.fft.irfft(np.fft.rfft(filtered, shape, axis=ax) * + np.fft.rfft(kernel, shape, axis=ax), + axis=ax) + + # clip off extra data + sl = [slice(None)] * data.ndim + sl[ax] = slice(filtered.shape[ax]-data.shape[ax],None,None) + filtered = filtered[sl] + return filtered + baseline + def downsample(data, n, axis=0, xvals='subsample'): """Downsample by averaging points together across axis. @@ -1556,7 +1595,7 @@ def traceImage(image, values, smooth=0.5): paths = [] for i in range(diff.shape[-1]): d = (labels==i).astype(float) - d = ndi.gaussian_filter(d, (smooth, smooth)) + d = gaussianFilter(d, (smooth, smooth)) lines = isocurve(d, 0.5, connected=True, extendToEdge=True) path = QtGui.QPainterPath() for line in lines: From e46bb3144926983cd51fb7e8ec64a91af8dd411e Mon Sep 17 00:00:00 2001 From: fabioz Date: Thu, 13 Mar 2014 15:52:49 -0300 Subject: [PATCH 153/268] Checking if view is alive before returning it. --- pyqtgraph/graphicsItems/GraphicsItem.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 2cae5d20..69432ea2 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -5,6 +5,10 @@ from .. import functions as fn import weakref import operator from ..util.lru_cache import LRUCache +from pyqtgraph.Qt import USE_PYSIDE + +if USE_PYSIDE: + from PySide import shiboken class GraphicsItem(object): @@ -53,7 +57,13 @@ class GraphicsItem(object): if len(views) < 1: return None self._viewWidget = weakref.ref(self.scene().views()[0]) - return self._viewWidget() + + v = self._viewWidget() + if USE_PYSIDE: + if not shiboken.isValid(v): + return None + + return v def forgetViewWidget(self): self._viewWidget = None From 6433795e7828c40a48e4796577cf8ac3e22c384a Mon Sep 17 00:00:00 2001 From: fabioz Date: Thu, 13 Mar 2014 17:38:50 -0300 Subject: [PATCH 154/268] Make sure we don't leave view boxes alive by doing a 'bridge' for the on the plot items. Also added warnings if proper cleanup wasn't done. --- pyqtgraph/Qt.py | 5 +++ pyqtgraph/__init__.py | 6 +++- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 34 +++++++++++++------- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 7 +++- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 410bfd83..62ffa2d0 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -32,6 +32,9 @@ else: if USE_PYSIDE: from PySide import QtGui, QtCore, QtOpenGL, QtSvg import PySide + from PySide import shiboken + isQObjectAlive = shiboken.isValid + VERSION_INFO = 'PySide ' + PySide.__version__ # Make a loadUiType function like PyQt has @@ -78,6 +81,8 @@ else: pass + import sip + isQObjectAlive = sip.isdeleted loadUiType = uic.loadUiType QtCore.Signal = QtCore.pyqtSignal diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 5f42e64f..cd78cfa8 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -256,6 +256,7 @@ from .graphicsWindows import * from .SignalProxy import * from .colormap import * from .ptime import time +from pyqtgraph.Qt import isQObjectAlive ############################################################## @@ -284,7 +285,10 @@ def cleanup(): s = QtGui.QGraphicsScene() for o in gc.get_objects(): try: - if isinstance(o, QtGui.QGraphicsItem) and o.scene() is None: + if isinstance(o, QtGui.QGraphicsItem) and isQObjectAlive(o) and o.scene() is None: + sys.stderr.write( + 'Error: graphics item without scene. Make sure ViewBox.close() and GraphicsView.close() are properly called before app shutdown (%s)\n' % (o,)) + s.addItem(o) except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object continue diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 7c02a534..c0b9adab 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -18,6 +18,7 @@ This class is very heavily featured: """ from ...Qt import QtGui, QtCore, QtSvg, USE_PYSIDE from ... import pixmaps +import sys if USE_PYSIDE: from .plotConfigTemplate_pyside import * @@ -193,14 +194,6 @@ class PlotItem(GraphicsWidget): self.layout.setColumnStretchFactor(1, 100) - ## Wrap a few methods from viewBox - for m in [ - 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', - 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', 'setLimits', - 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY', - 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. - setattr(self, m, getattr(self.vb, m)) - self.items = [] self.curves = [] self.itemMeta = weakref.WeakKeyDictionary() @@ -298,7 +291,26 @@ class PlotItem(GraphicsWidget): """Return the :class:`ViewBox ` contained within.""" return self.vb + ## Wrap a few methods from viewBox. + #Important: don't use a settattr(m, getattr(self.vb, m)) as we'd be leaving the viebox alive + #because we had a reference to an instance method (creating wrapper methods at runtime instead). + frame = sys._getframe() + for m in [ + 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', + 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', 'setLimits', + 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY', + 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. + def _create_method(name): # @NoSelf + def method(self, *args, **kwargs): + return getattr(self.vb, name)(*args, **kwargs) + method.__name__ = name + return method + + frame.f_locals[m] = _create_method(m) + + del _create_method + del frame def setLogMode(self, x=None, y=None): """ @@ -356,10 +368,8 @@ class PlotItem(GraphicsWidget): self.ctrlMenu.setParent(None) self.ctrlMenu = None - #self.ctrlBtn.setParent(None) - #self.ctrlBtn = None - #self.autoBtn.setParent(None) - #self.autoBtn = None + self.autoBtn.setParent(None) + self.autoBtn = None for k in self.axes: i = self.axes[k]['item'] diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 58b2aeba..ef5b4319 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -5,11 +5,12 @@ from ...Point import Point from ... import functions as fn from .. ItemGroup import ItemGroup from .. GraphicsWidget import GraphicsWidget -from ...GraphicsScene import GraphicsScene import weakref from copy import deepcopy from ... import debug as debug from ... import getConfigOption +import sys +from pyqtgraph.Qt import isQObjectAlive __all__ = ['ViewBox'] @@ -240,6 +241,7 @@ class ViewBox(GraphicsWidget): del ViewBox.NamedViews[self.name] def close(self): + self.clear() self.unregister() def implements(self, interface): @@ -1653,6 +1655,9 @@ class ViewBox(GraphicsWidget): ## called when the application is about to exit. ## this disables all callbacks, which might otherwise generate errors if invoked during exit. for k in ViewBox.AllViews: + if isQObjectAlive(k): + sys.stderr.write('ViewBox should be closed before application exit!') + try: k.destroyed.disconnect() except RuntimeError: ## signal is already disconnected. From 9cfc3a9f851e571277f6fe15c0e64d47faaadc51 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Mar 2014 14:11:53 -0400 Subject: [PATCH 155/268] Update README to reflect loss of scipy dependency --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e198abd..b585a6bd 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ Requirements * PyQt 4.7+ or PySide * python 2.6, 2.7, or 3.x - * numpy, scipy - * For 3D graphics: pyopengl + * NumPy + * For 3D graphics: pyopengl and qt-opengl * Known to run on Windows, Linux, and Mac. Support @@ -53,6 +53,9 @@ Installation Methods * To install system-wide from source distribution: `$ python setup.py install` * For instalation packages, see the website (pyqtgraph.org) + * On debian-like systems, pyqtgraph requires the following packages: + python-numpy, python-qt4 | python-pyside + For 3D support: python-opengl, python-qt4-gl | python-pyside.qtopengl Documentation ------------- From adfcfa99a1c8931477ffe7133861a74ca0a1512a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Mar 2014 03:45:46 -0400 Subject: [PATCH 156/268] Fixed multiprocess port re-use on windows --- pyqtgraph/multiprocess/processes.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index a08b449c..fac985e9 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -75,16 +75,8 @@ class Process(RemoteEventHandler): #print "key:", ' '.join([str(ord(x)) for x in authkey]) ## Listen for connection from remote process (and find free port number) - port = 10000 - while True: - try: - l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey) - break - except socket.error as ex: - if ex.errno != 98 and ex.errno != 10048: # unix=98, win=10048 - raise - port += 1 - + l = multiprocessing.connection.Listener(('localhost', 0), authkey=authkey) + port = l.address[1] ## start remote process, instruct it to run target function sysPath = sys.path if copySysPath else None From 89c04c8a8128ee73f55c575d79afc0bcecc85bda Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Mar 2014 16:23:23 -0400 Subject: [PATCH 157/268] Corrected bug in multiprocess causing deadlock at exit Multiprocess debugging messages now use one color per process Corrected RemoteGraphicsView not setting correct pg options on remote process New debugging tools: * util.cprint for printing color on terminal (based on colorama) * debug.ThreadColor causes each thread to print in a different color * debug.PeriodicTrace used for debugging deadlocks * Mutex for detecting deadlocks --- pyqtgraph/debug.py | 76 +++++- pyqtgraph/multiprocess/processes.py | 55 +++-- pyqtgraph/multiprocess/remoteproxy.py | 5 +- pyqtgraph/util/Mutex.py | 277 +++++++++++++++++++++ pyqtgraph/util/colorama/LICENSE.txt | 28 +++ pyqtgraph/util/colorama/README.txt | 304 ++++++++++++++++++++++++ pyqtgraph/util/colorama/__init__.py | 0 pyqtgraph/util/colorama/win32.py | 134 +++++++++++ pyqtgraph/util/colorama/winterm.py | 120 ++++++++++ pyqtgraph/util/cprint.py | 101 ++++++++ pyqtgraph/widgets/RemoteGraphicsView.py | 3 +- 11 files changed, 1079 insertions(+), 24 deletions(-) create mode 100644 pyqtgraph/util/Mutex.py create mode 100644 pyqtgraph/util/colorama/LICENSE.txt create mode 100644 pyqtgraph/util/colorama/README.txt create mode 100644 pyqtgraph/util/colorama/__init__.py create mode 100644 pyqtgraph/util/colorama/win32.py create mode 100644 pyqtgraph/util/colorama/winterm.py create mode 100644 pyqtgraph/util/cprint.py diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index f208f3a5..4756423c 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -7,10 +7,12 @@ Distributed under MIT/X11 license. See license.txt for more infomation. from __future__ import print_function -import sys, traceback, time, gc, re, types, weakref, inspect, os, cProfile +import sys, traceback, time, gc, re, types, weakref, inspect, os, cProfile, threading from . import ptime from numpy import ndarray from .Qt import QtCore, QtGui +from .util.Mutex import Mutex +from .util import cprint __ftraceDepth = 0 def ftrace(func): @@ -991,3 +993,75 @@ class PrintDetector(object): def flush(self): self.stdout.flush() + + +class PeriodicTrace(object): + """ + Used to debug freezing by starting a new thread that reports on the + location of the main thread periodically. + """ + class ReportThread(QtCore.QThread): + def __init__(self): + self.frame = None + self.ind = 0 + self.lastInd = None + self.lock = Mutex() + QtCore.QThread.__init__(self) + + def notify(self, frame): + with self.lock: + self.frame = frame + self.ind += 1 + + def run(self): + while True: + time.sleep(1) + with self.lock: + if self.lastInd != self.ind: + print("== Trace %d: ==" % self.ind) + traceback.print_stack(self.frame) + self.lastInd = self.ind + + def __init__(self): + self.mainThread = threading.current_thread() + self.thread = PeriodicTrace.ReportThread() + self.thread.start() + sys.settrace(self.trace) + + def trace(self, frame, event, arg): + if threading.current_thread() is self.mainThread: # and 'threading' not in frame.f_code.co_filename: + self.thread.notify(frame) + # print("== Trace ==", event, arg) + # traceback.print_stack(frame) + return self.trace + + + +class ThreadColor(object): + """ + Wrapper on stdout/stderr that colors text by the current thread ID. + + *stream* must be 'stdout' or 'stderr'. + """ + colors = {} + lock = Mutex() + + def __init__(self, stream): + self.stream = getattr(sys, stream) + self.err = stream == 'stderr' + setattr(sys, stream, self) + + def write(self, msg): + with self.lock: + cprint.cprint(self.stream, self.color(), msg, -1, stderr=self.err) + + def flush(self): + with self.lock: + self.stream.flush() + + def color(self): + tid = threading.current_thread() + if tid not in self.colors: + c = (len(self.colors) % 15) + 1 + self.colors[tid] = c + return self.colors[tid] diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index fac985e9..0dfb80b9 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -1,13 +1,15 @@ -from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy import subprocess, atexit, os, sys, time, random, socket, signal import multiprocessing.connection -from ..Qt import USE_PYSIDE - try: import cPickle as pickle except ImportError: import pickle +from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy +from ..Qt import USE_PYSIDE +from ..util import cprint # color printing for debugging + + __all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ClosedError', 'NoResultError'] class Process(RemoteEventHandler): @@ -35,7 +37,8 @@ class Process(RemoteEventHandler): return objects either by proxy or by value (if they are picklable). See ProxyObject for more information. """ - + _process_count = 1 # just used for assigning colors to each process for debugging + def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None): """ ============== ============================================================= @@ -64,7 +67,7 @@ class Process(RemoteEventHandler): name = str(self) if executable is None: executable = sys.executable - self.debug = debug + self.debug = 7 if debug is True else False # 7 causes printing in white ## random authentication key authkey = os.urandom(20) @@ -82,6 +85,13 @@ class Process(RemoteEventHandler): sysPath = sys.path if copySysPath else None bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py')) self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap)) + + # Decide on printing color for this process + if debug: + procDebug = (Process._process_count%6) + 1 # pick a color for this process to print in + Process._process_count += 1 + else: + procDebug = False if wrapStdout is None: wrapStdout = sys.platform.startswith('win') @@ -94,8 +104,8 @@ class Process(RemoteEventHandler): self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=stdout, stderr=stderr) ## to circumvent the bug and still make the output visible, we use ## background threads to pass data from pipes to stdout/stderr - self._stdoutForwarder = FileForwarder(self.proc.stdout, "stdout") - self._stderrForwarder = FileForwarder(self.proc.stderr, "stderr") + self._stdoutForwarder = FileForwarder(self.proc.stdout, "stdout", procDebug) + self._stderrForwarder = FileForwarder(self.proc.stderr, "stderr", procDebug) else: self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE) @@ -112,7 +122,7 @@ class Process(RemoteEventHandler): targetStr=targetStr, path=sysPath, pyside=USE_PYSIDE, - debug=debug + debug=procDebug ) pickle.dump(data, self.proc.stdin) self.proc.stdin.close() @@ -128,8 +138,8 @@ class Process(RemoteEventHandler): continue else: raise - - RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid, debug=debug) + + RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid, debug=self.debug) self.debugMsg('Connected to child process.') atexit.register(self.join) @@ -159,10 +169,11 @@ class Process(RemoteEventHandler): def startEventLoop(name, port, authkey, ppid, debug=False): if debug: import os - print('[%d] connecting to server at port localhost:%d, authkey=%s..' % (os.getpid(), port, repr(authkey))) + cprint.cout(debug, '[%d] connecting to server at port localhost:%d, authkey=%s..\n' + % (os.getpid(), port, repr(authkey)), -1) conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) if debug: - print('[%d] connected; starting remote proxy.' % os.getpid()) + cprint.cout(debug, '[%d] connected; starting remote proxy.\n' % os.getpid(), -1) global HANDLER #ppid = 0 if not hasattr(os, 'getppid') else os.getppid() HANDLER = RemoteEventHandler(conn, name, ppid, debug=debug) @@ -372,17 +383,17 @@ class QtProcess(Process): def __init__(self, **kwds): if 'target' not in kwds: kwds['target'] = startQtEventLoop + from ..Qt import QtGui ## avoid module-level import to keep bootstrap snappy. self._processRequests = kwds.pop('processRequests', True) + if self._processRequests and QtGui.QApplication.instance() is None: + raise Exception("Must create QApplication before starting QtProcess, or use QtProcess(processRequests=False)") Process.__init__(self, **kwds) self.startEventTimer() def startEventTimer(self): - from ..Qt import QtGui, QtCore ## avoid module-level import to keep bootstrap snappy. + from ..Qt import QtCore ## avoid module-level import to keep bootstrap snappy. self.timer = QtCore.QTimer() if self._processRequests: - app = QtGui.QApplication.instance() - if app is None: - raise Exception("Must create QApplication before starting QtProcess, or use QtProcess(processRequests=False)") self.startRequestProcessing() def startRequestProcessing(self, interval=0.01): @@ -404,10 +415,10 @@ class QtProcess(Process): def startQtEventLoop(name, port, authkey, ppid, debug=False): if debug: import os - print('[%d] connecting to server at port localhost:%d, authkey=%s..' % (os.getpid(), port, repr(authkey))) + cprint.cout(debug, '[%d] connecting to server at port localhost:%d, authkey=%s..\n' % (os.getpid(), port, repr(authkey)), -1) conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) if debug: - print('[%d] connected; starting remote proxy.' % os.getpid()) + cprint.cout(debug, '[%d] connected; starting remote proxy.\n' % os.getpid(), -1) from ..Qt import QtGui, QtCore #from PyQt4 import QtGui, QtCore app = QtGui.QApplication.instance() @@ -437,11 +448,13 @@ class FileForwarder(threading.Thread): which ensures that the correct behavior is achieved even if sys.stdout/stderr are replaced at runtime. """ - def __init__(self, input, output): + def __init__(self, input, output, color): threading.Thread.__init__(self) self.input = input self.output = output self.lock = threading.Lock() + self.daemon = True + self.color = color self.start() def run(self): @@ -449,12 +462,12 @@ class FileForwarder(threading.Thread): while True: line = self.input.readline() with self.lock: - sys.stdout.write(line) + cprint.cout(self.color, line, -1) elif self.output == 'stderr': while True: line = self.input.readline() with self.lock: - sys.stderr.write(line) + cprint.cerr(self.color, line, -1) else: while True: line = self.input.readline() diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 70ce90a6..8287d0e8 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -7,6 +7,9 @@ except ImportError: import builtins import pickle +# color printing for debugging +from ..util import cprint + class ClosedError(Exception): """Raised when an event handler receives a request to close the connection or discovers that the connection has been closed.""" @@ -80,7 +83,7 @@ class RemoteEventHandler(object): def debugMsg(self, msg): if not self.debug: return - print("[%d] %s" % (os.getpid(), str(msg))) + cprint.cout(self.debug, "%d [%d] %s\n" % (self.debug, os.getpid(), str(msg)), -1) def getProxyOption(self, opt): return self.proxyOptions[opt] diff --git a/pyqtgraph/util/Mutex.py b/pyqtgraph/util/Mutex.py new file mode 100644 index 00000000..8335a812 --- /dev/null +++ b/pyqtgraph/util/Mutex.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +""" +Mutex.py - Stand-in extension of Qt's QMutex class +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from PyQt4 import QtCore +import traceback + +class Mutex(QtCore.QMutex): + """Extends QMutex to provide warning messages when a mutex stays locked for a long time. + Mostly just useful for debugging purposes. Should only be used with MutexLocker, not + QMutexLocker. + """ + + def __init__(self, *args, **kargs): + if kargs.get('recursive', False): + args = (QtCore.QMutex.Recursive,) + QtCore.QMutex.__init__(self, *args) + self.l = QtCore.QMutex() ## for serializing access to self.tb + self.tb = [] + self.debug = False ## True to enable debugging functions + + def tryLock(self, timeout=None, id=None): + if timeout is None: + locked = QtCore.QMutex.tryLock(self) + else: + locked = QtCore.QMutex.tryLock(self, timeout) + + if self.debug and locked: + self.l.lock() + try: + if id is None: + self.tb.append(''.join(traceback.format_stack()[:-1])) + else: + self.tb.append(" " + str(id)) + #print 'trylock', self, len(self.tb) + finally: + self.l.unlock() + return locked + + def lock(self, id=None): + c = 0 + waitTime = 5000 # in ms + while True: + if self.tryLock(waitTime, id): + break + c += 1 + if self.debug: + self.l.lock() + try: + print "Waiting for mutex lock (%0.1f sec). Traceback follows:" % (c*waitTime/1000.) + traceback.print_stack() + if len(self.tb) > 0: + print "Mutex is currently locked from:\n", self.tb[-1] + else: + print "Mutex is currently locked from [???]" + finally: + self.l.unlock() + #print 'lock', self, len(self.tb) + + def unlock(self): + QtCore.QMutex.unlock(self) + if self.debug: + self.l.lock() + try: + #print 'unlock', self, len(self.tb) + if len(self.tb) > 0: + self.tb.pop() + else: + raise Exception("Attempt to unlock mutex before it has been locked") + finally: + self.l.unlock() + + def depth(self): + self.l.lock() + n = len(self.tb) + self.l.unlock() + return n + + def traceback(self): + self.l.lock() + try: + ret = self.tb[:] + finally: + self.l.unlock() + return ret + + def __exit__(self, *args): + self.unlock() + + def __enter__(self): + self.lock() + return self + + +class MutexLocker: + def __init__(self, lock): + #print self, "lock on init",lock, lock.depth() + self.lock = lock + self.lock.lock() + self.unlockOnDel = True + + def unlock(self): + #print self, "unlock by req",self.lock, self.lock.depth() + self.lock.unlock() + self.unlockOnDel = False + + + def relock(self): + #print self, "relock by req",self.lock, self.lock.depth() + self.lock.lock() + self.unlockOnDel = True + + def __del__(self): + if self.unlockOnDel: + #print self, "Unlock by delete:", self.lock, self.lock.depth() + self.lock.unlock() + #else: + #print self, "Skip unlock by delete", self.lock, self.lock.depth() + + def __exit__(self, *args): + if self.unlockOnDel: + self.unlock() + + def __enter__(self): + return self + + def mutex(self): + return self.lock + +#import functools +#def methodWrapper(fn, self, *args, **kargs): + #print repr(fn), repr(self), args, kargs + #obj = self.__wrapped_object__() + #return getattr(obj, fn)(*args, **kargs) + +##def WrapperClass(clsName, parents, attrs): + ##for parent in parents: + ##for name in dir(parent): + ##attr = getattr(parent, name) + ##if callable(attr) and name not in attrs: + ##attrs[name] = functools.partial(funcWrapper, name) + ##return type(clsName, parents, attrs) + +#def WrapperClass(name, bases, attrs): + #for n in ['__getattr__', '__setattr__', '__getitem__', '__setitem__']: + #if n not in attrs: + #attrs[n] = functools.partial(methodWrapper, n) + #return type(name, bases, attrs) + +#class WrapperClass(type): + #def __new__(cls, name, bases, attrs): + #fakes = [] + #for n in ['__getitem__', '__setitem__']: + #if n not in attrs: + #attrs[n] = lambda self, *args: getattr(self, n)(*args) + #fakes.append(n) + #print fakes + #typ = type(name, bases, attrs) + #typ.__faked_methods__ = fakes + #return typ + + #def __init__(self, name, bases, attrs): + #print self.__faked_methods__ + #for n in self.__faked_methods__: + #self.n = None + + + +#class ThreadsafeWrapper(object): + #def __init__(self, obj): + #self.__TSW_object__ = obj + + #def __wrapped_object__(self): + #return self.__TSW_object__ + + +class ThreadsafeWrapper(object): + """Wrapper that makes access to any object thread-safe (within reasonable limits). + Mostly tested for wrapping lists, dicts, etc. + NOTE: Do not instantiate directly; use threadsafe(obj) instead. + - all method calls and attribute/item accesses are protected by mutex + - optionally, attribute/item accesses may return protected objects + - can be manually locked for extended operations + """ + def __init__(self, obj, recursive=False, reentrant=True): + """ + If recursive is True, then sub-objects accessed from obj are wrapped threadsafe as well. + If reentrant is True, then the object can be locked multiple times from the same thread.""" + + self.__TSOwrapped_object__ = obj + + if reentrant: + self.__TSOwrap_lock__ = Mutex(QtCore.QMutex.Recursive) + else: + self.__TSOwrap_lock__ = Mutex() + self.__TSOrecursive__ = recursive + self.__TSOreentrant__ = reentrant + self.__TSOwrapped_objs__ = {} + + def lock(self, id=None): + self.__TSOwrap_lock__.lock(id=id) + + def tryLock(self, timeout=None, id=None): + self.__TSOwrap_lock__.tryLock(timeout=timeout, id=id) + + def unlock(self): + self.__TSOwrap_lock__.unlock() + + def unwrap(self): + return self.__TSOwrapped_object__ + + def __safe_call__(self, fn, *args, **kargs): + obj = self.__wrapped_object__() + ret = getattr(obj, fn)(*args, **kargs) + return self.__wrap_object__(ret) + + def __getattr__(self, attr): + #try: + #return object.__getattribute__(self, attr) + #except AttributeError: + with self.__TSOwrap_lock__: + val = getattr(self.__wrapped_object__(), attr) + #if callable(val): + #return self.__wrap_object__(val) + return self.__wrap_object__(val) + + def __setattr__(self, attr, val): + if attr[:5] == '__TSO': + #return object.__setattr__(self, attr, val) + self.__dict__[attr] = val + return + with self.__TSOwrap_lock__: + return setattr(self.__wrapped_object__(), attr, val) + + def __wrap_object__(self, obj): + if not self.__TSOrecursive__: + return obj + if obj.__class__ in [int, float, str, unicode, tuple]: + return obj + if id(obj) not in self.__TSOwrapped_objs__: + self.__TSOwrapped_objs__[id(obj)] = threadsafe(obj, recursive=self.__TSOrecursive__, reentrant=self.__TSOreentrant__) + return self.__TSOwrapped_objs__[id(obj)] + + def __wrapped_object__(self): + #if isinstance(self.__TSOwrapped_object__, weakref.ref): + #return self.__TSOwrapped_object__() + #else: + return self.__TSOwrapped_object__ + +def mkMethodWrapper(name): + return lambda self, *args, **kargs: self.__safe_call__(name, *args, **kargs) + +def threadsafe(obj, *args, **kargs): + """Return a thread-safe wrapper around obj. (see ThreadsafeWrapper) + args and kargs are passed directly to ThreadsafeWrapper.__init__() + This factory function is necessary for wrapping special methods (like __getitem__)""" + if type(obj) in [int, float, str, unicode, tuple, type(None), bool]: + return obj + clsName = 'Threadsafe_' + obj.__class__.__name__ + attrs = {} + ignore = set(['__new__', '__init__', '__class__', '__hash__', '__getattribute__', '__getattr__', '__setattr__']) + for n in dir(obj): + if not n.startswith('__') or n in ignore: + continue + v = getattr(obj, n) + if callable(v): + attrs[n] = mkMethodWrapper(n) + typ = type(clsName, (ThreadsafeWrapper,), attrs) + return typ(obj, *args, **kargs) + + +if __name__ == '__main__': + d = {'x': 3, 'y': [1,2,3,4], 'z': {'a': 3}, 'w': (1,2,3,4)} + t = threadsafe(d, recursive=True, reentrant=False) \ No newline at end of file diff --git a/pyqtgraph/util/colorama/LICENSE.txt b/pyqtgraph/util/colorama/LICENSE.txt new file mode 100644 index 00000000..5f567799 --- /dev/null +++ b/pyqtgraph/util/colorama/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright (c) 2010 Jonathan Hartley +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holders, nor those of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/pyqtgraph/util/colorama/README.txt b/pyqtgraph/util/colorama/README.txt new file mode 100644 index 00000000..8910ba5b --- /dev/null +++ b/pyqtgraph/util/colorama/README.txt @@ -0,0 +1,304 @@ +Download and docs: + http://pypi.python.org/pypi/colorama +Development: + http://code.google.com/p/colorama +Discussion group: + https://groups.google.com/forum/#!forum/python-colorama + +Description +=========== + +Makes ANSI escape character sequences for producing colored terminal text and +cursor positioning work under MS Windows. + +ANSI escape character sequences have long been used to produce colored terminal +text and cursor positioning on Unix and Macs. Colorama makes this work on +Windows, too, by wrapping stdout, stripping ANSI sequences it finds (which +otherwise show up as gobbledygook in your output), and converting them into the +appropriate win32 calls to modify the state of the terminal. On other platforms, +Colorama does nothing. + +Colorama also provides some shortcuts to help generate ANSI sequences +but works fine in conjunction with any other ANSI sequence generation library, +such as Termcolor (http://pypi.python.org/pypi/termcolor.) + +This has the upshot of providing a simple cross-platform API for printing +colored terminal text from Python, and has the happy side-effect that existing +applications or libraries which use ANSI sequences to produce colored output on +Linux or Macs can now also work on Windows, simply by calling +``colorama.init()``. + +An alternative approach is to install 'ansi.sys' on Windows machines, which +provides the same behaviour for all applications running in terminals. Colorama +is intended for situations where that isn't easy (e.g. maybe your app doesn't +have an installer.) + +Demo scripts in the source code repository prints some colored text using +ANSI sequences. Compare their output under Gnome-terminal's built in ANSI +handling, versus on Windows Command-Prompt using Colorama: + +.. image:: http://colorama.googlecode.com/hg/screenshots/ubuntu-demo.png + :width: 661 + :height: 357 + :alt: ANSI sequences on Ubuntu under gnome-terminal. + +.. image:: http://colorama.googlecode.com/hg/screenshots/windows-demo.png + :width: 668 + :height: 325 + :alt: Same ANSI sequences on Windows, using Colorama. + +These screengrabs show that Colorama on Windows does not support ANSI 'dim +text': it looks the same as 'normal text'. + + +License +======= + +Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. + + +Dependencies +============ + +None, other than Python. Tested on Python 2.5.5, 2.6.5, 2.7, 3.1.2, and 3.2 + +Usage +===== + +Initialisation +-------------- + +Applications should initialise Colorama using:: + + from colorama import init + init() + +If you are on Windows, the call to ``init()`` will start filtering ANSI escape +sequences out of any text sent to stdout or stderr, and will replace them with +equivalent Win32 calls. + +Calling ``init()`` has no effect on other platforms (unless you request other +optional functionality, see keyword args below.) The intention is that +applications can call ``init()`` unconditionally on all platforms, after which +ANSI output should just work. + +To stop using colorama before your program exits, simply call ``deinit()``. +This will restore stdout and stderr to their original values, so that Colorama +is disabled. To start using Colorama again, call ``reinit()``, which wraps +stdout and stderr again, but is cheaper to call than doing ``init()`` all over +again. + + +Colored Output +-------------- + +Cross-platform printing of colored text can then be done using Colorama's +constant shorthand for ANSI escape sequences:: + + from colorama import Fore, Back, Style + print(Fore.RED + 'some red text') + print(Back.GREEN + 'and with a green background') + print(Style.DIM + 'and in dim text') + print(Fore.RESET + Back.RESET + Style.RESET_ALL) + print('back to normal now') + +or simply by manually printing ANSI sequences from your own code:: + + print('/033[31m' + 'some red text') + print('/033[30m' # and reset to default color) + +or Colorama can be used happily in conjunction with existing ANSI libraries +such as Termcolor:: + + from colorama import init + from termcolor import colored + + # use Colorama to make Termcolor work on Windows too + init() + + # then use Termcolor for all colored text output + print(colored('Hello, World!', 'green', 'on_red')) + +Available formatting constants are:: + + Fore: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET. + Back: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET. + Style: DIM, NORMAL, BRIGHT, RESET_ALL + +Style.RESET_ALL resets foreground, background and brightness. Colorama will +perform this reset automatically on program exit. + + +Cursor Positioning +------------------ + +ANSI codes to reposition the cursor are supported. See demos/demo06.py for +an example of how to generate them. + + +Init Keyword Args +----------------- + +``init()`` accepts some kwargs to override default behaviour. + +init(autoreset=False): + If you find yourself repeatedly sending reset sequences to turn off color + changes at the end of every print, then ``init(autoreset=True)`` will + automate that:: + + from colorama import init + init(autoreset=True) + print(Fore.RED + 'some red text') + print('automatically back to default color again') + +init(strip=None): + Pass ``True`` or ``False`` to override whether ansi codes should be + stripped from the output. The default behaviour is to strip if on Windows. + +init(convert=None): + Pass ``True`` or ``False`` to override whether to convert ansi codes in the + output into win32 calls. The default behaviour is to convert if on Windows + and output is to a tty (terminal). + +init(wrap=True): + On Windows, colorama works by replacing ``sys.stdout`` and ``sys.stderr`` + with proxy objects, which override the .write() method to do their work. If + this wrapping causes you problems, then this can be disabled by passing + ``init(wrap=False)``. The default behaviour is to wrap if autoreset or + strip or convert are True. + + When wrapping is disabled, colored printing on non-Windows platforms will + continue to work as normal. To do cross-platform colored output, you can + use Colorama's ``AnsiToWin32`` proxy directly:: + + import sys + from colorama import init, AnsiToWin32 + init(wrap=False) + stream = AnsiToWin32(sys.stderr).stream + + # Python 2 + print >>stream, Fore.BLUE + 'blue text on stderr' + + # Python 3 + print(Fore.BLUE + 'blue text on stderr', file=stream) + + +Status & Known Problems +======================= + +I've personally only tested it on WinXP (CMD, Console2), Ubuntu +(gnome-terminal, xterm), and OSX. + +Some presumably valid ANSI sequences aren't recognised (see details below) +but to my knowledge nobody has yet complained about this. Puzzling. + +See outstanding issues and wishlist at: +http://code.google.com/p/colorama/issues/list + +If anything doesn't work for you, or doesn't do what you expected or hoped for, +I'd love to hear about it on that issues list, would be delighted by patches, +and would be happy to grant commit access to anyone who submits a working patch +or two. + + +Recognised ANSI Sequences +========================= + +ANSI sequences generally take the form: + + ESC [ ; ... + +Where is an integer, and is a single letter. Zero or more +params are passed to a . If no params are passed, it is generally +synonymous with passing a single zero. No spaces exist in the sequence, they +have just been inserted here to make it easy to read. + +The only ANSI sequences that colorama converts into win32 calls are:: + + ESC [ 0 m # reset all (colors and brightness) + ESC [ 1 m # bright + ESC [ 2 m # dim (looks same as normal brightness) + ESC [ 22 m # normal brightness + + # FOREGROUND: + ESC [ 30 m # black + ESC [ 31 m # red + ESC [ 32 m # green + ESC [ 33 m # yellow + ESC [ 34 m # blue + ESC [ 35 m # magenta + ESC [ 36 m # cyan + ESC [ 37 m # white + ESC [ 39 m # reset + + # BACKGROUND + ESC [ 40 m # black + ESC [ 41 m # red + ESC [ 42 m # green + ESC [ 43 m # yellow + ESC [ 44 m # blue + ESC [ 45 m # magenta + ESC [ 46 m # cyan + ESC [ 47 m # white + ESC [ 49 m # reset + + # cursor positioning + ESC [ y;x H # position cursor at x across, y down + + # clear the screen + ESC [ mode J # clear the screen. Only mode 2 (clear entire screen) + # is supported. It should be easy to add other modes, + # let me know if that would be useful. + +Multiple numeric params to the 'm' command can be combined into a single +sequence, eg:: + + ESC [ 36 ; 45 ; 1 m # bright cyan text on magenta background + +All other ANSI sequences of the form ``ESC [ ; ... `` +are silently stripped from the output on Windows. + +Any other form of ANSI sequence, such as single-character codes or alternative +initial characters, are not recognised nor stripped. It would be cool to add +them though. Let me know if it would be useful for you, via the issues on +google code. + + +Development +=========== + +Help and fixes welcome! Ask Jonathan for commit rights, you'll get them. + +Running tests requires: + +- Michael Foord's 'mock' module to be installed. +- Tests are written using the 2010 era updates to 'unittest', and require to + be run either using Python2.7 or greater, or else to have Michael Foord's + 'unittest2' module installed. + +unittest2 test discovery doesn't work for colorama, so I use 'nose':: + + nosetests -s + +The -s is required because 'nosetests' otherwise applies a proxy of its own to +stdout, which confuses the unit tests. + + +Contact +======= + +Created by Jonathan Hartley, tartley@tartley.com + + +Thanks +====== +| Ben Hoyt, for a magnificent fix under 64-bit Windows. +| Jesse@EmptySquare for submitting a fix for examples in the README. +| User 'jamessp', an observant documentation fix for cursor positioning. +| User 'vaal1239', Dave Mckee & Lackner Kristof for a tiny but much-needed Win7 fix. +| Julien Stuyck, for wisely suggesting Python3 compatible updates to README. +| Daniel Griffith for multiple fabulous patches. +| Oscar Lesta for valuable fix to stop ANSI chars being sent to non-tty output. +| Roger Binns, for many suggestions, valuable feedback, & bug reports. +| Tim Golden for thought and much appreciated feedback on the initial idea. + diff --git a/pyqtgraph/util/colorama/__init__.py b/pyqtgraph/util/colorama/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyqtgraph/util/colorama/win32.py b/pyqtgraph/util/colorama/win32.py new file mode 100644 index 00000000..f4024f95 --- /dev/null +++ b/pyqtgraph/util/colorama/win32.py @@ -0,0 +1,134 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. + +# from winbase.h +STDOUT = -11 +STDERR = -12 + +try: + from ctypes import windll + from ctypes import wintypes +except ImportError: + windll = None + SetConsoleTextAttribute = lambda *_: None +else: + from ctypes import ( + byref, Structure, c_char, c_short, c_uint32, c_ushort, POINTER + ) + + class CONSOLE_SCREEN_BUFFER_INFO(Structure): + """struct in wincon.h.""" + _fields_ = [ + ("dwSize", wintypes._COORD), + ("dwCursorPosition", wintypes._COORD), + ("wAttributes", wintypes.WORD), + ("srWindow", wintypes.SMALL_RECT), + ("dwMaximumWindowSize", wintypes._COORD), + ] + def __str__(self): + return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % ( + self.dwSize.Y, self.dwSize.X + , self.dwCursorPosition.Y, self.dwCursorPosition.X + , self.wAttributes + , self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right + , self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X + ) + + _GetStdHandle = windll.kernel32.GetStdHandle + _GetStdHandle.argtypes = [ + wintypes.DWORD, + ] + _GetStdHandle.restype = wintypes.HANDLE + + _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo + _GetConsoleScreenBufferInfo.argtypes = [ + wintypes.HANDLE, + POINTER(CONSOLE_SCREEN_BUFFER_INFO), + ] + _GetConsoleScreenBufferInfo.restype = wintypes.BOOL + + _SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute + _SetConsoleTextAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, + ] + _SetConsoleTextAttribute.restype = wintypes.BOOL + + _SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition + _SetConsoleCursorPosition.argtypes = [ + wintypes.HANDLE, + wintypes._COORD, + ] + _SetConsoleCursorPosition.restype = wintypes.BOOL + + _FillConsoleOutputCharacterA = windll.kernel32.FillConsoleOutputCharacterA + _FillConsoleOutputCharacterA.argtypes = [ + wintypes.HANDLE, + c_char, + wintypes.DWORD, + wintypes._COORD, + POINTER(wintypes.DWORD), + ] + _FillConsoleOutputCharacterA.restype = wintypes.BOOL + + _FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute + _FillConsoleOutputAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, + wintypes.DWORD, + wintypes._COORD, + POINTER(wintypes.DWORD), + ] + _FillConsoleOutputAttribute.restype = wintypes.BOOL + + handles = { + STDOUT: _GetStdHandle(STDOUT), + STDERR: _GetStdHandle(STDERR), + } + + def GetConsoleScreenBufferInfo(stream_id=STDOUT): + handle = handles[stream_id] + csbi = CONSOLE_SCREEN_BUFFER_INFO() + success = _GetConsoleScreenBufferInfo( + handle, byref(csbi)) + return csbi + + def SetConsoleTextAttribute(stream_id, attrs): + handle = handles[stream_id] + return _SetConsoleTextAttribute(handle, attrs) + + def SetConsoleCursorPosition(stream_id, position): + position = wintypes._COORD(*position) + # If the position is out of range, do nothing. + if position.Y <= 0 or position.X <= 0: + return + # Adjust for Windows' SetConsoleCursorPosition: + # 1. being 0-based, while ANSI is 1-based. + # 2. expecting (x,y), while ANSI uses (y,x). + adjusted_position = wintypes._COORD(position.Y - 1, position.X - 1) + # Adjust for viewport's scroll position + sr = GetConsoleScreenBufferInfo(STDOUT).srWindow + adjusted_position.Y += sr.Top + adjusted_position.X += sr.Left + # Resume normal processing + handle = handles[stream_id] + return _SetConsoleCursorPosition(handle, adjusted_position) + + def FillConsoleOutputCharacter(stream_id, char, length, start): + handle = handles[stream_id] + char = c_char(char) + length = wintypes.DWORD(length) + num_written = wintypes.DWORD(0) + # Note that this is hard-coded for ANSI (vs wide) bytes. + success = _FillConsoleOutputCharacterA( + handle, char, length, start, byref(num_written)) + return num_written.value + + def FillConsoleOutputAttribute(stream_id, attr, length, start): + ''' FillConsoleOutputAttribute( hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten )''' + handle = handles[stream_id] + attribute = wintypes.WORD(attr) + length = wintypes.DWORD(length) + num_written = wintypes.DWORD(0) + # Note that this is hard-coded for ANSI (vs wide) bytes. + return _FillConsoleOutputAttribute( + handle, attribute, length, start, byref(num_written)) diff --git a/pyqtgraph/util/colorama/winterm.py b/pyqtgraph/util/colorama/winterm.py new file mode 100644 index 00000000..27088115 --- /dev/null +++ b/pyqtgraph/util/colorama/winterm.py @@ -0,0 +1,120 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +from . import win32 + + +# from wincon.h +class WinColor(object): + BLACK = 0 + BLUE = 1 + GREEN = 2 + CYAN = 3 + RED = 4 + MAGENTA = 5 + YELLOW = 6 + GREY = 7 + +# from wincon.h +class WinStyle(object): + NORMAL = 0x00 # dim text, dim background + BRIGHT = 0x08 # bright text, dim background + + +class WinTerm(object): + + def __init__(self): + self._default = win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes + self.set_attrs(self._default) + self._default_fore = self._fore + self._default_back = self._back + self._default_style = self._style + + def get_attrs(self): + return self._fore + self._back * 16 + self._style + + def set_attrs(self, value): + self._fore = value & 7 + self._back = (value >> 4) & 7 + self._style = value & WinStyle.BRIGHT + + def reset_all(self, on_stderr=None): + self.set_attrs(self._default) + self.set_console(attrs=self._default) + + def fore(self, fore=None, on_stderr=False): + if fore is None: + fore = self._default_fore + self._fore = fore + self.set_console(on_stderr=on_stderr) + + def back(self, back=None, on_stderr=False): + if back is None: + back = self._default_back + self._back = back + self.set_console(on_stderr=on_stderr) + + def style(self, style=None, on_stderr=False): + if style is None: + style = self._default_style + self._style = style + self.set_console(on_stderr=on_stderr) + + def set_console(self, attrs=None, on_stderr=False): + if attrs is None: + attrs = self.get_attrs() + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + win32.SetConsoleTextAttribute(handle, attrs) + + def get_position(self, handle): + position = win32.GetConsoleScreenBufferInfo(handle).dwCursorPosition + # Because Windows coordinates are 0-based, + # and win32.SetConsoleCursorPosition expects 1-based. + position.X += 1 + position.Y += 1 + return position + + def set_cursor_position(self, position=None, on_stderr=False): + if position is None: + #I'm not currently tracking the position, so there is no default. + #position = self.get_position() + return + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + win32.SetConsoleCursorPosition(handle, position) + + def cursor_up(self, num_rows=0, on_stderr=False): + if num_rows == 0: + return + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + position = self.get_position(handle) + adjusted_position = (position.Y - num_rows, position.X) + self.set_cursor_position(adjusted_position, on_stderr) + + def erase_data(self, mode=0, on_stderr=False): + # 0 (or None) should clear from the cursor to the end of the screen. + # 1 should clear from the cursor to the beginning of the screen. + # 2 should clear the entire screen. (And maybe move cursor to (1,1)?) + # + # At the moment, I only support mode 2. From looking at the API, it + # should be possible to calculate a different number of bytes to clear, + # and to do so relative to the cursor position. + if mode[0] not in (2,): + return + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + # here's where we'll home the cursor + coord_screen = win32.COORD(0,0) + csbi = win32.GetConsoleScreenBufferInfo(handle) + # get the number of character cells in the current buffer + dw_con_size = csbi.dwSize.X * csbi.dwSize.Y + # fill the entire screen with blanks + win32.FillConsoleOutputCharacter(handle, ' ', dw_con_size, coord_screen) + # now set the buffer's attributes accordingly + win32.FillConsoleOutputAttribute(handle, self.get_attrs(), dw_con_size, coord_screen ); + # put the cursor at (0, 0) + win32.SetConsoleCursorPosition(handle, (coord_screen.X, coord_screen.Y)) diff --git a/pyqtgraph/util/cprint.py b/pyqtgraph/util/cprint.py new file mode 100644 index 00000000..e88bfd1a --- /dev/null +++ b/pyqtgraph/util/cprint.py @@ -0,0 +1,101 @@ +""" +Cross-platform color text printing + +Based on colorama (see pyqtgraph/util/colorama/README.txt) +""" +import sys, re + +from .colorama.winterm import WinTerm, WinColor, WinStyle +from .colorama.win32 import windll + +_WIN = sys.platform.startswith('win') +if windll is not None: + winterm = WinTerm() +else: + _WIN = False + +def winset(reset=False, fore=None, back=None, style=None, stderr=False): + if reset: + winterm.reset_all() + if fore is not None: + winterm.fore(fore, stderr) + if back is not None: + winterm.back(back, stderr) + if style is not None: + winterm.style(style, stderr) + +ANSI = {} +WIN = {} +for i,color in enumerate(['BLACK', 'RED', 'GREEN', 'YELLOW', 'BLUE', 'MAGENTA', 'CYAN', 'WHITE']): + globals()[color] = i + globals()['BR_' + color] = i + 8 + globals()['BACK_' + color] = i + 40 + ANSI[i] = "\033[%dm" % (30+i) + ANSI[i+8] = "\033[2;%dm" % (30+i) + ANSI[i+40] = "\033[%dm" % (40+i) + color = 'GREY' if color == 'WHITE' else color + WIN[i] = {'fore': getattr(WinColor, color), 'style': WinStyle.NORMAL} + WIN[i+8] = {'fore': getattr(WinColor, color), 'style': WinStyle.BRIGHT} + WIN[i+40] = {'back': getattr(WinColor, color)} + +RESET = -1 +ANSI[RESET] = "\033[0m" +WIN[RESET] = {'reset': True} + + +def cprint(stream, *args, **kwds): + """ + Print with color. Examples:: + + # colors are BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE + cprint('stdout', RED, 'This is in red. ', RESET, 'and this is normal\n') + + # Adding BR_ before the color manes it bright + cprint('stdout', BR_GREEN, 'This is bright green.\n', RESET) + + # Adding BACK_ changes background color + cprint('stderr', BACK_BLUE, WHITE, 'This is white-on-blue.', -1) + + # Integers 0-7 for normal, 8-15 for bright, and 40-47 for background. + # -1 to reset. + cprint('stderr', 1, 'This is in red.', -1) + + """ + if isinstance(stream, basestring): + stream = kwds.get('stream', 'stdout') + err = stream == 'stderr' + stream = getattr(sys, stream) + else: + err = kwds.get('stderr', False) + + if hasattr(stream, 'isatty') and stream.isatty(): + if _WIN: + # convert to win32 calls + for arg in args: + if isinstance(arg, basestring): + stream.write(arg) + else: + kwds = WIN[arg] + winset(stderr=err, **kwds) + else: + # convert to ANSI + for arg in args: + if isinstance(arg, basestring): + stream.write(arg) + else: + stream.write(ANSI[arg]) + else: + # ignore colors + for arg in args: + if isinstance(arg, basestring): + stream.write(arg) + +def cout(*args): + """Shorthand for cprint('stdout', ...)""" + cprint('stdout', *args) + +def cerr(*args): + """Shorthand for cprint('stderr', ...)""" + cprint('stderr', *args) + + diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index 54712f43..cb9a7052 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -3,6 +3,7 @@ if not USE_PYSIDE: import sip from .. import multiprocess as mp from .GraphicsView import GraphicsView +from .. import CONFIG_OPTIONS import numpy as np import mmap, tempfile, ctypes, atexit, sys, random @@ -35,7 +36,7 @@ class RemoteGraphicsView(QtGui.QWidget): self._proc = mp.QtProcess(**kwds) self.pg = self._proc._import('pyqtgraph') - self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS) + self.pg.setConfigOptions(**CONFIG_OPTIONS) rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') self._view = rpgRemote.Renderer(*args, **remoteKwds) self._view._setProxyOptions(deferGetattr=True) From 5d709251d1255fdcf77480281f2837ba922714e1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Mar 2014 17:57:44 -0400 Subject: [PATCH 158/268] clean up debug messages --- pyqtgraph/multiprocess/remoteproxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 8287d0e8..f2896c8b 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -83,7 +83,7 @@ class RemoteEventHandler(object): def debugMsg(self, msg): if not self.debug: return - cprint.cout(self.debug, "%d [%d] %s\n" % (self.debug, os.getpid(), str(msg)), -1) + cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)), -1) def getProxyOption(self, opt): return self.proxyOptions[opt] From 79cfd3601e6f6c6e2a59f1905c2bcd845a538ef0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Mar 2014 18:00:29 -0400 Subject: [PATCH 159/268] Rename Mutex module --- pyqtgraph/debug.py | 2 +- pyqtgraph/util/{Mutex.py => mutex.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pyqtgraph/util/{Mutex.py => mutex.py} (100%) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 4756423c..b59ee1a9 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -11,7 +11,7 @@ import sys, traceback, time, gc, re, types, weakref, inspect, os, cProfile, thre from . import ptime from numpy import ndarray from .Qt import QtCore, QtGui -from .util.Mutex import Mutex +from .util.mutex import Mutex from .util import cprint __ftraceDepth = 0 diff --git a/pyqtgraph/util/Mutex.py b/pyqtgraph/util/mutex.py similarity index 100% rename from pyqtgraph/util/Mutex.py rename to pyqtgraph/util/mutex.py From 5f7e4dc6442c747b8ef3091a2546916bd0d0c81d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Mar 2014 18:49:34 -0400 Subject: [PATCH 160/268] Removed extra image window from VideoSpeedTest --- examples/VideoSpeedTest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index b9acd698..6fce8a86 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -98,7 +98,6 @@ def mkData(): else: data = np.random.normal(size=(frames,width,height), loc=loc, scale=scale) data = pg.gaussianFilter(data, (0, 6, 6)) - pg.image(data) if dtype[0] != 'float': data = np.clip(data, 0, mx) data = data.astype(dt) From 22ecd3cc414ab6e42c6f750bdd592e616fed7072 Mon Sep 17 00:00:00 2001 From: JosefNevrly Date: Sun, 16 Mar 2014 10:20:42 +0100 Subject: [PATCH 161/268] PlotItem.showValues fixed (was not implemented before). --- pyqtgraph/graphicsItems/AxisItem.py | 166 ++++++++++++++-------------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 001ee2cf..5ebd8a50 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -775,92 +775,92 @@ class AxisItem(GraphicsWidget): textSize2 = 0 textRects = [] textSpecs = [] ## list of draw - textSize2 = 0 - for i in range(len(tickLevels)): - ## Get the list of strings to display for this level - if tickStrings is None: - spacing, values = tickLevels[i] - strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing) - else: - strings = tickStrings[i] - - if len(strings) == 0: - continue - - ## ignore strings belonging to ticks that were previously ignored - for j in range(len(strings)): - if tickPositions[i][j] is None: - strings[j] = None - - ## Measure density of text; decide whether to draw this level - rects = [] - for s in strings: - if s is None: - rects.append(None) + if self.showValues: + for i in range(len(tickLevels)): + ## Get the list of strings to display for this level + if tickStrings is None: + spacing, values = tickLevels[i] + strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing) else: - 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) + strings = tickStrings[i] - rects.append(br) - textRects.append(rects[-1]) - - if i > 0: ## always draw top level - ## measure all text, make sure there's enough room - if axis == 0: - textSize = np.sum([r.height() for r in textRects]) - textSize2 = np.max([r.width() for r in textRects]) if textRects else 0 - else: - textSize = np.sum([r.width() for r in textRects]) - textSize2 = np.max([r.height() for r in textRects]) if textRects else 0 - - ## If the strings are too crowded, stop drawing text now. - ## We use three different crowding limits based on the number - ## of texts drawn so far. - textFillRatio = float(textSize) / lengthInPixels - finished = False - for nTexts, limit in self.style['textFillLimits']: - if len(textSpecs) >= nTexts and textFillRatio >= limit: - finished = True - break - if finished: - break - - #spacing, values = tickLevels[best] - #strings = self.tickStrings(values, self.scale, spacing) - for j in range(len(strings)): - vstr = strings[j] - if vstr is None: ## this tick was ignored because it is out of bounds + if len(strings) == 0: continue - vstr = asUnicode(vstr) - x = tickPositions[i][j] - #textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) - textRect = rects[j] - height = textRect.height() - width = textRect.width() - #self.textHeight = height - offset = max(0,self.tickLength) + textOffset - if self.orientation == 'left': - textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height) - elif self.orientation == 'right': - textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop+offset, x-(height/2), width, height) - elif self.orientation == 'top': - textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom - rect = QtCore.QRectF(x-width/2., tickStop-offset-height, width, height) - elif self.orientation == 'bottom': - textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop - rect = QtCore.QRectF(x-width/2., tickStop+offset, width, height) - - #p.setPen(self.pen()) - #p.drawText(rect, textFlags, vstr) - textSpecs.append((rect, textFlags, vstr)) - profiler('compute text') - - ## update max text size if needed. - self._updateMaxTextSize(textSize2) + + ## ignore strings belonging to ticks that were previously ignored + for j in range(len(strings)): + if tickPositions[i][j] is None: + strings[j] = None + + ## Measure density of text; decide whether to draw this level + rects = [] + for s in strings: + if s is None: + rects.append(None) + else: + 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) + + rects.append(br) + textRects.append(rects[-1]) + + if i > 0: ## always draw top level + ## measure all text, make sure there's enough room + if axis == 0: + textSize = np.sum([r.height() for r in textRects]) + textSize2 = np.max([r.width() for r in textRects]) if textRects else 0 + else: + textSize = np.sum([r.width() for r in textRects]) + textSize2 = np.max([r.height() for r in textRects]) if textRects else 0 + + ## If the strings are too crowded, stop drawing text now. + ## We use three different crowding limits based on the number + ## of texts drawn so far. + textFillRatio = float(textSize) / lengthInPixels + finished = False + for nTexts, limit in self.style['textFillLimits']: + if len(textSpecs) >= nTexts and textFillRatio >= limit: + finished = True + break + if finished: + break + + #spacing, values = tickLevels[best] + #strings = self.tickStrings(values, self.scale, spacing) + for j in range(len(strings)): + vstr = strings[j] + if vstr is None: ## this tick was ignored because it is out of bounds + continue + vstr = asUnicode(vstr) + x = tickPositions[i][j] + #textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) + textRect = rects[j] + height = textRect.height() + width = textRect.width() + #self.textHeight = height + offset = max(0,self.tickLength) + textOffset + if self.orientation == 'left': + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height) + elif self.orientation == 'right': + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop+offset, x-(height/2), width, height) + elif self.orientation == 'top': + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom + rect = QtCore.QRectF(x-width/2., tickStop-offset-height, width, height) + elif self.orientation == 'bottom': + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop + rect = QtCore.QRectF(x-width/2., tickStop+offset, width, height) + + #p.setPen(self.pen()) + #p.drawText(rect, textFlags, vstr) + textSpecs.append((rect, textFlags, vstr)) + profiler('compute text') + + ## update max text size if needed. + self._updateMaxTextSize(textSize2) return (axisSpec, tickSpecs, textSpecs) From aa4b790dd290d5c5002e092a999c12251ed41ab8 Mon Sep 17 00:00:00 2001 From: JosefNevrly Date: Sun, 16 Mar 2014 10:38:08 +0100 Subject: [PATCH 162/268] Fixes calculation of axis width/height in case values are not shown. --- pyqtgraph/graphicsItems/AxisItem.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 5ebd8a50..703662e4 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -242,10 +242,12 @@ class AxisItem(GraphicsWidget): """Set the height of this axis reserved for ticks and tick labels. The height of the axis label is automatically added.""" if h is None: - if self.style['autoExpandTextSpace'] is True: - h = self.textHeight - else: - h = self.style['tickTextHeight'] + h = 0 + if self.showValues: + if self.style['autoExpandTextSpace'] is True: + h = self.textHeight + else: + h = self.style['tickTextHeight'] h += max(0, self.tickLength) + self.style['tickTextOffset'][1] if self.label.isVisible(): h += self.label.boundingRect().height() * 0.8 @@ -258,10 +260,12 @@ class AxisItem(GraphicsWidget): """Set the width of this axis reserved for ticks and tick labels. The width of the axis label is automatically added.""" if w is None: - if self.style['autoExpandTextSpace'] is True: - w = self.textWidth - else: - w = self.style['tickTextWidth'] + w = 0 + if self.showValues: + if self.style['autoExpandTextSpace'] is True: + w = self.textWidth + else: + w = self.style['tickTextWidth'] w += max(0, self.tickLength) + self.style['tickTextOffset'][0] if self.label.isVisible(): w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate @@ -859,8 +863,8 @@ class AxisItem(GraphicsWidget): textSpecs.append((rect, textFlags, vstr)) profiler('compute text') - ## update max text size if needed. - self._updateMaxTextSize(textSize2) + ## update max text size if needed. + self._updateMaxTextSize(textSize2) return (axisSpec, tickSpecs, textSpecs) From 70724a44b37a79c072a03963749464dc97331dda Mon Sep 17 00:00:00 2001 From: JosefNevrly Date: Sun, 16 Mar 2014 15:13:43 +0100 Subject: [PATCH 163/268] Added check for zero ViewBox width when calculating automatic downsampling ratio. (Prevents zero-division when downsampling is set before Plot is properly created and drawn within a container). --- pyqtgraph/graphicsItems/PlotDataItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index d2d18fd9..14a39dba 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -532,7 +532,8 @@ class PlotDataItem(GraphicsObject): x0 = (range.left()-x[0]) / dx x1 = (range.right()-x[0]) / dx width = self.getViewBox().width() - ds = int(max(1, int(0.2 * (x1-x0) / width))) + if width != 0.0: + ds = int(max(1, int(0.2 * (x1-x0) / width))) ## downsampling is expensive; delay until after clipping. if self.opts['clipToView']: From fd6cc955e218d1666da9da19eb1979370c6693fd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 16 Mar 2014 13:45:26 -0400 Subject: [PATCH 164/268] Fixed GLGridItem.setSize, added setSpacing --- CHANGELOG | 1 + pyqtgraph/opengl/items/GLGridItem.py | 38 +++++++++++++++++++++------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 11db18ac..b9e56d51 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -76,6 +76,7 @@ pyqtgraph-0.9.9 [unreleased] - Allow images with NaN in ImageView - MeshData can generate edges from face-indexed vertexes - Fixed multiprocess deadlocks on windows + - Fixed GLGridItem.setSize pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index 2c4642c8..a8d1fb7a 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -1,3 +1,5 @@ +import numpy as np + from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem from ... import QtGui @@ -16,8 +18,9 @@ class GLGridItem(GLGraphicsItem): self.setGLOptions(glOptions) self.antialias = antialias if size is None: - size = QtGui.QVector3D(1,1,1) + size = QtGui.QVector3D(20,20,1) self.setSize(size=size) + self.setSpacing(1, 1, 1) def setSize(self, x=None, y=None, z=None, size=None): """ @@ -33,8 +36,22 @@ class GLGridItem(GLGraphicsItem): def size(self): return self.__size[:] - - + + def setSpacing(self, x=None, y=None, z=None, spacing=None): + """ + Set the spacing between grid lines. + Arguments can be x,y,z or spacing=QVector3D(). + """ + if spacing is not None: + x = spacing.x() + y = spacing.y() + z = spacing.z() + self.__spacing = [x,y,z] + self.update() + + def spacing(self): + return self.__spacing[:] + def paint(self): self.setupGLState() @@ -47,12 +64,15 @@ class GLGridItem(GLGraphicsItem): glBegin( GL_LINES ) x,y,z = self.size() + xs,ys,zs = self.spacing() + xvals = np.arange(-x/2., x/2. + xs*0.001, xs) + yvals = np.arange(-y/2., y/2. + ys*0.001, ys) glColor4f(1, 1, 1, .3) - for x in range(-10, 11): - glVertex3f(x, -10, 0) - glVertex3f(x, 10, 0) - for y in range(-10, 11): - glVertex3f(-10, y, 0) - glVertex3f( 10, y, 0) + for x in xvals: + glVertex3f(x, yvals[0], 0) + glVertex3f(x, yvals[-1], 0) + for y in yvals: + glVertex3f(xvals[0], y, 0) + glVertex3f(xvals[-1], y, 0) glEnd() From 5b47eff2f705d3310e4376e6481b7cb36935d097 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 24 Mar 2014 08:27:52 -0400 Subject: [PATCH 165/268] Disable weave by default. --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 5f42e64f..2bad4982 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -52,7 +52,7 @@ CONFIG_OPTIONS = { '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 + 'useWeave': False, ## Use weave to speed up some operations, if it is available 'weaveDebug': False, ## Print full error message if weave compile fails 'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide 'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code) From bc57d5a6af12b6dd32249a4be58ef7341d14f675 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 24 Mar 2014 11:11:41 -0400 Subject: [PATCH 166/268] Minor corrections to debug.Profiler --- pyqtgraph/debug.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index b59ee1a9..7836ba90 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -430,7 +430,7 @@ class Profiler(object): try: caller_object_type = type(caller_frame.f_locals["self"]) except KeyError: # we are in a regular function - qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1] + qualifier = caller_frame.f_globals["__name__"].split(".", 1)[-1] else: # we are in a method qualifier = caller_object_type.__name__ func_qualname = qualifier + "." + caller_frame.f_code.co_name @@ -469,6 +469,7 @@ class Profiler(object): if self._delayed: self._msgs.append((msg, args)) else: + self.flush() print(msg % args) def __del__(self): @@ -485,10 +486,13 @@ class Profiler(object): self._newMsg("< Exiting %s, total time: %0.4f ms", self._name, (ptime.time() - self._firstTime) * 1000) type(self)._depth -= 1 - if self._depth < 1 and self._msgs: + if self._depth < 1: + self.flush() + + def flush(self): + if self._msgs: print("\n".join([m[0]%m[1] for m in self._msgs])) type(self)._msgs = [] - def profile(code, name='profile_run', sort='cumulative', num=30): From ed87cffd1f48f4fb5c54812c43d778913dcac25c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 24 Mar 2014 12:48:30 -0400 Subject: [PATCH 167/268] corrected SVG test, moved to its proper home --- pyqtgraph/exporters/tests/test_svg.py | 67 +++++++++++++++++++++++++ tests/__init__.py | 0 tests/svg.py | 70 --------------------------- 3 files changed, 67 insertions(+), 70 deletions(-) create mode 100644 pyqtgraph/exporters/tests/test_svg.py delete mode 100644 tests/__init__.py delete mode 100644 tests/svg.py diff --git a/pyqtgraph/exporters/tests/test_svg.py b/pyqtgraph/exporters/tests/test_svg.py new file mode 100644 index 00000000..871f43c2 --- /dev/null +++ b/pyqtgraph/exporters/tests/test_svg.py @@ -0,0 +1,67 @@ +""" +SVG export test +""" +import pyqtgraph as pg +import pyqtgraph.exporters +app = pg.mkQApp() + +def test_plotscene(): + pg.setConfigOption('foreground', (0,0,0)) + w = pg.GraphicsWindow() + w.show() + p1 = w.addPlot() + p2 = w.addPlot() + p1.plot([1,3,2,3,1,6,9,8,4,2,3,5,3], pen={'color':'k'}) + p1.setXRange(0,5) + p2.plot([1,5,2,3,4,6,1,2,4,2,3,5,3], pen={'color':'k', 'cosmetic':False, 'width': 0.3}) + app.processEvents() + app.processEvents() + + ex = pg.exporters.SVGExporter(w.scene()) + ex.export(fileName='test.svg') + + +def test_simple(): + scene = pg.QtGui.QGraphicsScene() + #rect = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100) + #scene.addItem(rect) + #rect.setPos(20,20) + #rect.translate(50, 50) + #rect.rotate(30) + #rect.scale(0.5, 0.5) + + #rect1 = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100) + #rect1.setParentItem(rect) + #rect1.setFlag(rect1.ItemIgnoresTransformations) + #rect1.setPos(20, 20) + #rect1.scale(2,2) + + #el1 = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 100) + #el1.setParentItem(rect1) + ##grp = pg.ItemGroup() + #grp.setParentItem(rect) + #grp.translate(200,0) + ##grp.rotate(30) + + #rect2 = pg.QtGui.QGraphicsRectItem(0, 0, 100, 25) + #rect2.setFlag(rect2.ItemClipsChildrenToShape) + #rect2.setParentItem(grp) + #rect2.setPos(0,25) + #rect2.rotate(30) + #el = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 50) + #el.translate(10,-5) + #el.scale(0.5,2) + #el.setParentItem(rect2) + + grp2 = pg.ItemGroup() + scene.addItem(grp2) + grp2.scale(100,100) + + rect3 = pg.QtGui.QGraphicsRectItem(0,0,2,2) + rect3.setPen(pg.mkPen(width=1, cosmetic=False)) + grp2.addItem(rect3) + + ex = pg.exporters.SVGExporter(scene) + ex.export(fileName='test.svg') + + diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/svg.py b/tests/svg.py deleted file mode 100644 index 7c26833e..00000000 --- a/tests/svg.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -SVG export test -""" -import test -import pyqtgraph as pg -app = pg.mkQApp() - -class SVGTest(test.TestCase): - #def test_plotscene(self): - #pg.setConfigOption('foreground', (0,0,0)) - #w = pg.GraphicsWindow() - #w.show() - #p1 = w.addPlot() - #p2 = w.addPlot() - #p1.plot([1,3,2,3,1,6,9,8,4,2,3,5,3], pen={'color':'k'}) - #p1.setXRange(0,5) - #p2.plot([1,5,2,3,4,6,1,2,4,2,3,5,3], pen={'color':'k', 'cosmetic':False, 'width': 0.3}) - #app.processEvents() - #app.processEvents() - - #ex = pg.exporters.SVGExporter.SVGExporter(w.scene()) - #ex.export(fileName='test.svg') - - - def test_simple(self): - scene = pg.QtGui.QGraphicsScene() - #rect = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100) - #scene.addItem(rect) - #rect.setPos(20,20) - #rect.translate(50, 50) - #rect.rotate(30) - #rect.scale(0.5, 0.5) - - #rect1 = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100) - #rect1.setParentItem(rect) - #rect1.setFlag(rect1.ItemIgnoresTransformations) - #rect1.setPos(20, 20) - #rect1.scale(2,2) - - #el1 = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 100) - #el1.setParentItem(rect1) - ##grp = pg.ItemGroup() - #grp.setParentItem(rect) - #grp.translate(200,0) - ##grp.rotate(30) - - #rect2 = pg.QtGui.QGraphicsRectItem(0, 0, 100, 25) - #rect2.setFlag(rect2.ItemClipsChildrenToShape) - #rect2.setParentItem(grp) - #rect2.setPos(0,25) - #rect2.rotate(30) - #el = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 50) - #el.translate(10,-5) - #el.scale(0.5,2) - #el.setParentItem(rect2) - - grp2 = pg.ItemGroup() - scene.addItem(grp2) - grp2.scale(100,100) - - rect3 = pg.QtGui.QGraphicsRectItem(0,0,2,2) - rect3.setPen(pg.mkPen(width=1, cosmetic=False)) - grp2.addItem(rect3) - - ex = pg.exporters.SVGExporter.SVGExporter(scene) - ex.export(fileName='test.svg') - - -if __name__ == '__main__': - test.unittest.main() \ No newline at end of file From 25e7d12f090301d29c16c6065aece9d8a72ae5a2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 24 Mar 2014 15:47:32 -0400 Subject: [PATCH 168/268] Cleanup / fixes: - Corrected isQObjectAlive for PyQt and older PySide - Warning messages are opt-in using pg.setConfigOptions(crashWarning=True) --- pyqtgraph/Qt.py | 21 +++++++++++++++++--- pyqtgraph/__init__.py | 7 +++++-- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 6 ++---- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 4 ++-- pyqtgraph/tests/test_qt.py | 10 ++++++++++ 5 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 pyqtgraph/tests/test_qt.py diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 62ffa2d0..2fcff32f 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -32,8 +32,22 @@ else: if USE_PYSIDE: from PySide import QtGui, QtCore, QtOpenGL, QtSvg import PySide - from PySide import shiboken - isQObjectAlive = shiboken.isValid + try: + from PySide import shiboken + isQObjectAlive = shiboken.isValid + except ImportError: + def isQObjectAlive(obj): + try: + if hasattr(obj, 'parent'): + obj.parent() + elif hasattr(obj, 'parentItem'): + obj.parentItem() + else: + raise Exception("Cannot determine whether Qt object %s is still alive." % obj) + except RuntimeError: + return False + else: + return True VERSION_INFO = 'PySide ' + PySide.__version__ @@ -82,7 +96,8 @@ else: import sip - isQObjectAlive = sip.isdeleted + def isQObjectAlive(obj): + return not sip.isdeleted(obj) loadUiType = uic.loadUiType QtCore.Signal = QtCore.pyqtSignal diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index cd78cfa8..64c642c0 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -56,6 +56,7 @@ CONFIG_OPTIONS = { 'weaveDebug': False, ## Print full error message if weave compile fails 'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide 'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code) + 'crashWarning': False, # If True, print warnings about situations that may result in a crash } @@ -286,8 +287,10 @@ def cleanup(): for o in gc.get_objects(): try: if isinstance(o, QtGui.QGraphicsItem) and isQObjectAlive(o) and o.scene() is None: - sys.stderr.write( - 'Error: graphics item without scene. Make sure ViewBox.close() and GraphicsView.close() are properly called before app shutdown (%s)\n' % (o,)) + if getConfigOption('crashWarning'): + sys.stderr.write('Error: graphics item without scene. ' + 'Make sure ViewBox.close() and GraphicsView.close() ' + 'are properly called before app shutdown (%s)\n' % (o,)) s.addItem(o) except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index c0b9adab..847ff3ac 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -295,22 +295,20 @@ class PlotItem(GraphicsWidget): #Important: don't use a settattr(m, getattr(self.vb, m)) as we'd be leaving the viebox alive #because we had a reference to an instance method (creating wrapper methods at runtime instead). - frame = sys._getframe() for m in [ 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY', 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. - def _create_method(name): # @NoSelf + def _create_method(name): def method(self, *args, **kwargs): return getattr(self.vb, name)(*args, **kwargs) method.__name__ = name return method - frame.f_locals[m] = _create_method(m) + locals()[m] = _create_method(m) del _create_method - del frame def setLogMode(self, x=None, y=None): """ diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index ef5b4319..b27e6e4b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1655,8 +1655,8 @@ class ViewBox(GraphicsWidget): ## called when the application is about to exit. ## this disables all callbacks, which might otherwise generate errors if invoked during exit. for k in ViewBox.AllViews: - if isQObjectAlive(k): - sys.stderr.write('ViewBox should be closed before application exit!') + if isQObjectAlive(k) and getConfigOption('crashWarning'): + sys.stderr.write('Warning: ViewBox should be closed before application exit.\n') try: k.destroyed.disconnect() diff --git a/pyqtgraph/tests/test_qt.py b/pyqtgraph/tests/test_qt.py new file mode 100644 index 00000000..cef54777 --- /dev/null +++ b/pyqtgraph/tests/test_qt.py @@ -0,0 +1,10 @@ +import pyqtgraph as pg +import gc + +def test_isQObjectAlive(): + o1 = pg.QtCore.QObject() + o2 = pg.QtCore.QObject() + o2.setParent(o1) + del o1 + gc.collect() + assert not pg.Qt.isQObjectAlive(o2) From 00c9c1e2a73b02dae944c86cc901487cd4a7aef4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 25 Mar 2014 09:21:19 -0400 Subject: [PATCH 169/268] Fix ROI.sigRemoveClicked to avoid repeated signal emission Update ROI example to demonstrate removal --- examples/ROIExamples.py | 8 +++++++- pyqtgraph/graphicsItems/ROI.py | 4 +--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py index 56b15bcf..55c671ad 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -132,7 +132,7 @@ label4 = w4.addLabel(text, row=0, col=0) v4 = w4.addViewBox(row=1, col=0, lockAspect=True) g = pg.GridItem() v4.addItem(g) -r4 = pg.ROI([0,0], [100,100]) +r4 = pg.ROI([0,0], [100,100], removable=True) r4.addRotateHandle([1,0], [0.5, 0.5]) r4.addRotateHandle([0,1], [0.5, 0.5]) img4 = pg.ImageItem(arr) @@ -142,6 +142,12 @@ img4.setParentItem(r4) v4.disableAutoRange('xy') v4.autoRange() +# Provide a callback to remove the ROI (and its children) when +# "remove" is selected from the context menu. +def remove(): + v4.removeItem(r4) +r4.sigRemoveRequested.connect(remove) + diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 5494f3e3..179dafdc 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -665,9 +665,7 @@ class ROI(GraphicsObject): def removeClicked(self): ## Send remove event only after we have exited the menu event handler - self.removeTimer = QtCore.QTimer() - self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self)) - self.removeTimer.start(0) + QtCore.QTimer.singleShot(0, lambda: self.sigRemoveRequested.emit(self)) def mouseDragEvent(self, ev): if ev.isStart(): From 2ce6196ac06e629bf1468c38bb23465e3aec762b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 25 Mar 2014 13:15:29 -0400 Subject: [PATCH 170/268] Fixed Parameter.sigValueChanging --- CHANGELOG | 1 + examples/parametertree.py | 11 +++++++++++ pyqtgraph/parametertree/parameterTypes.py | 6 ++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b9e56d51..64260ccb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -77,6 +77,7 @@ pyqtgraph-0.9.9 [unreleased] - MeshData can generate edges from face-indexed vertexes - Fixed multiprocess deadlocks on windows - Fixed GLGridItem.setSize + - Fixed parametertree.Parameter.sigValueChanging pyqtgraph-0.9.8 2013-11-24 diff --git a/examples/parametertree.py b/examples/parametertree.py index c0eb50db..b8638e02 100644 --- a/examples/parametertree.py +++ b/examples/parametertree.py @@ -123,6 +123,17 @@ def change(param, changes): p.sigTreeStateChanged.connect(change) +def valueChanging(param, value): + print "Value changing (not finalized):", param, value + +# Too lazy for recursion: +for child in p.children(): + child.sigValueChanging.connect(valueChanging) + for ch2 in child.children(): + ch2.sigValueChanging.connect(valueChanging) + + + def save(): global state state = p.saveState() diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 92eca90f..1f3eb692 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -208,12 +208,14 @@ class WidgetParameterItem(ParameterItem): val = self.widget.value() newVal = self.param.setValue(val) - def widgetValueChanging(self): + def widgetValueChanging(self, *args): """ Called when the widget's value is changing, but not finalized. For example: editing text before pressing enter or changing focus. """ - pass + # This is a bit sketchy: assume the last argument of each signal is + # the value.. + self.param.sigValueChanging.emit(self.param, args[-1]) def selected(self, sel): """Called when this item has been selected (sel=True) OR deselected (sel=False)""" From 604ea49477d74de1e4a2a68c28cbe90478752e4b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 25 Mar 2014 19:17:11 -0400 Subject: [PATCH 171/268] Fix for colorama bug: https://code.google.com/p/colorama/issues/detail?id=47 --- pyqtgraph/util/colorama/win32.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/util/colorama/win32.py b/pyqtgraph/util/colorama/win32.py index f4024f95..c86ce180 100644 --- a/pyqtgraph/util/colorama/win32.py +++ b/pyqtgraph/util/colorama/win32.py @@ -12,7 +12,7 @@ except ImportError: SetConsoleTextAttribute = lambda *_: None else: from ctypes import ( - byref, Structure, c_char, c_short, c_uint32, c_ushort, POINTER + byref, Structure, c_char, c_short, c_int, c_uint32, c_ushort, c_void_p, POINTER ) class CONSOLE_SCREEN_BUFFER_INFO(Structure): @@ -42,7 +42,8 @@ else: _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo _GetConsoleScreenBufferInfo.argtypes = [ wintypes.HANDLE, - POINTER(CONSOLE_SCREEN_BUFFER_INFO), + c_void_p, + #POINTER(CONSOLE_SCREEN_BUFFER_INFO), ] _GetConsoleScreenBufferInfo.restype = wintypes.BOOL @@ -56,7 +57,8 @@ else: _SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition _SetConsoleCursorPosition.argtypes = [ wintypes.HANDLE, - wintypes._COORD, + c_int, + #wintypes._COORD, ] _SetConsoleCursorPosition.restype = wintypes.BOOL @@ -75,7 +77,8 @@ else: wintypes.HANDLE, wintypes.WORD, wintypes.DWORD, - wintypes._COORD, + c_int, + #wintypes._COORD, POINTER(wintypes.DWORD), ] _FillConsoleOutputAttribute.restype = wintypes.BOOL From 26c57a0f14cf4ba3b2f2600165c9749988206107 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 29 Mar 2014 11:31:23 -0400 Subject: [PATCH 172/268] Style changes and a minor bug fix --- pyqtgraph/graphicsItems/AxisItem.py | 198 ++++++++++---------- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 +- 2 files changed, 102 insertions(+), 98 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 703662e4..e1cf1c4c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -33,7 +33,6 @@ class AxisItem(GraphicsWidget): GraphicsWidget.__init__(self, parent) self.label = QtGui.QGraphicsTextItem(self) - self.showValues = showValues self.picture = None self.orientation = orientation if orientation not in ['left', 'right', 'top', 'bottom']: @@ -53,7 +52,8 @@ class AxisItem(GraphicsWidget): (2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis (4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis (6, 0.2), ## If we already have 6 ticks with text, fill no more than 20% of the axis - ] + ], + 'showValues': showValues, } self.textWidth = 30 ## Keeps track of maximum width / height of tick text @@ -242,31 +242,31 @@ class AxisItem(GraphicsWidget): """Set the height of this axis reserved for ticks and tick labels. The height of the axis label is automatically added.""" if h is None: - h = 0 - if self.showValues: - if self.style['autoExpandTextSpace'] is True: - h = self.textHeight - else: - h = self.style['tickTextHeight'] - h += max(0, self.tickLength) + self.style['tickTextOffset'][1] + if not self.style['showValues']: + h = 0 + elif self.style['autoExpandTextSpace'] is True: + h = self.textHeight + else: + h = self.style['tickTextHeight'] + textOffset = self.style['tickTextOffset'][1] if self.style['showValues'] else 0 + h += max(0, self.tickLength) + textOffset if self.label.isVisible(): h += self.label.boundingRect().height() * 0.8 self.setMaximumHeight(h) self.setMinimumHeight(h) self.picture = None - def setWidth(self, w=None): """Set the width of this axis reserved for ticks and tick labels. The width of the axis label is automatically added.""" if w is None: - w = 0 - if self.showValues: - if self.style['autoExpandTextSpace'] is True: - w = self.textWidth - else: - w = self.style['tickTextWidth'] - w += max(0, self.tickLength) + self.style['tickTextOffset'][0] + if not self.style['showValues']: + w = 0 + elif self.style['autoExpandTextSpace'] is True: + w = self.textWidth + else: + w = self.style['tickTextWidth'] + textOffset = self.style['tickTextOffset'][0] if self.style['showValues'] else 0 if self.label.isVisible(): w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate self.setMaximumWidth(w) @@ -779,89 +779,93 @@ class AxisItem(GraphicsWidget): textSize2 = 0 textRects = [] textSpecs = [] ## list of draw - if self.showValues: - for i in range(len(tickLevels)): - ## Get the list of strings to display for this level - if tickStrings is None: - spacing, values = tickLevels[i] - strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing) + + # If values are hidden, return early + if not self.style['showValues']: + return (axisSpec, tickSpecs, textSpecs) + + for i in range(len(tickLevels)): + ## Get the list of strings to display for this level + if tickStrings is None: + spacing, values = tickLevels[i] + strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing) + else: + strings = tickStrings[i] + + if len(strings) == 0: + continue + + ## ignore strings belonging to ticks that were previously ignored + for j in range(len(strings)): + if tickPositions[i][j] is None: + strings[j] = None + + ## Measure density of text; decide whether to draw this level + rects = [] + for s in strings: + if s is None: + rects.append(None) else: - strings = tickStrings[i] + 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) - if len(strings) == 0: - continue - - ## ignore strings belonging to ticks that were previously ignored - for j in range(len(strings)): - if tickPositions[i][j] is None: - strings[j] = None - - ## Measure density of text; decide whether to draw this level - rects = [] - for s in strings: - if s is None: - rects.append(None) - else: - 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) - - rects.append(br) - textRects.append(rects[-1]) - - if i > 0: ## always draw top level - ## measure all text, make sure there's enough room - if axis == 0: - textSize = np.sum([r.height() for r in textRects]) - textSize2 = np.max([r.width() for r in textRects]) if textRects else 0 - else: - textSize = np.sum([r.width() for r in textRects]) - textSize2 = np.max([r.height() for r in textRects]) if textRects else 0 - - ## If the strings are too crowded, stop drawing text now. - ## We use three different crowding limits based on the number - ## of texts drawn so far. - textFillRatio = float(textSize) / lengthInPixels - finished = False - for nTexts, limit in self.style['textFillLimits']: - if len(textSpecs) >= nTexts and textFillRatio >= limit: - finished = True - break - if finished: + rects.append(br) + textRects.append(rects[-1]) + + if i > 0: ## always draw top level + ## measure all text, make sure there's enough room + if axis == 0: + textSize = np.sum([r.height() for r in textRects]) + textSize2 = np.max([r.width() for r in textRects]) if textRects else 0 + else: + textSize = np.sum([r.width() for r in textRects]) + textSize2 = np.max([r.height() for r in textRects]) if textRects else 0 + + ## If the strings are too crowded, stop drawing text now. + ## We use three different crowding limits based on the number + ## of texts drawn so far. + textFillRatio = float(textSize) / lengthInPixels + finished = False + for nTexts, limit in self.style['textFillLimits']: + if len(textSpecs) >= nTexts and textFillRatio >= limit: + finished = True break - - #spacing, values = tickLevels[best] - #strings = self.tickStrings(values, self.scale, spacing) - for j in range(len(strings)): - vstr = strings[j] - if vstr is None: ## this tick was ignored because it is out of bounds - continue - vstr = asUnicode(vstr) - x = tickPositions[i][j] - #textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) - textRect = rects[j] - height = textRect.height() - width = textRect.width() - #self.textHeight = height - offset = max(0,self.tickLength) + textOffset - if self.orientation == 'left': - textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height) - elif self.orientation == 'right': - textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop+offset, x-(height/2), width, height) - elif self.orientation == 'top': - textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom - rect = QtCore.QRectF(x-width/2., tickStop-offset-height, width, height) - elif self.orientation == 'bottom': - textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop - rect = QtCore.QRectF(x-width/2., tickStop+offset, width, height) - - #p.setPen(self.pen()) - #p.drawText(rect, textFlags, vstr) - textSpecs.append((rect, textFlags, vstr)) - profiler('compute text') + if finished: + break + + #spacing, values = tickLevels[best] + #strings = self.tickStrings(values, self.scale, spacing) + for j in range(len(strings)): + vstr = strings[j] + if vstr is None: ## this tick was ignored because it is out of bounds + continue + vstr = asUnicode(vstr) + x = tickPositions[i][j] + #textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) + textRect = rects[j] + height = textRect.height() + width = textRect.width() + #self.textHeight = height + offset = max(0,self.tickLength) + textOffset + if self.orientation == 'left': + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height) + elif self.orientation == 'right': + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop+offset, x-(height/2), width, height) + elif self.orientation == 'top': + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom + rect = QtCore.QRectF(x-width/2., tickStop-offset-height, width, height) + elif self.orientation == 'bottom': + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop + rect = QtCore.QRectF(x-width/2., tickStop+offset, width, height) + + #p.setPen(self.pen()) + #p.drawText(rect, textFlags, vstr) + textSpecs.append((rect, textFlags, vstr)) + profiler('compute text') ## update max text size if needed. self._updateMaxTextSize(textSize2) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 8474202c..71577422 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -58,7 +58,7 @@ class HistogramLUTItem(GraphicsWidget): self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) self.region.setZValue(1000) self.vb.addItem(self.region) - self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, showValues=False) + self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10) self.layout.addItem(self.axis, 0, 0) self.layout.addItem(self.vb, 0, 1) self.layout.addItem(self.gradient, 0, 2) From 77e02eded7875b3955927a1c4668abd94ef0b695 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 29 Mar 2014 11:34:05 -0400 Subject: [PATCH 173/268] py3 fix for parametertree example --- examples/parametertree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/parametertree.py b/examples/parametertree.py index b8638e02..6e8e0dbd 100644 --- a/examples/parametertree.py +++ b/examples/parametertree.py @@ -124,7 +124,7 @@ p.sigTreeStateChanged.connect(change) def valueChanging(param, value): - print "Value changing (not finalized):", param, value + print("Value changing (not finalized):", param, value) # Too lazy for recursion: for child in p.children(): From 20054c1807ef7ca7093916ff5959baa560492474 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 29 Mar 2014 16:03:43 -0400 Subject: [PATCH 174/268] Corrected how-to-use documentation on using git subtrees --- doc/source/how_to_use.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/how_to_use.rst b/doc/source/how_to_use.rst index c8b5b22b..3b1f48d0 100644 --- a/doc/source/how_to_use.rst +++ b/doc/source/how_to_use.rst @@ -115,9 +115,9 @@ For projects that already use git for code control, it is also possible to inclu my_project$ git remote add pyqtgraph-core https://github.com/pyqtgraph/pyqtgraph-core.git my_project$ git fetch pyqtgraph-core - my_project$ git merge -s ours --no-commit pyqtgraph-core/develop + my_project$ git merge -s ours --no-commit pyqtgraph-core/core my_project$ mkdir pyqtgraph - my_project$ git read-tree -u --prefix=pyqtgraph/ pyqtgraph-core/develop + my_project$ git read-tree -u --prefix=pyqtgraph/ pyqtgraph-core/core my_project$ git commit -m "Added pyqtgraph to project repository" See the ``git subtree`` documentation for more information. From 9c2a66ec86f27877fac4ccca25b4ee1b5aa58db4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 29 Mar 2014 21:47:14 -0400 Subject: [PATCH 175/268] Doc update --- doc/source/how_to_use.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/how_to_use.rst b/doc/source/how_to_use.rst index 3b1f48d0..e4424374 100644 --- a/doc/source/how_to_use.rst +++ b/doc/source/how_to_use.rst @@ -94,7 +94,7 @@ The basic approach is to clone the repository into the appropriate location in y To embed a specific version of pyqtgraph, we would clone the pyqtgraph-core repository inside the project:: - my_project$ git clone github.com/pyqtgraph/pyqtgraph-core.git + my_project$ git clone https://github.com/pyqtgraph/pyqtgraph-core.git Then adjust the import statements accordingly:: From 6b66edfd46ffd681e50eb493ee7533b486aa6c07 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 30 Mar 2014 02:51:32 -0400 Subject: [PATCH 176/268] Added Travis CI support Fixed bugs / style issues to please Lord Travis Squashed commit of the following: commit f25048a1e1e9d0be355f33fbaff6ef74845f4782 Author: Luke Campagnola Date: Sun Mar 30 02:40:47 2014 -0400 syntax commit cc8b69695a2698b75ff216dce27fabe46c79a325 Author: Luke Campagnola Date: Sun Mar 30 02:36:49 2014 -0400 add size check, diff style check commit 5d5ea065a4d4cc714bfaf7c7e7624295164cd86c Author: Luke Campagnola Date: Sun Mar 30 02:16:05 2014 -0400 travis fix commit b154c6d9971d35c2a46e575a0a884880ec14c8b1 Author: Luke Campagnola Date: Sun Mar 30 02:09:41 2014 -0400 travis, flake colored output commit 46921dcd878fdc08f05ef74d08be7953b1820a85 Author: Luke Campagnola Date: Wed Mar 26 12:37:54 2014 -0400 fix pyside+py3 bugs to satisfy CI commit 1d30f3c5c7763876ccfff54e460b83cb7422a5b4 Author: Luke Campagnola Date: Wed Mar 26 11:13:18 2014 -0400 fix py3 tests commit 426578fa4c5ec991d361c60005c32ca88c85e505 Author: Luke Campagnola Date: Wed Mar 26 07:39:19 2014 -0400 fix pytest install commit 88a13c1a7158904af7d25c5ba5bfc8e63e6cf49d Author: Luke Campagnola Date: Wed Mar 26 00:29:29 2014 -0400 qt5 updates commit 51995488ccc9f286aeced3bd49c3213819529e57 Author: Luke Campagnola Date: Wed Mar 26 00:16:04 2014 -0400 correct py.test command for py3 commit e2b02fbcbdbd95cbec5dd9307870f39337e6eb45 Author: Luke Campagnola Date: Tue Mar 25 23:50:38 2014 -0400 fix 2nd install test commit 4b3e3ee04adee3a8d1aabdfb18a198cbd0e83a06 Author: Luke Campagnola Date: Tue Mar 25 23:31:31 2014 -0400 syntax error commit 250eabdb34cac48d6f9622e6453235f0ec7f1151 Author: Luke Campagnola Date: Tue Mar 25 23:13:42 2014 -0400 look for py.test3 commit 9f9bca47c1a0a5c35be21835a674058242aa5f48 Author: Luke Campagnola Date: Tue Mar 25 22:54:19 2014 -0400 fix syntax commit 0a871c6f36ecddb7a1002b74d3b6d433e1648e8f Author: Luke Campagnola Date: Tue Mar 25 22:47:58 2014 -0400 output pip build log commit dbce58d8cd3f3a558202f8d7a149b5ec8fbfcf78 Author: Luke Campagnola Date: Tue Mar 25 22:38:55 2014 -0400 no comments allowed between shall lines commit b79c06121d8e40d8b2d2db0a4dd0a60fbf48edba Author: Luke Campagnola Date: Tue Mar 25 20:56:35 2014 -0400 another pip try commit 09f4f5d82ab41f66e5c746985f09dfcbe36f2089 Author: Luke Campagnola Date: Tue Mar 25 13:36:09 2014 -0400 pip correction commit 0eedb5c18e7242370f996c6b7481fbcdc8e6caf2 Author: Luke Campagnola Date: Tue Mar 25 13:29:00 2014 -0400 correct py version output commit d9fd039be22cb297f4de83f7ab543de25e2969dd Author: Luke Campagnola Date: Tue Mar 25 11:55:43 2014 -0400 apt checks commit cf95ccef86bd26964d73bdc649de8f23f64d2575 Author: Luke Campagnola Date: Tue Mar 25 10:23:10 2014 -0400 alternate pip install method commit bee0bcddfef44917a5ee425557ba6ff246edec87 Author: Luke Campagnola Date: Mon Mar 24 23:51:45 2014 -0400 correct deps install commit 963a4211fcaa5ebbfe0e13a5d879635bd77334ac Author: Luke Campagnola Date: Mon Mar 24 23:47:30 2014 -0400 fixes commit 0c86cd1dc28e286f999f01b37beb3c3252a8c864 Author: Luke Campagnola Date: Mon Mar 24 23:31:06 2014 -0400 permission fix commit 5d04ef53b80a83aa62452ff9a9f9152ca862f8d8 Author: Luke Campagnola Date: Mon Mar 24 23:30:19 2014 -0400 Fix py.test version selection commit b0e6c7cb94c7fa85653e492f8817e79b1ca30426 Author: Luke Campagnola Date: Mon Mar 24 23:25:34 2014 -0400 try another pyqt5 install method commit 422a7928665b5afb72861400672cc81b4bcd9779 Author: Luke Campagnola Date: Mon Mar 24 23:12:36 2014 -0400 syntax error commit 533133905a8e4f2ba26bc6e8f0e4fe631fbd119e Author: Luke Campagnola Date: Mon Mar 24 23:04:37 2014 -0400 fixes commit 8d65211ba4e08e4f4b13b68f9778c3aee4b43cdc Author: Luke Campagnola Date: Mon Mar 24 22:40:18 2014 -0400 Add Qt5 test minor fixes commit 4484efaefe0c99516940812d0472e82998e801f6 Author: Luke Campagnola Date: Mon Mar 24 22:31:56 2014 -0400 use correct py.test for python version commit 5d2441a29b98ed573e15580fc5efd533352ffb45 Author: Luke Campagnola Date: Mon Mar 24 22:24:27 2014 -0400 add setup tests commit 9291db64f25afeb46ee46d92b3bd13aabb325cfe Author: Luke Campagnola Date: Mon Mar 24 21:48:43 2014 -0400 fix py3-pyqt install commit a7aa675c5a5a5c4a2fff69feefc9298bcc626641 Author: Luke Campagnola Date: Mon Mar 24 21:31:33 2014 -0400 travis tests commit e71cd2b23ab09490c29c1c8ee18fc4db87ff0c01 Author: Luke Campagnola Date: Mon Mar 24 21:17:15 2014 -0400 more corrections commit 527df3bca897ba6a02cb3fe4a5a6db34042600b5 Author: Luke Campagnola Date: Mon Mar 24 20:56:01 2014 -0400 travis corrections commit 87d65cac4aa3bf815860030fac78e8e28069e29d Author: Luke Campagnola Date: Mon Mar 24 20:48:02 2014 -0400 Add flake tests Correct style in a few files to please Lord Travis commit 537028f88f17da59a6e8a09b9a3dee34282791cf Author: Luke Campagnola Date: Mon Mar 24 17:36:24 2014 -0400 minimize pyside package install correct line endings to satisfy Lord Travis commit 1e3cc95e37f03f70f50900dcb2e8a4dc4772208a Author: Luke Campagnola Date: Mon Mar 24 17:23:03 2014 -0400 enable pyside, line ending check fix test commit d7df4517f9004399703cb44237d27be313405e84 Author: Luke Campagnola Date: Mon Mar 24 17:12:06 2014 -0400 syntax fix commit 1ad77a21551c38f7ff77bd537eb6c2a9e13a26ae Author: Luke Campagnola Date: Mon Mar 24 17:00:30 2014 -0400 alt. pytest install commit 5edcc020729b1ecd452555852652720b4c3285f5 Author: Luke Campagnola Date: Mon Mar 24 16:52:33 2014 -0400 Added initial travis.yml --- .travis.yml | 230 +++++++++++++++++ pyqtgraph/GraphicsScene/mouseEvents.py | 21 +- pyqtgraph/debug.py | 15 +- pyqtgraph/exporters/PrintExporter.py | 2 +- pyqtgraph/exporters/SVGExporter.py | 7 +- pyqtgraph/flowchart/eq.py | 2 +- pyqtgraph/flowchart/library/Data.py | 2 +- pyqtgraph/frozenSupport.py | 102 ++++---- pyqtgraph/functions.py | 109 ++++---- pyqtgraph/graphicsItems/PlotCurveItem.py | 4 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 14 +- pyqtgraph/multiprocess/remoteproxy.py | 2 +- pyqtgraph/opengl/items/GLAxisItem.py | 2 +- pyqtgraph/opengl/items/GLGridItem.py | 2 +- pyqtgraph/opengl/items/GLLinePlotItem.py | 2 +- pyqtgraph/ordereddict.py | 254 +++++++++---------- pyqtgraph/pixmaps/__init__.py | 52 ++-- pyqtgraph/pixmaps/compile.py | 38 +-- pyqtgraph/tests/test_functions.py | 10 +- pyqtgraph/util/colorama/winterm.py | 2 +- setup.py | 21 +- tools/rebuildUi.py | 2 +- tools/setupHelpers.py | 239 ++++++++++++++++- 23 files changed, 819 insertions(+), 315 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..80cd5067 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,230 @@ +language: python + +# Credit: Original .travis.yml lifted from VisPy + +# Here we use anaconda for 2.6 and 3.3, since it provides the simplest +# interface for running different versions of Python. We could also use +# it for 2.7, but the Ubuntu system has installable 2.7 Qt4-GL, which +# allows for more complete testing. +notifications: + email: false + +virtualenv: + system_site_packages: true + + +env: + # Enable python 2 and python 3 builds + # Note that the 2.6 build doesn't get flake8, and runs old versions of + # Pyglet and GLFW to make sure we deal with those correctly + #- PYTHON=2.6 QT=pyqt TEST=standard + - PYTHON=2.7 QT=pyqt TEST=extra + - PYTHON=2.7 QT=pyside TEST=standard + - PYTHON=3.2 QT=pyqt TEST=standard + - PYTHON=3.2 QT=pyside TEST=standard + #- PYTHON=3.2 QT=pyqt5 TEST=standard + + +before_install: + - TRAVIS_DIR=`pwd` + - travis_retry sudo apt-get update; +# - if [ "${PYTHON}" != "2.7" ]; then +# wget http://repo.continuum.io/miniconda/Miniconda-2.2.2-Linux-x86_64.sh -O miniconda.sh && +# chmod +x miniconda.sh && +# ./miniconda.sh -b && +# export PATH=/home/$USER/anaconda/bin:$PATH && +# conda update --yes conda && +# travis_retry sudo apt-get -qq -y install libgl1-mesa-dri; +# fi; + - if [ "${TRAVIS_PULL_REQUEST}" != "false" ]; then + GIT_TARGET_EXTRA="+refs/heads/${TRAVIS_BRANCH}"; + GIT_SOURCE_EXTRA="+refs/pull/${TRAVIS_PULL_REQUEST}/merge"; + else + GIT_TARGET_EXTRA=""; + GIT_SOURCE_EXTRA=""; + fi; + + # to aid in debugging + - echo ${TRAVIS_BRANCH} + - echo ${TRAVIS_REPO_SLUG} + - echo ${GIT_TARGET_EXTRA} + - echo ${GIT_SOURCE_EXTRA} + +install: + # Dependencies + - if [ "${PYTHON}" == "2.7" ]; then + travis_retry sudo apt-get -qq -y install python-numpy && + export PIP=pip && + sudo ${PIP} install pytest && + sudo ${PIP} install flake8 && + export PYTEST=py.test; + else + travis_retry sudo apt-get -qq -y install python3-numpy && + curl http://python-distribute.org/distribute_setup.py | sudo python3 && + curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | sudo python3 && + export PIP=pip3.2 && + sudo ${PIP} install pytest && + sudo ${PIP} install flake8 && + export PYTEST=py.test-3.2; + fi; + + # Qt + - if [ "${PYTHON}" == "2.7" ]; then + if [ ${QT} == 'pyqt' ]; then + travis_retry sudo apt-get -qq -y install python-qt4 python-qt4-gl; + else + travis_retry sudo apt-get -qq -y install python-pyside.qtcore python-pyside.qtgui python-pyside.qtsvg python-pyside.qtopengl; + fi; + elif [ "${PYTHON}" == "3.2" ]; then + if [ ${QT} == 'pyqt' ]; then + travis_retry sudo apt-get -qq -y install python3-pyqt4; + elif [ ${QT} == 'pyside' ]; then + travis_retry sudo apt-get -qq -y install python3-pyside; + else + ${PIP} search PyQt5; + ${PIP} install PyQt5; + cat /home/travis/.pip/pip.log; + fi; + else + conda create -n testenv --yes --quiet pip python=$PYTHON && + source activate testenv && + if [ ${QT} == 'pyqt' ]; then + conda install --yes --quiet pyside; + else + conda install --yes --quiet pyside; + fi; + fi; + + # Install PyOpenGL + - if [ "${PYTHON}" == "2.7" ]; then + echo "Using OpenGL stable version (apt)"; + travis_retry sudo apt-get -qq -y install python-opengl; + else + echo "Using OpenGL stable version (pip)"; + ${PIP} install -q PyOpenGL; + cat /home/travis/.pip/pip.log; + fi; + + + # Debugging helpers + - uname -a + - cat /etc/issue + - if [ "${PYTHON}" == "2.7" ]; then + python --version; + else + python3 --version; + fi; + - apt-cache search python3-pyqt + - apt-cache search python3-pyside + - apt-cache search pytest + - apt-cache search python pip + - apt-cache search python qt5 + + +before_script: + # We need to create a (fake) display on Travis, let's use a funny resolution + - export DISPLAY=:99.0 + - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render + + # Make sure everyone uses the correct python + - mkdir ~/bin && ln -s `which python${PYTHON}` ~/bin/python + - export PATH=/home/travis/bin:$PATH + - which python + - python --version + # Help color output from each test + - RESET='\033[0m'; + RED='\033[00;31m'; + GREEN='\033[00;32m'; + YELLOW='\033[00;33m'; + BLUE='\033[00;34m'; + PURPLE='\033[00;35m'; + CYAN='\033[00;36m'; + WHITE='\033[00;37m'; + start_test() { + echo -e "${BLUE}======== Starting $1 ========${RESET}"; + }; + check_output() { + ret=$?; + if [ $ret == 0 ]; then + echo -e "${GREEN}>>>>>> $1 passed <<<<<<${RESET}"; + else + echo -e "${RED}>>>>>> $1 FAILED <<<<<<${RESET}"; + fi; + return $ret; + }; + + - if [ "${TEST}" == "extra" ]; then + start_test "repo size check"; + mkdir ~/repo-clone && cd ~/repo-clone && + git init && git remote add -t ${TRAVIS_BRANCH} origin git://github.com/${TRAVIS_REPO_SLUG}.git && + git fetch origin ${GIT_TARGET_EXTRA} && + git checkout -qf FETCH_HEAD && + git tag travis-merge-target && + git gc --aggressive && + TARGET_SIZE=`du -s . | sed -e "s/\t.*//"` && + git pull origin ${GIT_SOURCE_EXTRA} && + git gc --aggressive && + MERGE_SIZE=`du -s . | sed -e "s/\t.*//"` && + if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then + SIZE_DIFF=`expr \( ${MERGE_SIZE} - ${TARGET_SIZE} \)`; + else + SIZE_DIFF=0; + fi; + fi; + + - cd $TRAVIS_DIR + + +script: + + # Run unit tests + - start_test "unit tests"; + PYTHONPATH=. ${PYTEST} pyqtgraph/; + check_output "unit tests"; + + + # check line endings + - if [ "${TEST}" == "extra" ]; then + start_test "line ending check"; + ! find ./ -name "*.py" | xargs file | grep CRLF && + ! find ./ -name "*.rst" | xargs file | grep CRLF; + check_output "line ending check"; + fi; + + # Check repo size does not expand too much + - if [ "${TEST}" == "extra" ]; then + start_test "repo size check"; + echo -e "Estimated content size difference = ${SIZE_DIFF} kB" && + test ${SIZE_DIFF} -lt 100; + check_output "repo size check"; + fi; + + # Check for style issues + - if [ "${TEST}" == "extra" ]; then + start_test "style check"; + cd ~/repo-clone && + git reset -q travis-merge-target && + python setup.py style && + check_output "style check"; + fi; + + - cd $TRAVIS_DIR + + # Check install works + - start_test "install test"; + sudo python${PYTHON} setup.py --quiet install; + check_output "install test"; + + # Check double-install fails + # Note the bash -c is because travis strips off the ! otherwise. + - start_test "double install test"; + bash -c "! sudo python${PYTHON} setup.py --quiet install"; + check_output "double install test"; + + # Check we can import pg + - start_test "import test"; + echo "import sys; print(sys.path)" | python && + cd /; echo "import pyqtgraph.examples" | python; + check_output "import test"; + + diff --git a/pyqtgraph/GraphicsScene/mouseEvents.py b/pyqtgraph/GraphicsScene/mouseEvents.py index fa9bc36d..7809d464 100644 --- a/pyqtgraph/GraphicsScene/mouseEvents.py +++ b/pyqtgraph/GraphicsScene/mouseEvents.py @@ -131,8 +131,12 @@ class MouseDragEvent(object): return self.finish def __repr__(self): - lp = self.lastPos() - p = self.pos() + if self.currentItem is None: + lp = self._lastScenePos + p = self._scenePos + else: + lp = self.lastPos() + p = self.pos() return "(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish())) def modifiers(self): @@ -222,7 +226,10 @@ class MouseClickEvent(object): def __repr__(self): try: - p = self.pos() + if self.currentItem is None: + p = self._scenePos + else: + p = self.pos() return "" % (p.x(), p.y(), int(self.button())) except: return "" % (int(self.button())) @@ -348,8 +355,12 @@ class HoverEvent(object): return Point(self.currentItem.mapFromScene(self._lastScenePos)) def __repr__(self): - lp = self.lastPos() - p = self.pos() + if self.currentItem is None: + lp = self._lastScenePos + p = self._scenePos + else: + lp = self.lastPos() + p = self.pos() return "(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit())) def modifiers(self): diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 7836ba90..0deae0e0 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -240,7 +240,8 @@ def refPathString(chain): def objectSize(obj, ignore=None, verbose=False, depth=0, recursive=False): """Guess how much memory an object is using""" - ignoreTypes = [types.MethodType, types.UnboundMethodType, types.BuiltinMethodType, types.FunctionType, types.BuiltinFunctionType] + ignoreTypes = ['MethodType', 'UnboundMethodType', 'BuiltinMethodType', 'FunctionType', 'BuiltinFunctionType'] + ignoreTypes = [getattr(types, key) for key in ignoreTypes if hasattr(types, key)] ignoreRegex = re.compile('(method-wrapper|Flag|ItemChange|Option|Mode)') @@ -624,12 +625,12 @@ class ObjTracker(object): ## Which refs have disappeared since call to start() (these are only displayed once, then forgotten.) delRefs = {} - for i in self.startRefs.keys(): + for i in list(self.startRefs.keys()): if i not in refs: delRefs[i] = self.startRefs[i] del self.startRefs[i] self.forgetRef(delRefs[i]) - for i in self.newRefs.keys(): + for i in list(self.newRefs.keys()): if i not in refs: delRefs[i] = self.newRefs[i] del self.newRefs[i] @@ -667,7 +668,8 @@ class ObjTracker(object): for k in self.startCount: c1[k] = c1.get(k, 0) - self.startCount[k] typs = list(c1.keys()) - typs.sort(lambda a,b: cmp(c1[a], c1[b])) + #typs.sort(lambda a,b: cmp(c1[a], c1[b])) + typs.sort(key=lambda a: c1[a]) for t in typs: if c1[t] == 0: continue @@ -767,7 +769,8 @@ class ObjTracker(object): c = count.get(typ, [0,0]) count[typ] = [c[0]+1, c[1]+objectSize(obj)] typs = list(count.keys()) - typs.sort(lambda a,b: cmp(count[a][1], count[b][1])) + #typs.sort(lambda a,b: cmp(count[a][1], count[b][1])) + typs.sort(key=lambda a: count[a][1]) for t in typs: line = " %d\t%d\t%s" % (count[t][0], count[t][1], t) @@ -827,7 +830,7 @@ def describeObj(obj, depth=4, path=None, ignore=None): def typeStr(obj): """Create a more useful type string by making types report their class.""" typ = type(obj) - if typ == types.InstanceType: + if typ == getattr(types, 'InstanceType', None): return "" % obj.__class__.__name__ else: return str(typ) diff --git a/pyqtgraph/exporters/PrintExporter.py b/pyqtgraph/exporters/PrintExporter.py index 3e2d45fa..530a1800 100644 --- a/pyqtgraph/exporters/PrintExporter.py +++ b/pyqtgraph/exporters/PrintExporter.py @@ -36,7 +36,7 @@ class PrintExporter(Exporter): dialog = QtGui.QPrintDialog(printer) dialog.setWindowTitle("Print Document") if dialog.exec_() != QtGui.QDialog.Accepted: - return; + return #dpi = QtGui.QDesktopWidget().physicalDpiX() diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 4a02965b..e46c9981 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -1,7 +1,7 @@ from .Exporter import Exporter from ..python2_3 import asUnicode from ..parametertree import Parameter -from ..Qt import QtGui, QtCore, QtSvg +from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE from .. import debug from .. import functions as fn import re @@ -219,7 +219,10 @@ def _generateItemSvg(item, nodes=None, root=None): #if hasattr(item, 'setExportMode'): #item.setExportMode(False) - xmlStr = bytes(arr).decode('utf-8') + if USE_PYSIDE: + xmlStr = str(arr) + else: + xmlStr = bytes(arr).decode('utf-8') doc = xml.parseString(xmlStr) try: diff --git a/pyqtgraph/flowchart/eq.py b/pyqtgraph/flowchart/eq.py index 89ebe09f..554989b2 100644 --- a/pyqtgraph/flowchart/eq.py +++ b/pyqtgraph/flowchart/eq.py @@ -29,7 +29,7 @@ def eq(a, b): except: return False if (hasattr(e, 'implements') and e.implements('MetaArray')): - return e.asarray().all() + return e.asarray().all() else: return e.all() else: diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py index 52458bd9..532f6c5b 100644 --- a/pyqtgraph/flowchart/library/Data.py +++ b/pyqtgraph/flowchart/library/Data.py @@ -328,7 +328,7 @@ class ColumnJoinNode(Node): ## Node.restoreState should have created all of the terminals we need ## However: to maintain support for some older flowchart files, we need - ## to manually add any terminals that were not taken care of. + ## to manually add any terminals that were not taken care of. for name in [n for n in state['order'] if n not in inputs]: Node.addInput(self, name, renamable=True, removable=True, multiable=True) inputs = self.inputs() diff --git a/pyqtgraph/frozenSupport.py b/pyqtgraph/frozenSupport.py index 385bb435..c42a12e1 100644 --- a/pyqtgraph/frozenSupport.py +++ b/pyqtgraph/frozenSupport.py @@ -1,52 +1,52 @@ -## Definitions helpful in frozen environments (eg py2exe) -import os, sys, zipfile - -def listdir(path): - """Replacement for os.listdir that works in frozen environments.""" - if not hasattr(sys, 'frozen'): - return os.listdir(path) - - (zipPath, archivePath) = splitZip(path) - if archivePath is None: - return os.listdir(path) - - with zipfile.ZipFile(zipPath, "r") as zipobj: - contents = zipobj.namelist() - results = set() - for name in contents: - # components in zip archive paths are always separated by forward slash - if name.startswith(archivePath) and len(name) > len(archivePath): - name = name[len(archivePath):].split('/')[0] - results.add(name) - return list(results) - -def isdir(path): - """Replacement for os.path.isdir that works in frozen environments.""" - if not hasattr(sys, 'frozen'): - return os.path.isdir(path) - - (zipPath, archivePath) = splitZip(path) - if archivePath is None: - return os.path.isdir(path) - with zipfile.ZipFile(zipPath, "r") as zipobj: - contents = zipobj.namelist() - archivePath = archivePath.rstrip('/') + '/' ## make sure there's exactly one '/' at the end - for c in contents: - if c.startswith(archivePath): - return True - return False - - -def splitZip(path): - """Splits a path containing a zip file into (zipfile, subpath). - If there is no zip file, returns (path, None)""" - components = os.path.normpath(path).split(os.sep) - for index, component in enumerate(components): - if component.endswith('.zip'): - zipPath = os.sep.join(components[0:index+1]) - archivePath = ''.join([x+'/' for x in components[index+1:]]) - return (zipPath, archivePath) - else: - return (path, None) - +## Definitions helpful in frozen environments (eg py2exe) +import os, sys, zipfile + +def listdir(path): + """Replacement for os.listdir that works in frozen environments.""" + if not hasattr(sys, 'frozen'): + return os.listdir(path) + + (zipPath, archivePath) = splitZip(path) + if archivePath is None: + return os.listdir(path) + + with zipfile.ZipFile(zipPath, "r") as zipobj: + contents = zipobj.namelist() + results = set() + for name in contents: + # components in zip archive paths are always separated by forward slash + if name.startswith(archivePath) and len(name) > len(archivePath): + name = name[len(archivePath):].split('/')[0] + results.add(name) + return list(results) + +def isdir(path): + """Replacement for os.path.isdir that works in frozen environments.""" + if not hasattr(sys, 'frozen'): + return os.path.isdir(path) + + (zipPath, archivePath) = splitZip(path) + if archivePath is None: + return os.path.isdir(path) + with zipfile.ZipFile(zipPath, "r") as zipobj: + contents = zipobj.namelist() + archivePath = archivePath.rstrip('/') + '/' ## make sure there's exactly one '/' at the end + for c in contents: + if c.startswith(archivePath): + return True + return False + + +def splitZip(path): + """Splits a path containing a zip file into (zipfile, subpath). + If there is no zip file, returns (path, None)""" + components = os.path.normpath(path).split(os.sep) + for index, component in enumerate(components): + if component.endswith('.zip'): + zipPath = os.sep.join(components[0:index+1]) + archivePath = ''.join([x+'/' for x in components[index+1:]]) + return (zipPath, archivePath) + else: + return (path, None) + \ No newline at end of file diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index f76a71c9..2325186c 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1424,30 +1424,30 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): data = d2 sideTable = [ - [], - [0,1], - [1,2], - [0,2], - [0,3], - [1,3], - [0,1,2,3], - [2,3], - [2,3], - [0,1,2,3], - [1,3], - [0,3], - [0,2], - [1,2], - [0,1], - [] - ] + [], + [0,1], + [1,2], + [0,2], + [0,3], + [1,3], + [0,1,2,3], + [2,3], + [2,3], + [0,1,2,3], + [1,3], + [0,3], + [0,2], + [1,2], + [0,1], + [] + ] edgeKey=[ - [(0,1), (0,0)], - [(0,0), (1,0)], - [(1,0), (1,1)], - [(1,1), (0,1)] - ] + [(0,1), (0,0)], + [(0,0), (1,0)], + [(1,0), (1,1)], + [(1,1), (0,1)] + ] lines = [] @@ -1635,38 +1635,39 @@ def isosurface(data, level): ## edge index tells us which edges are cut by the isosurface. ## (Data stolen from Bourk; see above.) edgeTable = np.array([ - 0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, - 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, - 0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, - 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, - 0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c, - 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, - 0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac, - 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, - 0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c, - 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, - 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc, - 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, - 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c, - 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, - 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc , - 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, - 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, - 0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, - 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, - 0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, - 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, - 0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, - 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, - 0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460, - 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, - 0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0, - 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, - 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230, - 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, - 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190, - 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, - 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ], dtype=np.uint16) + 0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, + 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, + 0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, + 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, + 0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c, + 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, + 0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac, + 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, + 0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c, + 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, + 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc, + 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, + 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c, + 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, + 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc , + 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, + 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, + 0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, + 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, + 0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, + 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, + 0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, + 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, + 0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460, + 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, + 0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0, + 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, + 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230, + 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, + 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190, + 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, + 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 + ], dtype=np.uint16) ## Table of triangles to use for filling each grid cell. ## Each set of three integers tells us which three edges to diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index ea337100..2197a6cd 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -486,7 +486,7 @@ class PlotCurveItem(GraphicsObject): gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP) ## draw stencil pattern - gl.glStencilMask(0xFF); + gl.glStencilMask(0xFF) gl.glClear(gl.GL_STENCIL_BUFFER_BIT) gl.glBegin(gl.GL_TRIANGLES) gl.glVertex2f(rect.x(), rect.y()) @@ -520,7 +520,7 @@ class PlotCurveItem(GraphicsObject): gl.glEnable(gl.GL_LINE_SMOOTH) gl.glEnable(gl.GL_BLEND) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) - gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST); + gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST) gl.glDrawArrays(gl.GL_LINE_STRIP, 0, pos.size / pos.shape[-1]) finally: gl.glDisableClientState(gl.GL_VERTEX_ARRAY) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 847ff3ac..5c102d95 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -290,16 +290,17 @@ class PlotItem(GraphicsWidget): def getViewBox(self): """Return the :class:`ViewBox ` contained within.""" return self.vb + ## Wrap a few methods from viewBox. - #Important: don't use a settattr(m, getattr(self.vb, m)) as we'd be leaving the viebox alive #because we had a reference to an instance method (creating wrapper methods at runtime instead). - for m in [ - 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', - 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', 'setLimits', - 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY', - 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. + + for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE: + 'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please + 'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', # update the class docstring + 'setAspectLocked', 'invertY', 'register', 'unregister']: # as well. + def _create_method(name): def method(self, *args, **kwargs): return getattr(self.vb, name)(*args, **kwargs) @@ -310,6 +311,7 @@ class PlotItem(GraphicsWidget): del _create_method + def setLogMode(self, x=None, y=None): """ Set log scaling for x and/or y axes. diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index f2896c8b..4e7b7a1c 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -579,7 +579,7 @@ class Request(object): return self._result if timeout is None: - timeout = self.timeout + timeout = self.timeout if block: start = time.time() diff --git a/pyqtgraph/opengl/items/GLAxisItem.py b/pyqtgraph/opengl/items/GLAxisItem.py index c6c206e4..989a44ca 100644 --- a/pyqtgraph/opengl/items/GLAxisItem.py +++ b/pyqtgraph/opengl/items/GLAxisItem.py @@ -45,7 +45,7 @@ class GLAxisItem(GLGraphicsItem): if self.antialias: glEnable(GL_LINE_SMOOTH) - glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST) glBegin( GL_LINES ) diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index a8d1fb7a..4d6bc9d6 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -59,7 +59,7 @@ class GLGridItem(GLGraphicsItem): glEnable(GL_LINE_SMOOTH) glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST) glBegin( GL_LINES ) diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py index 29c7ab5a..f5cb7545 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -96,7 +96,7 @@ class GLLinePlotItem(GLGraphicsItem): glEnable(GL_LINE_SMOOTH) glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST) if self.mode == 'line_strip': glDrawArrays(GL_LINE_STRIP, 0, int(self.pos.size / self.pos.shape[-1])) diff --git a/pyqtgraph/ordereddict.py b/pyqtgraph/ordereddict.py index 5b0303f5..7242b506 100644 --- a/pyqtgraph/ordereddict.py +++ b/pyqtgraph/ordereddict.py @@ -1,127 +1,127 @@ -# Copyright (c) 2009 Raymond Hettinger -# -# 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. - -from UserDict import DictMixin - -class OrderedDict(dict, DictMixin): - - def __init__(self, *args, **kwds): - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__end - except AttributeError: - self.clear() - self.update(*args, **kwds) - - def clear(self): - self.__end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.__map = {} # key --> [key, prev, next] - dict.clear(self) - - def __setitem__(self, key, value): - if key not in self: - end = self.__end - curr = end[1] - curr[2] = end[1] = self.__map[key] = [key, curr, end] - dict.__setitem__(self, key, value) - - def __delitem__(self, key): - dict.__delitem__(self, key) - key, prev, next = self.__map.pop(key) - prev[2] = next - next[1] = prev - - def __iter__(self): - end = self.__end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self): - end = self.__end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - def popitem(self, last=True): - if not self: - raise KeyError('dictionary is empty') - if last: - key = reversed(self).next() - else: - key = iter(self).next() - value = self.pop(key) - return key, value - - def __reduce__(self): - items = [[k, self[k]] for k in self] - tmp = self.__map, self.__end - del self.__map, self.__end - inst_dict = vars(self).copy() - self.__map, self.__end = tmp - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def keys(self): - return list(self) - - setdefault = DictMixin.setdefault - update = DictMixin.update - pop = DictMixin.pop - values = DictMixin.values - items = DictMixin.items - iterkeys = DictMixin.iterkeys - itervalues = DictMixin.itervalues - iteritems = DictMixin.iteritems - - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - - def copy(self): - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - if isinstance(other, OrderedDict): - if len(self) != len(other): - return False - for p, q in zip(self.items(), other.items()): - if p != q: - return False - return True - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other +# Copyright (c) 2009 Raymond Hettinger +# +# 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. + +from UserDict import DictMixin + +class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff --git a/pyqtgraph/pixmaps/__init__.py b/pyqtgraph/pixmaps/__init__.py index 42bd3276..c26e4a6b 100644 --- a/pyqtgraph/pixmaps/__init__.py +++ b/pyqtgraph/pixmaps/__init__.py @@ -1,26 +1,26 @@ -""" -Allows easy loading of pixmaps used in UI elements. -Provides support for frozen environments as well. -""" - -import os, sys, pickle -from ..functions import makeQImage -from ..Qt import QtGui -if sys.version_info[0] == 2: - from . import pixmapData_2 as pixmapData -else: - from . import pixmapData_3 as pixmapData - - -def getPixmap(name): - """ - Return a QPixmap corresponding to the image file with the given name. - (eg. getPixmap('auto') loads pyqtgraph/pixmaps/auto.png) - """ - key = name+'.png' - data = pixmapData.pixmapData[key] - if isinstance(data, basestring) or isinstance(data, bytes): - pixmapData.pixmapData[key] = pickle.loads(data) - arr = pixmapData.pixmapData[key] - return QtGui.QPixmap(makeQImage(arr, alpha=True)) - +""" +Allows easy loading of pixmaps used in UI elements. +Provides support for frozen environments as well. +""" + +import os, sys, pickle +from ..functions import makeQImage +from ..Qt import QtGui +if sys.version_info[0] == 2: + from . import pixmapData_2 as pixmapData +else: + from . import pixmapData_3 as pixmapData + + +def getPixmap(name): + """ + Return a QPixmap corresponding to the image file with the given name. + (eg. getPixmap('auto') loads pyqtgraph/pixmaps/auto.png) + """ + key = name+'.png' + data = pixmapData.pixmapData[key] + if isinstance(data, basestring) or isinstance(data, bytes): + pixmapData.pixmapData[key] = pickle.loads(data) + arr = pixmapData.pixmapData[key] + return QtGui.QPixmap(makeQImage(arr, alpha=True)) + diff --git a/pyqtgraph/pixmaps/compile.py b/pyqtgraph/pixmaps/compile.py index ae07d487..fa0d2408 100644 --- a/pyqtgraph/pixmaps/compile.py +++ b/pyqtgraph/pixmaps/compile.py @@ -1,19 +1,19 @@ -import numpy as np -from PyQt4 import QtGui -import os, pickle, sys - -path = os.path.abspath(os.path.split(__file__)[0]) -pixmaps = {} -for f in os.listdir(path): - if not f.endswith('.png'): - continue - print(f) - img = QtGui.QImage(os.path.join(path, f)) - ptr = img.bits() - ptr.setsize(img.byteCount()) - arr = np.asarray(ptr).reshape(img.height(), img.width(), 4).transpose(1,0,2) - pixmaps[f] = pickle.dumps(arr) -ver = sys.version_info[0] -fh = open(os.path.join(path, 'pixmapData_%d.py' %ver), 'w') -fh.write("import numpy as np; pixmapData=%s" % repr(pixmaps)) - +import numpy as np +from PyQt4 import QtGui +import os, pickle, sys + +path = os.path.abspath(os.path.split(__file__)[0]) +pixmaps = {} +for f in os.listdir(path): + if not f.endswith('.png'): + continue + print(f) + img = QtGui.QImage(os.path.join(path, f)) + ptr = img.bits() + ptr.setsize(img.byteCount()) + arr = np.asarray(ptr).reshape(img.height(), img.width(), 4).transpose(1,0,2) + pixmaps[f] = pickle.dumps(arr) +ver = sys.version_info[0] +fh = open(os.path.join(path, 'pixmapData_%d.py' %ver), 'w') +fh.write("import numpy as np; pixmapData=%s" % repr(pixmaps)) + diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index af0dde58..47fa266d 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -34,8 +34,9 @@ def test_interpolateArray(): result = pg.interpolateArray(data, x) - import scipy.ndimage - spresult = scipy.ndimage.map_coordinates(data, x.T, order=1) + #import scipy.ndimage + #spresult = scipy.ndimage.map_coordinates(data, x.T, order=1) + spresult = np.array([ 5.92, 20. , 11. , 0. , 0. ]) # generated with the above line assert_array_almost_equal(result, spresult) @@ -54,7 +55,10 @@ def test_interpolateArray(): [[1.5, 0.5], [1.5, 1.0], [1.5, 1.5]]]) r1 = pg.interpolateArray(data, x) - r2 = scipy.ndimage.map_coordinates(data, x.transpose(2,0,1), order=1) + #r2 = scipy.ndimage.map_coordinates(data, x.transpose(2,0,1), order=1) + r2 = np.array([[ 8.25, 11. , 16.5 ], # generated with the above line + [ 82.5 , 110. , 165. ]]) + assert_array_almost_equal(r1, r2) diff --git a/pyqtgraph/util/colorama/winterm.py b/pyqtgraph/util/colorama/winterm.py index 27088115..9c1c8185 100644 --- a/pyqtgraph/util/colorama/winterm.py +++ b/pyqtgraph/util/colorama/winterm.py @@ -115,6 +115,6 @@ class WinTerm(object): # fill the entire screen with blanks win32.FillConsoleOutputCharacter(handle, ' ', dw_con_size, coord_screen) # now set the buffer's attributes accordingly - win32.FillConsoleOutputAttribute(handle, self.get_attrs(), dw_con_size, coord_screen ); + win32.FillConsoleOutputAttribute(handle, self.get_attrs(), dw_con_size, coord_screen ) # put the cursor at (0, 0) win32.SetConsoleCursorPosition(handle, (coord_screen.X, coord_screen.Y)) diff --git a/setup.py b/setup.py index 7f2db6bf..b9143245 100644 --- a/setup.py +++ b/setup.py @@ -93,17 +93,34 @@ class Build(distutils.command.build.build): sys.excepthook(*sys.exc_info()) return ret +import distutils.command.install +class Install(distutils.command.install.install): + """ + * Check for previously-installed version before installing + """ + def run(self): + name = self.config_vars['dist_name'] + if name in os.listdir(self.install_libbase): + raise Exception("It appears another version of %s is already " + "installed at %s; remove this before installing." + % (name, self.install_libbase)) + print("Installing to %s" % self.install_libbase) + return distutils.command.install.install.run(self) setup( version=version, - cmdclass={'build': Build, 'deb': helpers.DebCommand, 'test': helpers.TestCommand}, + cmdclass={'build': Build, + 'install': Install, + 'deb': helpers.DebCommand, + 'test': helpers.TestCommand, + 'debug': helpers.DebugCommand, + 'style': helpers.StyleCommand}, 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/rebuildUi.py b/tools/rebuildUi.py index 1e4cbf9c..36f4d34c 100644 --- a/tools/rebuildUi.py +++ b/tools/rebuildUi.py @@ -15,7 +15,7 @@ for path, sd, files in os.walk('.'): py = os.path.join(path, base + '_pyqt.py') if not os.path.exists(py) or os.stat(ui).st_mtime > os.stat(py).st_mtime: os.system('%s %s > %s' % (pyqtuic, ui, py)) - print(py) + print(py) py = os.path.join(path, base + '_pyside.py') if not os.path.exists(py) or os.stat(ui).st_mtime > os.stat(py).st_mtime: diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index ea6aba3f..16defeaa 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os, sys, re try: from subprocess import check_output @@ -9,9 +10,211 @@ except ImportError: output = proc.stdout.read() proc.wait() if proc.returncode != 0: - raise Exception("Process had nonzero return value", proc.returncode) + ex = Exception("Process had nonzero return value %d" % proc.returncode) + ex.returncode = proc.returncode + ex.output = output + raise ex return output +# Paths that are checked for style by flake and flake_diff +FLAKE_CHECK_PATHS = ['pyqtgraph', 'examples', 'tools'] + +# Flake style checks -- mandatory, recommended, optional +# See: http://pep8.readthedocs.org/en/1.4.6/intro.html +# and https://flake8.readthedocs.org/en/2.0/warnings.html +FLAKE_MANDATORY = set([ + 'E101', # indentation contains mixed spaces and tabs + 'E112', # expected an indented block + 'E122', # continuation line missing indentation or outdented + 'E125', # continuation line does not distinguish itself from next line + 'E133', # closing bracket is missing indentation + + 'E223', # tab before operator + 'E224', # tab after operator + 'E242', # tab after ‘,’ + 'E273', # tab after keyword + 'E274', # tab before keyword + + 'E901', # SyntaxError or IndentationError + 'E902', # IOError + + 'W191', # indentation contains tabs + + 'W601', # .has_key() is deprecated, use ‘in’ + 'W602', # deprecated form of raising exception + 'W603', # ‘<>’ is deprecated, use ‘!=’ + 'W604', # backticks are deprecated, use ‘repr()’ + ]) + +FLAKE_RECOMMENDED = set([ + 'E124', # closing bracket does not match visual indentation + 'E231', # missing whitespace after ‘,’ + + 'E211', # whitespace before ‘(‘ + 'E261', # at least two spaces before inline comment + 'E271', # multiple spaces after keyword + 'E272', # multiple spaces before keyword + 'E304', # blank lines found after function decorator + + 'F401', # module imported but unused + 'F402', # import module from line N shadowed by loop variable + 'F403', # ‘from module import *’ used; unable to detect undefined names + 'F404', # future import(s) name after other statements + + 'E501', # line too long (82 > 79 characters) + 'E502', # the backslash is redundant between brackets + + 'E702', # multiple statements on one line (semicolon) + 'E703', # statement ends with a semicolon + 'E711', # comparison to None should be ‘if cond is None:’ + 'E712', # comparison to True should be ‘if cond is True:’ or ‘if cond:’ + 'E721', # do not compare types, use ‘isinstance()’ + + 'F811', # redefinition of unused name from line N + 'F812', # list comprehension redefines name from line N + 'F821', # undefined name name + 'F822', # undefined name name in __all__ + 'F823', # local variable name ... referenced before assignment + 'F831', # duplicate argument name in function definition + 'F841', # local variable name is assigned to but never used + + 'W292', # no newline at end of file + + ]) + +FLAKE_OPTIONAL = set([ + 'E121', # continuation line indentation is not a multiple of four + 'E123', # closing bracket does not match indentation of opening bracket + 'E126', # continuation line over-indented for hanging indent + 'E127', # continuation line over-indented for visual indent + 'E128', # continuation line under-indented for visual indent + + 'E201', # whitespace after ‘(‘ + 'E202', # whitespace before ‘)’ + 'E203', # whitespace before ‘:’ + 'E221', # multiple spaces before operator + 'E222', # multiple spaces after operator + 'E225', # missing whitespace around operator + 'E227', # missing whitespace around bitwise or shift operator + 'E226', # missing whitespace around arithmetic operator + 'E228', # missing whitespace around modulo operator + 'E241', # multiple spaces after ‘,’ + 'E251', # unexpected spaces around keyword / parameter equals + 'E262', # inline comment should start with ‘# ‘ + + 'E301', # expected 1 blank line, found 0 + 'E302', # expected 2 blank lines, found 0 + 'E303', # too many blank lines (3) + + 'E401', # multiple imports on one line + + 'E701', # multiple statements on one line (colon) + + 'W291', # trailing whitespace + 'W293', # blank line contains whitespace + + 'W391', # blank line at end of file + ]) + +FLAKE_IGNORE = set([ + # 111 and 113 are ignored because they appear to be broken. + 'E111', # indentation is not a multiple of four + 'E113', # unexpected indentation + ]) + + +#def checkStyle(): + #try: + #out = check_output(['flake8', '--select=%s' % FLAKE_TESTS, '--statistics', 'pyqtgraph/']) + #ret = 0 + #print("All style checks OK.") + #except Exception as e: + #out = e.output + #ret = e.returncode + #print(out.decode('utf-8')) + #return ret + + +def checkStyle(): + """ Run flake8, checking only lines that are modified since the last + git commit. """ + test = [ 1,2,3 ] + + # First check _all_ code against mandatory error codes + print('flake8: check all code against mandatory error set...') + errors = ','.join(FLAKE_MANDATORY) + cmd = ['flake8', '--select=' + errors] + FLAKE_CHECK_PATHS + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) + #ret = proc.wait() + output = proc.stdout.read().decode('utf-8') + ret = proc.wait() + printFlakeOutput(output) + + # Next check new code with optional error codes + print('flake8: check new code against recommended error set...') + diff = subprocess.check_output(['git', 'diff']) + proc = subprocess.Popen(['flake8', '--diff', #'--show-source', + '--ignore=' + errors], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + proc.stdin.write(diff) + proc.stdin.close() + output = proc.stdout.read().decode('utf-8') + ret |= printFlakeOutput(output) + + if ret == 0: + print('flake8 test passed.') + else: + print('flake8 test failed: %d' % ret) + sys.exit(ret) + + +def printFlakeOutput(text): + """ Print flake output, colored by error category. + Return 2 if there were any mandatory errors, + 1 if only recommended / optional errors, and + 0 if only optional errors. + """ + ret = 0 + gotError = False + for line in text.split('\n'): + m = re.match(r'[^\:]+\:\d+\:\d+\: (\w+) .*', line) + if m is None: + print(line) + else: + gotError = True + error = m.group(1) + if error in FLAKE_MANDATORY: + print("\033[0;31m" + line + "\033[0m") + ret |= 2 + elif error in FLAKE_RECOMMENDED: + print("\033[0;33m" + line + "\033[0m") + #ret |= 1 + elif error in FLAKE_OPTIONAL: + print("\033[0;32m" + line + "\033[0m") + elif error in FLAKE_IGNORE: + continue + else: + print("\033[0;36m" + line + "\033[0m") + if not gotError: + print(" [ no errors ]\n") + return ret + + + +def unitTests(): + try: + if sys.version[0] == '3': + out = check_output('PYTHONPATH=. py.test-3', shell=True) + else: + out = check_output('PYTHONPATH=. py.test', shell=True) + ret = 0 + except Exception as e: + out = e.output + ret = e.returncode + print(out.decode('utf-8')) + return ret + def listAllPackages(pkgroot): path = os.getcwd() n = len(path.split(os.path.sep)) @@ -190,8 +393,8 @@ class DebCommand(Command): raise Exception("Error during debuild.") -class TestCommand(Command): - """Just for learning about distutils; not for running package tests.""" +class DebugCommand(Command): + """Just for learning about distutils.""" description = "" user_options = [] def initialize_options(self): @@ -203,3 +406,33 @@ class TestCommand(Command): cmd = self print(self.distribution.name) print(self.distribution.version) + + +class TestCommand(Command): + description = "Run all package tests and exit immediately with informative return code." + user_options = [] + + def run(self): + sys.exit(unitTests()) + + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + +class StyleCommand(Command): + description = "Check all code for style, exit immediately with informative return code." + user_options = [] + + def run(self): + sys.exit(checkStyle()) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + From 0fb5f05fad5ee748200e036c1d7a707de7092405 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 3 Apr 2014 13:31:02 -0400 Subject: [PATCH 177/268] Added py2exe example from Nitish --- examples/py2exe/plotTest.py | 20 ++++++++++++++++++++ examples/py2exe/setup.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 examples/py2exe/plotTest.py create mode 100644 examples/py2exe/setup.py diff --git a/examples/py2exe/plotTest.py b/examples/py2exe/plotTest.py new file mode 100644 index 00000000..1a53a984 --- /dev/null +++ b/examples/py2exe/plotTest.py @@ -0,0 +1,20 @@ +import sys +from PyQt4 import QtGui +import pyqtgraph as pg +from pyqtgraph.graphicsItems import TextItem +# For packages that require scipy, these may be needed: +# from scipy.stats import futil +# from scipy.sparse.csgraph import _validation + +from pyqtgraph import setConfigOption +pg.setConfigOption('background','w') +pg.setConfigOption('foreground','k') +app = QtGui.QApplication(sys.argv) + +pw = pg.plot(x = [0, 1, 2, 4], y = [4, 5, 9, 6]) +pw.showGrid(x=True,y=True) +text = pg.TextItem(html='
%s
' % "here",anchor=(0.0, 0.0)) +text.setPos(1.0, 5.0) +pw.addItem(text) +status = app.exec_() +sys.exit(status) diff --git a/examples/py2exe/setup.py b/examples/py2exe/setup.py new file mode 100644 index 00000000..760ed6a4 --- /dev/null +++ b/examples/py2exe/setup.py @@ -0,0 +1,32 @@ +from distutils.core import setup + +import shutil +from glob import glob +# Remove the build folder +shutil.rmtree("build", ignore_errors=True) +shutil.rmtree("dist", ignore_errors=True) +import py2exe +import sys + +includes = ['PyQt4', 'PyQt4.QtGui', 'PyQt4.QtSvg', 'sip', 'pyqtgraph.graphicsItems'] +excludes = ['_gtkagg', '_tkagg', 'bsddb', 'curses', 'email', 'pywin.debugger', + 'pywin.debugger.dbgcon', 'pywin.dialogs', 'tcl', + 'Tkconstants', 'Tkinter', 'zmq'] +packages = [] +dll_excludes = ['libgdk-win32-2.0-0.dll', 'libgobject-2.0-0.dll', 'tcl84.dll', + 'tk84.dll', 'MSVCP90.dll'] +icon_resources = [] +bitmap_resources = [] +other_resources = [] +data_files = [] +setup( + data_files=data_files, + console=['plotTest.py'] , + options={"py2exe": {"excludes": excludes, + "includes": includes, + "dll_excludes": dll_excludes, + "optimize": 2, + "compressed": 2, + "bundle_files": 1}}, + zipfile=None, +) From cae310c570faf6c54325ad88b7d03c1085fff842 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 3 Apr 2014 13:33:16 -0400 Subject: [PATCH 178/268] Fix: avoid importing py3 module from pyqt when using py2 --- examples/py2exe/setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/py2exe/setup.py b/examples/py2exe/setup.py index 760ed6a4..c342f90d 100644 --- a/examples/py2exe/setup.py +++ b/examples/py2exe/setup.py @@ -12,6 +12,10 @@ includes = ['PyQt4', 'PyQt4.QtGui', 'PyQt4.QtSvg', 'sip', 'pyqtgraph.graphicsIte excludes = ['_gtkagg', '_tkagg', 'bsddb', 'curses', 'email', 'pywin.debugger', 'pywin.debugger.dbgcon', 'pywin.dialogs', 'tcl', 'Tkconstants', 'Tkinter', 'zmq'] +if sys.version[0] == '2': + # causes syntax error on py2 + excludes.append('PyQt4.uic.port_v3') + packages = [] dll_excludes = ['libgdk-win32-2.0-0.dll', 'libgobject-2.0-0.dll', 'tcl84.dll', 'tk84.dll', 'MSVCP90.dll'] @@ -25,7 +29,7 @@ setup( options={"py2exe": {"excludes": excludes, "includes": includes, "dll_excludes": dll_excludes, - "optimize": 2, + "optimize": 0, "compressed": 2, "bundle_files": 1}}, zipfile=None, From 895a5145d8b6d9b05da61e1d58f0e4ce4b2e7cb2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 3 Apr 2014 15:56:22 -0400 Subject: [PATCH 179/268] Added line endings check to style test --- tools/setupHelpers.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index 16defeaa..34f1d871 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -150,6 +150,26 @@ def checkStyle(): ret = proc.wait() printFlakeOutput(output) + # Check for DOS newlines + print('check line endings in all files...') + count = 0 + allowedEndings = set([None, '\n']) + for path, dirs, files in os.walk('.'): + for f in files: + if os.path.splitext(f)[1] not in ('.py', '.rst'): + continue + filename = os.path.join(path, f) + fh = open(filename, 'U') + x = fh.readlines() + endings = set(fh.newlines if isinstance(fh.newlines, tuple) else (fh.newlines,)) + endings -= allowedEndings + if len(endings) > 0: + print("\033[0;31m" + "File has invalid line endings: %s" % filename + "\033[0m") + ret = ret | 2 + count += 1 + print('checked line endings in %d files' % count) + + # Next check new code with optional error codes print('flake8: check new code against recommended error set...') diff = subprocess.check_output(['git', 'diff']) @@ -163,9 +183,9 @@ def checkStyle(): ret |= printFlakeOutput(output) if ret == 0: - print('flake8 test passed.') + print('style test passed.') else: - print('flake8 test failed: %d' % ret) + print('style test failed: %d' % ret) sys.exit(ret) From 2b9a8be01f8803d2edc42a9e62021ec271e7a4c4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 3 Apr 2014 16:56:17 -0400 Subject: [PATCH 180/268] Add test for repository size --- setup.py | 1 + tools/setupHelpers.py | 107 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index b9143245..ea560959 100644 --- a/setup.py +++ b/setup.py @@ -115,6 +115,7 @@ setup( 'deb': helpers.DebCommand, 'test': helpers.TestCommand, 'debug': helpers.DebugCommand, + 'mergetest': helpers.MergeTestCommand, 'style': helpers.StyleCommand}, packages=allPackages, package_dir={'pyqtgraph.examples': 'examples'}, ## install examples along with the rest of the source diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index 34f1d871..3cf0122a 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import os, sys, re try: - from subprocess import check_output + from subprocess import check_output, check_call except ImportError: import subprocess as sp def check_output(*args, **kwds): @@ -16,6 +16,11 @@ except ImportError: raise ex return output +# Maximum allowed repository size difference (in kB) following merge. +# This is used to prevent large files from being inappropriately added to +# the repository history. +MERGE_SIZE_LIMIT = 100 + # Paths that are checked for style by flake and flake_diff FLAKE_CHECK_PATHS = ['pyqtgraph', 'examples', 'tools'] @@ -186,8 +191,7 @@ def checkStyle(): print('style test passed.') else: print('style test failed: %d' % ret) - sys.exit(ret) - + return ret def printFlakeOutput(text): """ Print flake output, colored by error category. @@ -223,6 +227,10 @@ def printFlakeOutput(text): def unitTests(): + """ + Run all unit tests (using py.test) + Return the exit code. + """ try: if sys.version[0] == '3': out = check_output('PYTHONPATH=. py.test-3', shell=True) @@ -235,6 +243,82 @@ def unitTests(): print(out.decode('utf-8')) return ret + +def checkMergeSize(sourceBranch=None, targetBranch='develop', sourceRepo=None, targetRepo=None): + """ + Check that a git merge would not increase the repository size by MERGE_SIZE_LIMIT. + """ + if sourceBranch is None: + sourceBranch = getGitBranch() + if sourceRepo is None: + sourceRepo = '..' + if targetRepo is None: + targetRepo = '..' + + workingDir = '__merge-test-clone' + env = dict(TARGET_BRANCH=targetBranch, + SOURCE_BRANCH=sourceBranch, + TARGET_REPO=targetRepo, + SOURCE_REPO=sourceRepo, + WORKING_DIR=workingDir, + ) + + print("Testing merge size difference:\n" + " SOURCE: {SOURCE_REPO} {SOURCE_BRANCH}\n" + " TARGET: {TARGET_BRANCH} {TARGET_REPO}".format(**env)) + + setup = """ + mkdir {WORKING_DIR} && cd {WORKING_DIR} && + git init && git remote add -t {TARGET_BRANCH} target {TARGET_REPO} && + git fetch target {TARGET_BRANCH} && + git checkout -qf target/{TARGET_BRANCH} && + git gc -q --aggressive + """.format(**env) + + checkSize = """ + cd {WORKING_DIR} && + du -s . | sed -e "s/\t.*//" + """.format(**env) + + merge = """ + cd {WORKING_DIR} && + git pull -q {SOURCE_REPO} {SOURCE_BRANCH} && + git gc -q --aggressive + """.format(**env) + + try: + print("Check out target branch:\n" + setup) + check_call(setup, shell=True) + targetSize = int(check_output(checkSize, shell=True)) + print("TARGET SIZE: %d kB" % targetSize) + print("Merge source branch:\n" + merge) + check_call(merge, shell=True) + mergeSize = int(check_output(checkSize, shell=True)) + print("MERGE SIZE: %d kB" % mergeSize) + + diff = mergeSize - targetSize + if diff <= MERGE_SIZE_LIMIT: + print("DIFFERENCE: %d kB [OK]" % diff) + return 0 + else: + print("\033[0;31m" + "DIFFERENCE: %d kB [exceeds %d kB]" % (diff, MERGE_SIZE_LIMIT) + "\033[0m") + return 2 + finally: + if os.path.isdir(workingDir): + shutil.rmtree(workingDir) + + +def mergeTests(): + ret = checkMergeSize() + ret |= unitTests() + ret |= checkStyle() + if ret == 0: + print("\033[0;32m" + "\nAll merge tests passed." + "\033[0m") + else: + print("\033[0;31m" + "\nMerge tests failed." + "\033[0m") + return ret + + def listAllPackages(pkgroot): path = os.getcwd() n = len(path.split(os.path.sep)) @@ -435,7 +519,6 @@ class TestCommand(Command): def run(self): sys.exit(unitTests()) - def initialize_options(self): pass @@ -455,4 +538,20 @@ class StyleCommand(Command): def finalize_options(self): pass + +class MergeTestCommand(Command): + description = "Run all tests needed to determine whether the current code is suitable for merge." + user_options = [] + + def run(self): + sys.exit(mergeTests()) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + + \ No newline at end of file From ebccf431bc1ce6d01526169769e15bfbc720c9ae Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 3 Apr 2014 17:20:01 -0400 Subject: [PATCH 181/268] Update merge test to auto-check against github repo --- tools/setupHelpers.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index 3cf0122a..b23fea0a 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -244,16 +244,21 @@ def unitTests(): return ret -def checkMergeSize(sourceBranch=None, targetBranch='develop', sourceRepo=None, targetRepo=None): +def checkMergeSize(sourceBranch=None, targetBranch=None, sourceRepo=None, targetRepo=None): """ Check that a git merge would not increase the repository size by MERGE_SIZE_LIMIT. """ if sourceBranch is None: sourceBranch = getGitBranch() - if sourceRepo is None: sourceRepo = '..' - if targetRepo is None: - targetRepo = '..' + + if targetBranch is None: + if sourceBranch == 'develop': + targetBranch = 'develop' + targetRepo = 'https://github.com/pyqtgraph/pyqtgraph.git' + else: + targetBranch = 'develop' + targetRepo = '..' workingDir = '__merge-test-clone' env = dict(TARGET_BRANCH=targetBranch, From d4553a1eb829db8e9572470b781cd923b73c1349 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 8 Apr 2014 15:29:31 -0400 Subject: [PATCH 182/268] Adde AxisItem.setStyle() --- CHANGELOG | 1 + pyqtgraph/graphicsItems/AxisItem.py | 96 ++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b0df0b13..0db2cf82 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -45,6 +45,7 @@ pyqtgraph-0.9.9 [unreleased] - Multiprocess debugging colors messages by process - Stdout filter that colors text by thread - PeriodicTrace used to report deadlocks + - Added AxisItem.setStyle() Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index e1cf1c4c..bbf59571 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -41,7 +41,7 @@ class AxisItem(GraphicsWidget): self.label.rotate(-90) self.style = { - 'tickTextOffset': (5, 2), ## (horizontal, vertical) spacing between text and axis + 'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis 'tickTextWidth': 30, ## space reserved for tick text 'tickTextHeight': 18, 'autoExpandTextSpace': True, ## automatically expand text space if needed @@ -54,6 +54,7 @@ class AxisItem(GraphicsWidget): (6, 0.2), ## If we already have 6 ticks with text, fill no more than 20% of the axis ], 'showValues': showValues, + 'tickLength': maxTickLength, } self.textWidth = 30 ## Keeps track of maximum width / height of tick text @@ -66,7 +67,6 @@ class AxisItem(GraphicsWidget): self.logMode = False self.tickFont = None - self.tickLength = maxTickLength self._tickLevels = None ## used to override the automatic ticking system with explicit ticks self.scale = 1.0 self.autoSIPrefix = True @@ -87,6 +87,71 @@ class AxisItem(GraphicsWidget): self.grid = False #self.setCacheMode(self.DeviceCoordinateCache) + + def setStyle(self, **kwds): + """ + Set various style options. + + =================== ======================================================= + Keyword Arguments: + tickLength (int) The maximum length of ticks in pixels. + Positive values point toward the text; negative + values point away. + tickTextOffset (int) reserved spacing between text and axis in px + tickTextWidth (int) Horizontal space reserved for tick text in px + tickTextHeight (int) Vertical space reserved for tick text in px + autoExpandTextSpace (bool) Automatically expand text space if the tick + strings become too long. + tickFont (QFont or None) Determines the font used for tick + values. Use None for the default font. + stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis + line is drawn only as far as the last tick. + Otherwise, the line is drawn to the edge of the + AxisItem boundary. + textFillLimits (list of (tick #, % fill) tuples). This structure + determines how the AxisItem decides how many ticks + should have text appear next to them. Each tuple in + the list specifies what fraction of the axis length + may be occupied by text, given the number of ticks + that already have text displayed. For example:: + + [(0, 0.8), # Never fill more than 80% of the axis + (2, 0.6), # If we already have 2 ticks with text, + # fill no more than 60% of the axis + (4, 0.4), # If we already have 4 ticks with text, + # fill no more than 40% of the axis + (6, 0.2)] # If we already have 6 ticks with text, + # fill no more than 20% of the axis + + showValues (bool) indicates whether text is displayed adjacent + to ticks. + =================== ======================================================= + """ + for kwd,value in kwds.items(): + if kwd not in self.style: + raise NameError("%s is not a valid style argument." % kwd) + + if kwd in ('tickLength', 'tickTextOffset', 'tickTextWidth', 'tickTextHeight'): + if not isinstance(value, int): + raise ValueError("Argument '%s' must be int" % kwd) + + if kwd == 'tickTextOffset': + if self.orientation in ('left', 'right'): + self.style['tickTextOffset'][0] = value + else: + self.style['tickTextOffset'][1] = value + elif kwd == 'stopAxisAtTick': + try: + assert len(value) == 2 and isinstance(value[0], bool) and isinstance(value[1], bool) + except: + raise ValueError("Argument 'stopAxisAtTick' must have type (bool, bool)") + self.style[kwd] = value + else: + self.style[kwd] = value + + self.picture = None + self._adjustSize() + self.update() def close(self): self.scene().removeItem(self.label) @@ -128,20 +193,15 @@ class AxisItem(GraphicsWidget): if self.orientation == 'left': p.setY(int(self.size().height()/2 + br.width()/2)) p.setX(-nudge) - #s.setWidth(10) elif self.orientation == 'right': - #s.setWidth(10) p.setY(int(self.size().height()/2 + br.width()/2)) p.setX(int(self.size().width()-br.height()+nudge)) elif self.orientation == 'top': - #s.setHeight(10) p.setY(-nudge) p.setX(int(self.size().width()/2. - br.width()/2.)) elif self.orientation == 'bottom': p.setX(int(self.size().width()/2. - br.width()/2.)) - #s.setHeight(10) p.setY(int(self.size().height()-br.height()+nudge)) - #self.label.resize(s) self.label.setPos(p) self.picture = None @@ -167,7 +227,7 @@ class AxisItem(GraphicsWidget): without any scaling prefix (eg, 'V' instead of 'mV'). The scaling prefix will be automatically prepended based on the range of data displayed. - \**args All extra keyword arguments become CSS style options for + **args All extra keyword arguments become CSS style options for the tag which will surround the axis label and units. ============== ============================================================= @@ -248,8 +308,8 @@ class AxisItem(GraphicsWidget): h = self.textHeight else: h = self.style['tickTextHeight'] - textOffset = self.style['tickTextOffset'][1] if self.style['showValues'] else 0 - h += max(0, self.tickLength) + textOffset + h += self.style['tickTextOffset'][1] if self.style['showValues'] else 0 + h += max(0, self.style['tickLength']) if self.label.isVisible(): h += self.label.boundingRect().height() * 0.8 self.setMaximumHeight(h) @@ -266,7 +326,8 @@ class AxisItem(GraphicsWidget): w = self.textWidth else: w = self.style['tickTextWidth'] - textOffset = self.style['tickTextOffset'][0] if self.style['showValues'] else 0 + w += self.style['tickTextOffset'][0] if self.style['showValues'] else 0 + w += max(0, self.style['tickLength']) if self.label.isVisible(): w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate self.setMaximumWidth(w) @@ -399,14 +460,15 @@ class AxisItem(GraphicsWidget): rect = self.mapRectFromParent(self.geometry()) ## extend rect if ticks go in negative direction ## also extend to account for text that flows past the edges + tl = self.style['tickLength'] if self.orientation == 'left': - rect = rect.adjusted(0, -15, -min(0,self.tickLength), 15) + rect = rect.adjusted(0, -15, -min(0,tl), 15) elif self.orientation == 'right': - rect = rect.adjusted(min(0,self.tickLength), -15, 0, 15) + rect = rect.adjusted(min(0,tl), -15, 0, 15) elif self.orientation == 'top': - rect = rect.adjusted(-15, 0, 15, -min(0,self.tickLength)) + rect = rect.adjusted(-15, 0, 15, -min(0,tl)) elif self.orientation == 'bottom': - rect = rect.adjusted(-15, min(0,self.tickLength), 15, 0) + rect = rect.adjusted(-15, min(0,tl), 15, 0) return rect else: return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) @@ -723,7 +785,7 @@ class AxisItem(GraphicsWidget): ticks = tickLevels[i][1] ## length of tick - tickLength = self.tickLength / ((i*0.5)+1.0) + tickLength = self.style['tickLength'] / ((i*0.5)+1.0) lineAlpha = 255 / (i+1) if self.grid is not False: @@ -848,7 +910,7 @@ class AxisItem(GraphicsWidget): height = textRect.height() width = textRect.width() #self.textHeight = height - offset = max(0,self.tickLength) + textOffset + offset = max(0,self.style['tickLength']) + textOffset if self.orientation == 'left': textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height) From e46439b9038cd7418c6f9e0d44b0a44c2202eb4e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 11 Apr 2014 10:54:21 -0400 Subject: [PATCH 183/268] minor docstring updates --- pyqtgraph/graphicsItems/AxisItem.py | 4 +++- pyqtgraph/graphicsItems/FillBetweenItem.py | 5 ++++- pyqtgraph/graphicsItems/ImageItem.py | 5 +++++ pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 8 +++++++- pyqtgraph/widgets/MultiPlotWidget.py | 2 ++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index bbf59571..9e1aa143 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -123,9 +123,11 @@ class AxisItem(GraphicsWidget): (6, 0.2)] # If we already have 6 ticks with text, # fill no more than 20% of the axis - showValues (bool) indicates whether text is displayed adjacent + showValues (bool) indicates whether text is displayed adjacent to ticks. =================== ======================================================= + + Added in version 0.9.9 """ for kwd,value in kwds.items(): if kwd not in self.style: diff --git a/pyqtgraph/graphicsItems/FillBetweenItem.py b/pyqtgraph/graphicsItems/FillBetweenItem.py index d2ee393c..15a14f86 100644 --- a/pyqtgraph/graphicsItems/FillBetweenItem.py +++ b/pyqtgraph/graphicsItems/FillBetweenItem.py @@ -22,7 +22,10 @@ class FillBetweenItem(QtGui.QGraphicsPathItem): def setCurves(self, curve1, curve2): """Set the curves to fill between. - Arguments must be instances of PlotDataItem or PlotCurveItem.""" + Arguments must be instances of PlotDataItem or PlotCurveItem. + + Added in version 0.9.9 + """ if self.curves is not None: for c in self.curves: diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 379fdb26..f5c2d248 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -140,6 +140,11 @@ class ImageItem(GraphicsObject): self.updateImage() def setAutoDownsample(self, ads): + """ + Set the automatic downsampling mode for this ImageItem. + + Added in version 0.9.9 + """ self.autoDownsample = ads self.qimage = None self.update() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index b27e6e4b..4bd2d980 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -326,6 +326,8 @@ class ViewBox(GraphicsWidget): Set the background color of the ViewBox. If color is None, then no background will be drawn. + + Added in version 0.9.9 """ self.background.setVisible(color is not None) self.state['background'] = color @@ -639,6 +641,7 @@ class ViewBox(GraphicsWidget): **Panning limits**. The following arguments define the region within the viewbox coordinate system that may be accessed by panning the view. + =========== ============================================================ xMin Minimum allowed x-axis value xMax Maximum allowed x-axis value @@ -648,12 +651,15 @@ class ViewBox(GraphicsWidget): **Scaling limits**. These arguments prevent the view being zoomed in or out too far. + =========== ============================================================ minXRange Minimum allowed left-to-right span across the view. maxXRange Maximum allowed left-to-right span across the view. minYRange Minimum allowed top-to-bottom span across the view. maxYRange Maximum allowed top-to-bottom span across the view. - =========== ============================================================ + =========== ============================================================ + + Added in version 0.9.9 """ update = False diff --git a/pyqtgraph/widgets/MultiPlotWidget.py b/pyqtgraph/widgets/MultiPlotWidget.py index abad55ef..d1f56034 100644 --- a/pyqtgraph/widgets/MultiPlotWidget.py +++ b/pyqtgraph/widgets/MultiPlotWidget.py @@ -36,6 +36,8 @@ class MultiPlotWidget(GraphicsView): If the total height of all plots is greater than the height of the widget, then a scroll bar will appear to provide access to the entire set of plots. + + Added in version 0.9.9 """ self.minPlotHeight = min self.resizeEvent(None) From df8562ce55567147eb653c6fefa5f36c2705bfd3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 11 Apr 2014 21:29:15 -0400 Subject: [PATCH 184/268] Fixed TableWidget append / sort issues --- CHANGELOG | 1 + pyqtgraph/widgets/TableWidget.py | 146 ++++++++++++++++++-- pyqtgraph/widgets/tests/test_tablewidget.py | 87 ++++++++++++ 3 files changed, 219 insertions(+), 15 deletions(-) create mode 100644 pyqtgraph/widgets/tests/test_tablewidget.py diff --git a/CHANGELOG b/CHANGELOG index 0db2cf82..13438ea4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -80,6 +80,7 @@ pyqtgraph-0.9.9 [unreleased] - Fixed GLGridItem.setSize - Fixed parametertree.Parameter.sigValueChanging - Fixed AxisItem.__init__(showValues=False) + - Fixed TableWidget append / sort issues pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index d28d07c3..a2976911 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -9,7 +9,30 @@ try: except ImportError: HAVE_METAARRAY = False + __all__ = ['TableWidget'] + + +def _defersort(fn): + def defersort(self, *args, **kwds): + # may be called recursively; only the first call needs to block sorting + setSorting = False + if self._sorting is None: + self._sorting = self.isSortingEnabled() + setSorting = True + self.setSortingEnabled(False) + try: + return fn(self, *args, **kwds) + finally: + if setSorting: + self.setSortingEnabled(self._sorting) + self._sorting = None + + + defersort.func_name = fn.func_name + '_defersort' + return defersort + + class TableWidget(QtGui.QTableWidget): """Extends QTableWidget with some useful functions for automatic data handling and copy / export context menu. Can automatically format and display a variety @@ -18,14 +41,42 @@ class TableWidget(QtGui.QTableWidget): """ def __init__(self, *args, **kwds): + """ + All positional arguments are passed to QTableWidget.__init__(). + + ===================== ================================================= + **Keyword Arguments** + editable (bool) If True, cells in the table can be edited + by the user. Default is False. + sortable (bool) If True, the table may be soted by + clicking on column headers. Note that this also + causes rows to appear initially shuffled until + a sort column is selected. Default is True. + *(added in version 0.9.9)* + ===================== ================================================= + """ + QtGui.QTableWidget.__init__(self, *args) + self.setVerticalScrollMode(self.ScrollPerPixel) self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection) self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) - self.setSortingEnabled(True) self.clear() - editable = kwds.get('editable', False) - self.setEditable(editable) + + if 'sortable' not in kwds: + kwds['sortable'] = True + for kwd, val in kwds.items(): + if kwd == 'editable': + self.setEditable(val) + elif kwd == 'sortable': + self.setSortingEnabled(val) + else: + raise TypeError("Invalid keyword argument '%s'" % kwd) + + self._sorting = None + + self.itemChanged.connect(self.handleItemChanged) + self.contextMenu = QtGui.QMenu() self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel) self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll) @@ -40,6 +91,7 @@ class TableWidget(QtGui.QTableWidget): self.items = [] self.setRowCount(0) self.setColumnCount(0) + self.sortModes = {} def setData(self, data): """Set the data displayed in the table. @@ -56,12 +108,16 @@ class TableWidget(QtGui.QTableWidget): self.appendData(data) self.resizeColumnsToContents() + @_defersort def appendData(self, data): - """Types allowed: - 1 or 2D numpy array or metaArray - 1D numpy record array - list-of-lists, list-of-dicts or dict-of-lists """ + Add new rows to the table. + + See :func:`setData() ` for accepted + data types. + """ + startRow = self.rowCount() + fn0, header0 = self.iteratorFn(data) if fn0 is None: self.clear() @@ -80,18 +136,22 @@ class TableWidget(QtGui.QTableWidget): self.setColumnCount(len(firstVals)) if not self.verticalHeadersSet and header0 is not None: - self.setRowCount(len(header0)) - self.setVerticalHeaderLabels(header0) + labels = [self.verticalHeaderItem(i).text() for i in range(self.rowCount())] + self.setRowCount(startRow + len(header0)) + self.setVerticalHeaderLabels(labels + header0) self.verticalHeadersSet = True if not self.horizontalHeadersSet and header1 is not None: self.setHorizontalHeaderLabels(header1) self.horizontalHeadersSet = True - self.setRow(0, firstVals) - i = 1 + i = startRow + self.setRow(i, firstVals) for row in it0: - self.setRow(i, [x for x in fn1(row)]) i += 1 + self.setRow(i, [x for x in fn1(row)]) + + if self._sorting and self.horizontalHeader().sortIndicatorSection() >= self.columnCount(): + self.sortByColumn(0, QtCore.Qt.AscendingOrder) def setEditable(self, editable=True): self.editable = editable @@ -135,21 +195,46 @@ class TableWidget(QtGui.QTableWidget): def appendRow(self, data): self.appendData([data]) + @_defersort def addRow(self, vals): row = self.rowCount() self.setRowCount(row + 1) self.setRow(row, vals) + @_defersort def setRow(self, row, vals): if row > self.rowCount() - 1: self.setRowCount(row + 1) for col in range(len(vals)): val = vals[col] - item = TableWidgetItem(val) + item = TableWidgetItem(val, row) item.setEditable(self.editable) + sortMode = self.sortModes.get(col, None) + if sortMode is not None: + item.setSortMode(sortMode) self.items.append(item) self.setItem(row, col, item) + def setSortMode(self, column, mode): + """ + Set the mode used to sort *column*. + + ============== ======================================================== + **Sort Modes** + value Compares item.value if available; falls back to text + comparison. + text Compares item.text() + index Compares by the order in which items were inserted. + ============== ======================================================== + + Added in version 0.9.9 + """ + for r in range(self.rowCount()): + item = self.item(r, column) + if hasattr(item, 'setSortMode'): + item.setSortMode(mode) + self.sortModes[column] = mode + def sizeHint(self): # based on http://stackoverflow.com/a/7195443/54056 width = sum(self.columnWidth(i) for i in range(self.columnCount())) @@ -234,25 +319,56 @@ class TableWidget(QtGui.QTableWidget): else: ev.ignore() + def handleItemChanged(self, item): + try: + item.value = type(item.value)(item.text()) + except ValueError: + item.value = str(item.text()) + + class TableWidgetItem(QtGui.QTableWidgetItem): - def __init__(self, val): + def __init__(self, val, index): if isinstance(val, float) or isinstance(val, np.floating): s = "%0.3g" % val else: s = asUnicode(val) QtGui.QTableWidgetItem.__init__(self, s) + self.sortMode = 'value' self.value = val + self.index = index flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled self.setFlags(flags) def setEditable(self, editable): + """ + Set whether this item is user-editable. + """ if editable: self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable) else: self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable) + + def setSortMode(self, mode): + """ + Set the mode used to sort this item against others in its column. + + ============== ======================================================== + **Sort Modes** + value Compares item.value if available; falls back to text + comparison. + text Compares item.text() + index Compares by the order in which items were inserted. + ============== ======================================================== + """ + modes = ('value', 'text', 'index', None) + if mode not in modes: + raise ValueError('Sort mode must be one of %s' % str(modes)) + self.sortMode = mode def __lt__(self, other): - if hasattr(other, 'value'): + if self.sortMode == 'index' and hasattr(other, 'index'): + return self.index < other.index + if self.sortMode == 'value' and hasattr(other, 'value'): return self.value < other.value else: return self.text() < other.text() diff --git a/pyqtgraph/widgets/tests/test_tablewidget.py b/pyqtgraph/widgets/tests/test_tablewidget.py new file mode 100644 index 00000000..c96953af --- /dev/null +++ b/pyqtgraph/widgets/tests/test_tablewidget.py @@ -0,0 +1,87 @@ +import pyqtgraph as pg +import numpy as np +from pyqtgraph.pgcollections import OrderedDict + +app = pg.mkQApp() + + +listOfTuples = [('text_%d' % i, i, i/10.) for i in range(12)] +listOfLists = [list(row) for row in listOfTuples] +plainArray = np.array(listOfLists, dtype=object) +recordArray = np.array(listOfTuples, dtype=[('string', object), + ('integer', int), + ('floating', float)]) +dictOfLists = OrderedDict([(name, list(recordArray[name])) for name in recordArray.dtype.names]) +listOfDicts = [OrderedDict([(name, rec[name]) for name in recordArray.dtype.names]) for rec in recordArray] +transposed = [[row[col] for row in listOfTuples] for col in range(len(listOfTuples[0]))] + +def assertTableData(table, data): + assert len(data) == table.rowCount() + rows = list(range(table.rowCount())) + columns = list(range(table.columnCount())) + for r in rows: + assert len(data[r]) == table.columnCount() + row = [] + for c in columns: + item = table.item(r, c) + if item is not None: + row.append(item.value) + else: + row.append(None) + assert row == list(data[r]) + + +def test_TableWidget(): + w = pg.TableWidget(sortable=False) + + # Test all input data types + w.setData(listOfTuples) + assertTableData(w, listOfTuples) + + w.setData(listOfLists) + assertTableData(w, listOfTuples) + + w.setData(plainArray) + assertTableData(w, listOfTuples) + + w.setData(recordArray) + assertTableData(w, listOfTuples) + + w.setData(dictOfLists) + assertTableData(w, transposed) + + w.appendData(dictOfLists) + assertTableData(w, transposed * 2) + + w.setData(listOfDicts) + assertTableData(w, listOfTuples) + + w.appendData(listOfDicts) + assertTableData(w, listOfTuples * 2) + + # Test sorting + w.setData(listOfTuples) + w.sortByColumn(0, pg.QtCore.Qt.AscendingOrder) + assertTableData(w, sorted(listOfTuples, key=lambda a: a[0])) + + w.sortByColumn(1, pg.QtCore.Qt.AscendingOrder) + assertTableData(w, sorted(listOfTuples, key=lambda a: a[1])) + + w.sortByColumn(2, pg.QtCore.Qt.AscendingOrder) + assertTableData(w, sorted(listOfTuples, key=lambda a: a[2])) + + w.setSortMode(1, 'text') + w.sortByColumn(1, pg.QtCore.Qt.AscendingOrder) + assertTableData(w, sorted(listOfTuples, key=lambda a: str(a[1]))) + + w.setSortMode(1, 'index') + w.sortByColumn(1, pg.QtCore.Qt.AscendingOrder) + assertTableData(w, listOfTuples) + + +if __name__ == '__main__': + w = pg.TableWidget(editable=True) + w.setData(listOfTuples) + w.resize(600, 600) + w.show() + From f54dac6c7077e3b603a9f7f5d43be499dce06e0d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 11 Apr 2014 23:43:45 -0400 Subject: [PATCH 185/268] cleanup --- pyqtgraph/widgets/TableWidget.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index a2976911..4eaca231 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -28,8 +28,6 @@ def _defersort(fn): self.setSortingEnabled(self._sorting) self._sorting = None - - defersort.func_name = fn.func_name + '_defersort' return defersort @@ -63,15 +61,13 @@ class TableWidget(QtGui.QTableWidget): self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) self.clear() - if 'sortable' not in kwds: - kwds['sortable'] = True - for kwd, val in kwds.items(): - if kwd == 'editable': - self.setEditable(val) - elif kwd == 'sortable': - self.setSortingEnabled(val) - else: - raise TypeError("Invalid keyword argument '%s'" % kwd) + kwds.setdefault('sortable', True) + kwds.setdefault('editable', False) + self.setEditable(kwds.pop('editable')) + self.setSortingEnabled(kwds.pop('sortable')) + + if len(kwds) > 0: + raise TypeError("Invalid keyword arguments '%s'" % kwds.keys()) self._sorting = None From 12d1a5dccfc986abf3652d794d14f01b0881ef9c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 12 Apr 2014 11:54:23 -0400 Subject: [PATCH 186/268] Added configurable formatting for TableWidget --- CHANGELOG | 1 + pyqtgraph/widgets/TableWidget.py | 127 +++++++++++++++++--- pyqtgraph/widgets/tests/test_tablewidget.py | 43 ++++++- 3 files changed, 154 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 13438ea4..bf692284 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -46,6 +46,7 @@ pyqtgraph-0.9.9 [unreleased] - Stdout filter that colors text by thread - PeriodicTrace used to report deadlocks - Added AxisItem.setStyle() + - Added configurable formatting for TableWidget Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 4eaca231..14060546 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -56,6 +56,8 @@ class TableWidget(QtGui.QTableWidget): QtGui.QTableWidget.__init__(self, *args) + self.itemClass = TableWidgetItem + self.setVerticalScrollMode(self.ScrollPerPixel) self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection) self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) @@ -69,7 +71,10 @@ class TableWidget(QtGui.QTableWidget): if len(kwds) > 0: raise TypeError("Invalid keyword arguments '%s'" % kwds.keys()) - self._sorting = None + self._sorting = None # used when temporarily disabling sorting + + self._formats = {None: None} # stores per-column formats and entire table format + self.sortModes = {} # stores per-column sort mode self.itemChanged.connect(self.handleItemChanged) @@ -153,7 +158,49 @@ class TableWidget(QtGui.QTableWidget): self.editable = editable for item in self.items: item.setEditable(editable) - + + def setFormat(self, format, column=None): + """ + Specify the default text formatting for the entire table, or for a + single column if *column* is specified. + + If a string is specified, it is used as a format string for converting + float values (and all other types are converted using str). If a + function is specified, it will be called with the item as its only + argument and must return a string. Setting format = None causes the + default formatter to be used instead. + + Added in version 0.9.9. + + """ + if format is not None and not isinstance(format, basestring) and not callable(format): + raise ValueError("Format argument must string, callable, or None. (got %s)" % format) + + self._formats[column] = format + + + if column is None: + # update format of all items that do not have a column format + # specified + for c in range(self.columnCount()): + if self._formats.get(c, None) is None: + for r in range(self.rowCount()): + item = self.item(r, c) + if item is None: + continue + item.setFormat(format) + else: + # set all items in the column to use this format, or the default + # table format if None was specified. + if format is None: + format = self._formats[None] + for r in range(self.rowCount()): + item = self.item(r, column) + if item is None: + continue + item.setFormat(format) + + def iteratorFn(self, data): ## Return 1) a function that will provide an iterator for data and 2) a list of header strings if isinstance(data, list) or isinstance(data, tuple): @@ -203,13 +250,17 @@ class TableWidget(QtGui.QTableWidget): self.setRowCount(row + 1) for col in range(len(vals)): val = vals[col] - item = TableWidgetItem(val, row) + item = self.itemClass(val, row) item.setEditable(self.editable) sortMode = self.sortModes.get(col, None) if sortMode is not None: item.setSortMode(sortMode) + format = self._formats.get(col, self._formats[None]) + item.setFormat(format) self.items.append(item) self.setItem(row, col, item) + item.setValue(val) # Required--the text-change callback is invoked + # when we call setItem. def setSortMode(self, column, mode): """ @@ -254,7 +305,6 @@ class TableWidget(QtGui.QTableWidget): rows = list(range(self.rowCount())) columns = list(range(self.columnCount())) - data = [] if self.horizontalHeadersSet: row = [] @@ -303,7 +353,6 @@ class TableWidget(QtGui.QTableWidget): if fileName == '': return open(fileName, 'w').write(data) - def contextMenuEvent(self, ev): self.contextMenu.popup(ev.globalPos()) @@ -316,24 +365,21 @@ class TableWidget(QtGui.QTableWidget): ev.ignore() def handleItemChanged(self, item): - try: - item.value = type(item.value)(item.text()) - except ValueError: - item.value = str(item.text()) + item.textChanged() class TableWidgetItem(QtGui.QTableWidgetItem): - def __init__(self, val, index): - if isinstance(val, float) or isinstance(val, np.floating): - s = "%0.3g" % val - else: - s = asUnicode(val) - QtGui.QTableWidgetItem.__init__(self, s) + def __init__(self, val, index, format=None): + QtGui.QTableWidgetItem.__init__(self, '') + self._blockValueChange = False + self._format = None + self._defaultFormat = '%0.3g' self.sortMode = 'value' - self.value = val self.index = index flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled self.setFlags(flags) + self.setValue(val) + self.setFormat(format) def setEditable(self, editable): """ @@ -360,6 +406,55 @@ class TableWidgetItem(QtGui.QTableWidgetItem): if mode not in modes: raise ValueError('Sort mode must be one of %s' % str(modes)) self.sortMode = mode + + def setFormat(self, fmt): + """Define the conversion from item value to displayed text. + + If a string is specified, it is used as a format string for converting + float values (and all other types are converted using str). If a + function is specified, it will be called with the item as its only + argument and must return a string. + + Added in version 0.9.9. + """ + if fmt is not None and not isinstance(fmt, basestring) and not callable(fmt): + raise ValueError("Format argument must string, callable, or None. (got %s)" % fmt) + self._format = fmt + self._updateText() + + def _updateText(self): + self._blockValueChange = True + try: + self.setText(self.format()) + finally: + self._blockValueChange = False + + def setValue(self, value): + self.value = value + self._updateText() + + def textChanged(self): + """Called when this item's text has changed for any reason.""" + if self._blockValueChange: + # text change was result of value or format change; do not + # propagate. + return + + try: + self.value = type(self.value)(self.text()) + except ValueError: + self.value = str(self.text()) + + def format(self): + if callable(self._format): + return self._format(self) + if isinstance(self.value, (float, np.floating)): + if self._format is None: + return self._defaultFormat % self.value + else: + return self._format % self.value + else: + return asUnicode(self.value) def __lt__(self, other): if self.sortMode == 'index' and hasattr(other, 'index'): diff --git a/pyqtgraph/widgets/tests/test_tablewidget.py b/pyqtgraph/widgets/tests/test_tablewidget.py index c96953af..11416430 100644 --- a/pyqtgraph/widgets/tests/test_tablewidget.py +++ b/pyqtgraph/widgets/tests/test_tablewidget.py @@ -5,7 +5,7 @@ from pyqtgraph.pgcollections import OrderedDict app = pg.mkQApp() -listOfTuples = [('text_%d' % i, i, i/10.) for i in range(12)] +listOfTuples = [('text_%d' % i, i, i/9.) for i in range(12)] listOfLists = [list(row) for row in listOfTuples] plainArray = np.array(listOfLists, dtype=object) recordArray = np.array(listOfTuples, dtype=[('string', object), @@ -78,6 +78,47 @@ def test_TableWidget(): w.sortByColumn(1, pg.QtCore.Qt.AscendingOrder) assertTableData(w, listOfTuples) + # Test formatting + item = w.item(0, 2) + assert item.text() == ('%0.3g' % item.value) + + w.setFormat('%0.6f') + assert item.text() == ('%0.6f' % item.value) + + w.setFormat('X%0.7f', column=2) + assert isinstance(item.value, float) + assert item.text() == ('X%0.7f' % item.value) + + # test setting items that do not exist yet + w.setFormat('X%0.7f', column=3) + + # test append uses correct formatting + w.appendRow(('x', 10, 7.3)) + item = w.item(w.rowCount()-1, 2) + assert isinstance(item.value, float) + assert item.text() == ('X%0.7f' % item.value) + + # test reset back to defaults + w.setFormat(None, column=2) + assert isinstance(item.value, float) + assert item.text() == ('%0.6f' % item.value) + + w.setFormat(None) + assert isinstance(item.value, float) + assert item.text() == ('%0.3g' % item.value) + + # test function formatter + def fmt(item): + if isinstance(item.value, float): + return "%d %f" % (item.index, item.value) + else: + return pg.asUnicode(item.value) + w.setFormat(fmt) + assert isinstance(item.value, float) + assert isinstance(item.index, int) + assert item.text() == ("%d %f" % (item.index, item.value)) + + if __name__ == '__main__': w = pg.TableWidget(editable=True) From ac90bf4c3b996e80032baf28f3da17eecb0f1679 Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Sat, 12 Apr 2014 15:31:20 -0400 Subject: [PATCH 187/268] PlotDataItem: add missing 'stepMode' keyword argument for PlotCurveItem --- examples/histogram.py | 6 ++++-- pyqtgraph/graphicsItems/PlotDataItem.py | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/histogram.py b/examples/histogram.py index 057abffd..d9d70d8b 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -15,6 +15,7 @@ win.resize(800,350) win.setWindowTitle('pyqtgraph example: Histogram') plt1 = win.addPlot() plt2 = win.addPlot() +plt3 = win.addPlot() ## make interesting distribution of values vals = np.hstack([np.random.normal(size=500), np.random.normal(size=260, loc=4)]) @@ -27,11 +28,12 @@ y,x = np.histogram(vals, bins=np.linspace(-3, 8, 40)) curve = pg.PlotCurveItem(x, y, stepMode=True, fillLevel=0, brush=(0, 0, 255, 80)) plt1.addItem(curve) +plt2.plot(x, y, stepMode=True, fillLevel=0, brush=(0,0,255,150)) ## Now draw all points as a nicely-spaced scatter plot y = pg.pseudoScatter(vals, spacing=0.15) -#plt2.plot(vals, y, pen=None, symbol='o', symbolSize=5) -plt2.plot(vals, y, pen=None, symbol='o', symbolSize=5, symbolPen=(255,255,255,200), symbolBrush=(0,0,255,150)) +#plt3.plot(vals, y, pen=None, symbol='o', symbolSize=5) +plt3.plot(vals, y, pen=None, symbol='o', symbolSize=5, symbolPen=(255,255,255,200), symbolBrush=(0,0,255,150)) ## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index ab9c9911..a8c094a9 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -68,6 +68,9 @@ class PlotDataItem(GraphicsObject): fillLevel Fill the area between the curve and fillLevel fillBrush Fill to use when fillLevel is specified. May be any single argument accepted by :func:`mkBrush() ` + stepMode If True, two orthogonal lines are drawn for each sample + as steps. This is commonly used when drawing histograms. + Note that in this case, len(x) == len(y) + 1 ========== ============================================================================== **Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() ` for more information) @@ -150,6 +153,7 @@ class PlotDataItem(GraphicsObject): 'shadowPen': None, 'fillLevel': None, 'fillBrush': None, + 'stepMode': None, 'symbol': None, 'symbolSize': 10, @@ -456,7 +460,7 @@ class PlotDataItem(GraphicsObject): def updateItems(self): curveArgs = {} - for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect')]: + for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect'), ('stepMode', 'stepMode')]: curveArgs[v] = self.opts[k] scatterArgs = {} From c5d4c92a7555e61ad9a723d40f08faa5695aa7aa Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 12 Apr 2014 17:02:56 -0400 Subject: [PATCH 188/268] Fixed AxisItem not resizing text area when setTicks() is used --- CHANGELOG | 1 + pyqtgraph/graphicsItems/AxisItem.py | 25 ++++++++++++------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bf692284..66544979 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -82,6 +82,7 @@ pyqtgraph-0.9.9 [unreleased] - Fixed parametertree.Parameter.sigValueChanging - Fixed AxisItem.__init__(showValues=False) - Fixed TableWidget append / sort issues + - Fixed AxisItem not resizing text area when setTicks() is used pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 9e1aa143..c25f7a7f 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -690,7 +690,7 @@ class AxisItem(GraphicsWidget): def generateDrawSpecs(self, p): """ - Calls tickValues() and tickStrings to determine where and how ticks should + Calls tickValues() and tickStrings() to determine where and how ticks should be drawn, then generates from this a set of drawing commands to be interpreted by drawPicture(). """ @@ -739,6 +739,7 @@ class AxisItem(GraphicsWidget): if lengthInPixels == 0: return + # Determine major / minor / subminor axis ticks if self._tickLevels is None: tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels) tickStrings = None @@ -778,8 +779,7 @@ class AxisItem(GraphicsWidget): tickPositions = [] # remembers positions of previously drawn ticks - ## draw ticks - ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching) + ## compute coordinates to draw ticks ## draw three different intervals, long ticks first tickSpecs = [] for i in range(len(tickLevels)): @@ -814,7 +814,6 @@ class AxisItem(GraphicsWidget): tickSpecs.append((tickPen, Point(p1), Point(p2))) profiler('compute ticks') - ## This is where the long axis line should be drawn if self.style['stopAxisAtTick'][0] is True: stop = max(span[0].y(), min(map(min, tickPositions))) @@ -831,7 +830,6 @@ class AxisItem(GraphicsWidget): axisSpec = (self.pen(), span[0], span[1]) - textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text #if self.style['autoExpandTextSpace'] is True: #textWidth = self.textWidth @@ -878,15 +876,15 @@ class AxisItem(GraphicsWidget): rects.append(br) textRects.append(rects[-1]) - if i > 0: ## always draw top level - ## measure all text, make sure there's enough room - if axis == 0: - textSize = np.sum([r.height() for r in textRects]) - textSize2 = np.max([r.width() for r in textRects]) if textRects else 0 - else: - textSize = np.sum([r.width() for r in textRects]) - textSize2 = np.max([r.height() for r in textRects]) if textRects else 0 + ## measure all text, make sure there's enough room + if axis == 0: + textSize = np.sum([r.height() for r in textRects]) + textSize2 = np.max([r.width() for r in textRects]) if textRects else 0 + else: + textSize = np.sum([r.width() for r in textRects]) + textSize2 = np.max([r.height() for r in textRects]) if textRects else 0 + if i > 0: ## always draw top level ## If the strings are too crowded, stop drawing text now. ## We use three different crowding limits based on the number ## of texts drawn so far. @@ -901,6 +899,7 @@ class AxisItem(GraphicsWidget): #spacing, values = tickLevels[best] #strings = self.tickStrings(values, self.scale, spacing) + # Determine exactly where tick text should be drawn for j in range(len(strings)): vstr = strings[j] if vstr is None: ## this tick was ignored because it is out of bounds From 98dec9e954b649fc711969975b0d1e997c82d27b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 12 Apr 2014 18:01:50 -0400 Subject: [PATCH 189/268] minor cleanup --- examples/histogram.py | 14 ++++---------- pyqtgraph/graphicsItems/PlotDataItem.py | 3 ++- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/examples/histogram.py b/examples/histogram.py index d9d70d8b..2674ba30 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -2,8 +2,6 @@ """ In this example we draw two different kinds of histogram. """ - - import initExample ## Add path to library (just for examples; you do not need this) import pyqtgraph as pg @@ -15,7 +13,6 @@ win.resize(800,350) win.setWindowTitle('pyqtgraph example: Histogram') plt1 = win.addPlot() plt2 = win.addPlot() -plt3 = win.addPlot() ## make interesting distribution of values vals = np.hstack([np.random.normal(size=500), np.random.normal(size=260, loc=4)]) @@ -23,17 +20,14 @@ vals = np.hstack([np.random.normal(size=500), np.random.normal(size=260, loc=4)] ## compute standard histogram y,x = np.histogram(vals, bins=np.linspace(-3, 8, 40)) +## Using stepMode=True causes the plot to draw two lines for each sample. ## notice that len(x) == len(y)+1 -## We are required to use stepMode=True so that PlotCurveItem will interpret this data correctly. -curve = pg.PlotCurveItem(x, y, stepMode=True, fillLevel=0, brush=(0, 0, 255, 80)) -plt1.addItem(curve) - -plt2.plot(x, y, stepMode=True, fillLevel=0, brush=(0,0,255,150)) +plt1.plot(x, y, stepMode=True, fillLevel=0, brush=(0,0,255,150)) ## Now draw all points as a nicely-spaced scatter plot y = pg.pseudoScatter(vals, spacing=0.15) -#plt3.plot(vals, y, pen=None, symbol='o', symbolSize=5) -plt3.plot(vals, y, pen=None, symbol='o', symbolSize=5, symbolPen=(255,255,255,200), symbolBrush=(0,0,255,150)) +#plt2.plot(vals, y, pen=None, symbol='o', symbolSize=5) +plt2.plot(vals, y, pen=None, symbol='o', symbolSize=5, symbolPen=(255,255,255,200), symbolBrush=(0,0,255,150)) ## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index a8c094a9..3e760ce1 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -70,7 +70,8 @@ class PlotDataItem(GraphicsObject): May be any single argument accepted by :func:`mkBrush() ` stepMode If True, two orthogonal lines are drawn for each sample as steps. This is commonly used when drawing histograms. - Note that in this case, len(x) == len(y) + 1 + Note that in this case, `len(x) == len(y) + 1` + (added in version 0.9.9) ========== ============================================================================== **Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() ` for more information) From b8840e22245bb505b54777c386238a6014fb5ce4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 15 Apr 2014 15:49:17 -0400 Subject: [PATCH 190/268] Removed absolute imports --- pyqtgraph/__init__.py | 2 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- pyqtgraph/opengl/MeshData.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 30160565..01e84c49 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -257,7 +257,7 @@ from .graphicsWindows import * from .SignalProxy import * from .colormap import * from .ptime import time -from pyqtgraph.Qt import isQObjectAlive +from .Qt import isQObjectAlive ############################################################## diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 4bd2d980..5fdbdf08 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -10,7 +10,7 @@ from copy import deepcopy from ... import debug as debug from ... import getConfigOption import sys -from pyqtgraph.Qt import isQObjectAlive +from ...Qt import isQObjectAlive __all__ = ['ViewBox'] diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index 34a6e3fc..5adf4b64 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -1,5 +1,5 @@ -from pyqtgraph.Qt import QtGui -import pyqtgraph.functions as fn +from ..Qt import QtGui +from .. import functions as fn import numpy as np class MeshData(object): @@ -501,4 +501,4 @@ class MeshData(object): faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols return MeshData(vertexes=verts, faces=faces) - \ No newline at end of file + From 5cea58545f4140d5958c633f267d194d729da273 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 20 Apr 2014 14:04:33 -0400 Subject: [PATCH 191/268] Fixed mouse wheel in RemoteGraphicsView + PySide --- pyqtgraph/widgets/RemoteGraphicsView.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index cb9a7052..75ce90b0 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -108,7 +108,7 @@ class RemoteGraphicsView(QtGui.QWidget): return QtGui.QWidget.mouseMoveEvent(self, ev) def wheelEvent(self, ev): - self._view.wheelEvent(ev.pos(), ev.globalPos(), ev.delta(), int(ev.buttons()), int(ev.modifiers()), ev.orientation(), _callSync='off') + self._view.wheelEvent(ev.pos(), ev.globalPos(), ev.delta(), int(ev.buttons()), int(ev.modifiers()), int(ev.orientation()), _callSync='off') ev.accept() return QtGui.QWidget.wheelEvent(self, ev) @@ -243,6 +243,7 @@ class Renderer(GraphicsView): def wheelEvent(self, pos, gpos, d, btns, mods, ori): btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) + ori = (None, QtCore.Qt.Horizontal, QtCore.Qt.Vertical)[ori] return GraphicsView.wheelEvent(self, QtGui.QWheelEvent(pos, gpos, d, btns, mods, ori)) def keyEvent(self, typ, mods, text, autorep, count): From 4e555e0bf3ae92daa47b24f5706d139f326e8e30 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 26 Apr 2014 11:13:32 -0400 Subject: [PATCH 192/268] Fix OSX division-by-zero in ViewBox --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 5fdbdf08..46ed2984 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1454,9 +1454,10 @@ class ViewBox(GraphicsWidget): if aspect is not False and aspect != 0 and tr.height() != 0 and bounds.height() != 0: ## This is the view range aspect ratio we have requested - targetRatio = tr.width() / tr.height() + targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1 ## This is the view range aspect ratio we need to obey aspect constraint - viewRatio = (bounds.width() / bounds.height()) / aspect + viewRatio = (bounds.width() / bounds.height() if bounds.height() != 0 else 1) / aspect + viewRatio = 1 if viewRatio == 0 else viewRatio # Decide which range to keep unchanged #print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] From 5a8d77d6f263eaf90f282f23bd4dc9806eed7b45 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 26 Apr 2014 13:45:34 -0400 Subject: [PATCH 193/268] Fixed unicode issues with PySide loadUiType Added unit test for loadUiType --- pyqtgraph/Qt.py | 17 +++++++++++++++-- pyqtgraph/tests/test_qt.py | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 2fcff32f..4fe8c3ab 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -11,6 +11,8 @@ This module exists to smooth out some of the differences between PySide and PyQt import sys, re +from .python2_3 import asUnicode + ## Automatically determine whether to use PyQt or PySide. ## This is done by first checking to see whether one of the libraries ## is already imported. If not, then attempt to import PyQt4, then PySide. @@ -56,18 +58,29 @@ if USE_PYSIDE: # Credit: # http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313 + class StringIO(object): + """Alternative to built-in StringIO needed to circumvent unicode/ascii issues""" + def __init__(self): + self.data = [] + + def write(self, data): + self.data.append(data) + + def getvalue(self): + return ''.join(map(asUnicode, self.data)).encode('utf8') + def loadUiType(uiFile): """ Pyside "loadUiType" command like PyQt4 has one, so we have to convert the ui file to py code in-memory first and then execute it in a special frame to retrieve the form_class. """ import pysideuic import xml.etree.ElementTree as xml - from io import StringIO + #from io import StringIO parsed = xml.parse(uiFile) widget_class = parsed.find('widget').get('class') form_class = parsed.find('class').text - + with open(uiFile, 'r') as f: o = StringIO() frame = {} diff --git a/pyqtgraph/tests/test_qt.py b/pyqtgraph/tests/test_qt.py index cef54777..729bf695 100644 --- a/pyqtgraph/tests/test_qt.py +++ b/pyqtgraph/tests/test_qt.py @@ -1,5 +1,7 @@ import pyqtgraph as pg -import gc +import gc, os + +app = pg.mkQApp() def test_isQObjectAlive(): o1 = pg.QtCore.QObject() @@ -8,3 +10,14 @@ def test_isQObjectAlive(): del o1 gc.collect() assert not pg.Qt.isQObjectAlive(o2) + + +def test_loadUiType(): + path = os.path.dirname(__file__) + formClass, baseClass = pg.Qt.loadUiType(os.path.join(path, 'uictest.ui')) + w = baseClass() + ui = formClass() + ui.setupUi(w) + w.show() + app.processEvents() + From 8dd7f07158a2bb9135c3f056fb614659b293b3a5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 3 Apr 2014 13:37:07 -0400 Subject: [PATCH 194/268] Start stability tests: - randomly add / remove widgets and graphicsitems to provoke a crash - create and delete various objects and ensure that nothing is left in memory --- pyqtgraph/tests/test_ref_cycles.py | 45 ++++++++++++++++++++++++++++++ pyqtgraph/tests/test_stability.py | 39 ++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 pyqtgraph/tests/test_ref_cycles.py create mode 100644 pyqtgraph/tests/test_stability.py diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py new file mode 100644 index 00000000..ac65eaf9 --- /dev/null +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -0,0 +1,45 @@ +""" +Test for unwanted reference cycles + +""" +import pyqtgraph as pg +import numpy as np +import gc +app = pg.mkQApp() + +def processEvents(): + for i in range(3): + gc.collect() + app.processEvents() + # processEvents ignored DeferredDelete events; we must process these + # manually. + app.sendPostedEvents(None, pg.QtCore.QEvent.DeferredDelete) + +def test_PlotItem(): + for i in range(10): + plt = pg.PlotItem() + plt.plot(np.random.normal(size=10000)) + processEvents() + + ot = pg.debug.ObjTracker() + + plots = [] + for i in range(10): + plt = pg.PlotItem() + plt.plot(np.random.normal(size=10000)) + plots.append(plt) + processEvents() + + ot.diff() + + del plots + processEvents() + + ot.diff() + + + return ot + + +if __name__ == '__main__': + ot = test_PlotItem() diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py new file mode 100644 index 00000000..3838af7d --- /dev/null +++ b/pyqtgraph/tests/test_stability.py @@ -0,0 +1,39 @@ +""" +PyQt/PySide stress test: + +Create lots of random widgets and graphics items, connect them together randomly, +the tear them down repeatedly. + +The purpose of this is to attempt to generate segmentation faults. +""" +import pyqtgraph as pg +import random + +random.seed(12345) + +widgetTypes = [pg.PlotWidget, pg.ImageView, pg.GraphicsView, pg.QtGui.QWidget, + pg.QtGui.QTreeWidget, pg.QtGui.QPushButton] + +itemTypes = [pg.PlotCurveItem, pg.ImageItem, pg.PlotDataItem, pg.ViewBox, + pg.QtGui.QGraphicsRectItem] + +while True: + action = random.randint(0,5) + if action == 0: + # create a widget + pass + elif action == 1: + # set parent (widget or None), possibly create a reference in either direction + pass + elif action == 2: + # + pass + elif action == 3: + pass + + + + + + + From 82bd5ef584e782ca08888a0c29a93a6901aa6a55 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 13 Apr 2014 22:51:54 -0400 Subject: [PATCH 195/268] stability test is successfully crashing, ref_cycle test exposes cycles! --- pyqtgraph/tests/test_ref_cycles.py | 51 +++++---- pyqtgraph/tests/test_stability.py | 162 ++++++++++++++++++++++++----- 2 files changed, 170 insertions(+), 43 deletions(-) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index ac65eaf9..87e0d71d 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -4,7 +4,7 @@ Test for unwanted reference cycles """ import pyqtgraph as pg import numpy as np -import gc +import gc, weakref app = pg.mkQApp() def processEvents(): @@ -15,31 +15,46 @@ def processEvents(): # manually. app.sendPostedEvents(None, pg.QtCore.QEvent.DeferredDelete) -def test_PlotItem(): - for i in range(10): - plt = pg.PlotItem() - plt.plot(np.random.normal(size=10000)) - processEvents() +#def test_PlotItem(): + #for i in range(10): + #plt = pg.PlotItem() + #plt.plot(np.random.normal(size=10000)) + #processEvents() - ot = pg.debug.ObjTracker() + #ot = pg.debug.ObjTracker() - plots = [] - for i in range(10): - plt = pg.PlotItem() - plt.plot(np.random.normal(size=10000)) - plots.append(plt) - processEvents() + #plots = [] + #for i in range(10): + #plt = pg.PlotItem() + #plt.plot(np.random.normal(size=10000)) + #plots.append(plt) + #processEvents() - ot.diff() + #ot.diff() - del plots - processEvents() + #del plots + #processEvents() - ot.diff() + #ot.diff() - return ot + #return ot +def test_PlotWidget(): + def mkref(*args, **kwds): + iv = pg.PlotWidget(*args, **kwds) + return weakref.ref(iv) + + for i in range(100): + assert mkref()() is None + +def test_ImageView(): + def mkref(*args, **kwds): + iv = pg.ImageView(*args, **kwds) + return weakref.ref(iv) + + for i in range(100): + assert mkref()() is None if __name__ == '__main__': ot = test_PlotItem() diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index 3838af7d..709080ac 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -6,34 +6,146 @@ the tear them down repeatedly. The purpose of this is to attempt to generate segmentation faults. """ +from PyQt4.QtTest import QTest import pyqtgraph as pg -import random +from random import seed, randint +import sys, gc, weakref -random.seed(12345) +app = pg.mkQApp() -widgetTypes = [pg.PlotWidget, pg.ImageView, pg.GraphicsView, pg.QtGui.QWidget, - pg.QtGui.QTreeWidget, pg.QtGui.QPushButton] +seed(12345) -itemTypes = [pg.PlotCurveItem, pg.ImageItem, pg.PlotDataItem, pg.ViewBox, - pg.QtGui.QGraphicsRectItem] +widgetTypes = [ + pg.PlotWidget, + pg.ImageView, + pg.GraphicsView, + pg.QtGui.QWidget, + pg.QtGui.QTreeWidget, + pg.QtGui.QPushButton, + ] + +itemTypes = [ + pg.PlotCurveItem, + pg.ImageItem, + pg.PlotDataItem, + pg.ViewBox, + pg.QtGui.QGraphicsRectItem + ] + +widgets = [] +items = [] +allWidgets = weakref.WeakSet() + +def test_stability(): + global allWidgets + try: + gc.disable() + actions = [ + createWidget, + #setParent, + forgetWidget, + showWidget, + processEvents, + #raiseException, + #addReference, + ] + + thread = WorkThread() + #thread.start() + + while True: + try: + action = randItem(actions) + action() + print('[%d widgets alive]' % len(allWidgets)) + except KeyboardInterrupt: + thread.kill() + break + except: + sys.excepthook(*sys.exc_info()) + finally: + gc.enable() + + + +class WorkThread(pg.QtCore.QThread): + '''Intended to give the gc an opportunity to run from a non-gui thread.''' + def run(self): + i = 0 + while True: + i += 1 + if (i % 1000000) == 0: + print('--worker--') + + +def randItem(items): + return items[randint(0, len(items)-1)] + +def p(msg): + print(msg) + sys.stdout.flush() + +def createWidget(): + p('create widget') + global widgets, allWidgets + widget = randItem(widgetTypes)() + widgets.append(widget) + allWidgets.add(widget) + p(" %s" % widget) + return widget + +def setParent(): + p('set parent') + global widgets + if len(widgets) < 2: + return + child = parent = None + while child is parent: + child = randItem(widgets) + parent = randItem(widgets) + p(" %s parent of %s" % (parent, child)) + child.setParent(parent) + +def forgetWidget(): + p('forget widget') + global widgets + if len(widgets) < 1: + return + widget = randItem(widgets) + p(' %s' % widget) + widgets.remove(widget) + +def showWidget(): + p('show widget') + global widgets + if len(widgets) < 1: + return + widget = randItem(widgets) + p(' %s' % widget) + widget.show() + +def processEvents(): + p('process events') + QTest.qWait(25) + +class TestException(Exception): + pass + +def raiseException(): + p('raise exception') + raise TestException("A test exception") + +def addReference(): + p('add reference') + global widgets + if len(widgets) < 1: + return + obj1 = randItem(widgets) + obj2 = randItem(widgets) + p(' %s -> %s' % (obj1, obj2)) + obj1._testref = obj2 + -while True: - action = random.randint(0,5) - if action == 0: - # create a widget - pass - elif action == 1: - # set parent (widget or None), possibly create a reference in either direction - pass - elif action == 2: - # - pass - elif action == 3: - pass - - - - - - +if __name__ == '__main__': + test_stability() \ No newline at end of file From ef7949a35eca727f3f4d4bc05d684871db640b85 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 13 Apr 2014 22:59:57 -0400 Subject: [PATCH 196/268] Fixed ref cycle in SignalProxy --- pyqtgraph/SignalProxy.py | 5 +++-- pyqtgraph/tests/test_ref_cycles.py | 4 ++-- pyqtgraph/tests/test_stability.py | 10 +++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py index 6f9b9112..d36282fa 100644 --- a/pyqtgraph/SignalProxy.py +++ b/pyqtgraph/SignalProxy.py @@ -2,6 +2,7 @@ from .Qt import QtCore from .ptime import time from . import ThreadsafeTimer +import weakref __all__ = ['SignalProxy'] @@ -34,7 +35,7 @@ class SignalProxy(QtCore.QObject): self.timer = ThreadsafeTimer.ThreadsafeTimer() self.timer.timeout.connect(self.flush) self.block = False - self.slot = slot + self.slot = weakref.ref(slot) self.lastFlushTime = None if slot is not None: self.sigDelayed.connect(slot) @@ -80,7 +81,7 @@ class SignalProxy(QtCore.QObject): except: pass try: - self.sigDelayed.disconnect(self.slot) + self.sigDelayed.disconnect(self.slot()) except: pass diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 87e0d71d..2154632e 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -45,7 +45,7 @@ def test_PlotWidget(): iv = pg.PlotWidget(*args, **kwds) return weakref.ref(iv) - for i in range(100): + for i in range(5): assert mkref()() is None def test_ImageView(): @@ -53,7 +53,7 @@ def test_ImageView(): iv = pg.ImageView(*args, **kwds) return weakref.ref(iv) - for i in range(100): + for i in range(5): assert mkref()() is None if __name__ == '__main__': diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index 709080ac..dfcfb15e 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -41,11 +41,11 @@ def test_stability(): try: gc.disable() actions = [ - createWidget, - #setParent, - forgetWidget, - showWidget, - processEvents, + createWidget, + #setParent, + forgetWidget, + showWidget, + #processEvents, #raiseException, #addReference, ] From d45eadc9a5faaea03fd2d7f911fbb1f9302fae70 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 15 Apr 2014 15:10:04 -0400 Subject: [PATCH 197/268] update stability test --- pyqtgraph/tests/test_stability.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index dfcfb15e..6d9acd0d 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -45,22 +45,27 @@ def test_stability(): #setParent, forgetWidget, showWidget, - #processEvents, + processEvents, #raiseException, #addReference, ] thread = WorkThread() - #thread.start() + thread.start() while True: try: action = randItem(actions) action() - print('[%d widgets alive]' % len(allWidgets)) + print('[%d widgets alive, %d zombie]' % (len(allWidgets), len(allWidgets) - len(widgets))) except KeyboardInterrupt: - thread.kill() - break + print("Caught interrupt; send another to exit.") + try: + for i in range(100): + QTest.qWait(100) + except KeyboardInterrupt: + thread.terminate() + break except: sys.excepthook(*sys.exc_info()) finally: @@ -88,7 +93,10 @@ def p(msg): def createWidget(): p('create widget') global widgets, allWidgets + if len(widgets) > 50: + return widget = randItem(widgetTypes)() + widget.setWindowTitle(widget.__class__.__name__) widgets.append(widget) allWidgets.add(widget) p(" %s" % widget) From 0479507dbbba308e9b416e917cf5aca9310a3164 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 13:07:31 -0400 Subject: [PATCH 198/268] Expanded ref checks Fixed ref cycle in ImageItem -> HistogramLutItem --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 17 +++--- pyqtgraph/tests/test_ref_cycles.py | 66 +++++++++------------ 2 files changed, 36 insertions(+), 47 deletions(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 71577422..6a915902 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -17,6 +17,7 @@ from .. import functions as fn import numpy as np from .. import debug as debug +import weakref __all__ = ['HistogramLUTItem'] @@ -42,7 +43,7 @@ class HistogramLUTItem(GraphicsWidget): """ GraphicsWidget.__init__(self) self.lut = None - self.imageItem = None + self.imageItem = lambda: None # fake a dead weakref self.layout = QtGui.QGraphicsGridLayout() self.setLayout(self.layout) @@ -138,7 +139,7 @@ class HistogramLUTItem(GraphicsWidget): #self.region.setBounds([vr.top(), vr.bottom()]) def setImageItem(self, img): - self.imageItem = img + self.imageItem = weakref.ref(img) img.sigImageChanged.connect(self.imageChanged) img.setLookupTable(self.getLookupTable) ## send function pointer, not the result #self.gradientChanged() @@ -150,11 +151,11 @@ class HistogramLUTItem(GraphicsWidget): self.update() def gradientChanged(self): - if self.imageItem is not None: + if self.imageItem() is not None: if self.gradient.isLookupTrivial(): - self.imageItem.setLookupTable(None) #lambda x: x.astype(np.uint8)) + self.imageItem().setLookupTable(None) #lambda x: x.astype(np.uint8)) else: - self.imageItem.setLookupTable(self.getLookupTable) ## send function pointer, not the result + self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result self.lut = None #if self.imageItem is not None: @@ -178,14 +179,14 @@ class HistogramLUTItem(GraphicsWidget): #self.update() def regionChanging(self): - if self.imageItem is not None: - self.imageItem.setLevels(self.region.getRegion()) + if self.imageItem() is not None: + self.imageItem().setLevels(self.region.getRegion()) self.sigLevelsChanged.emit(self) self.update() def imageChanged(self, autoLevel=False, autoRange=False): profiler = debug.Profiler() - h = self.imageItem.getHistogram() + h = self.imageItem().getHistogram() profiler('get histogram') if h[0] is None: return diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 2154632e..3e78b382 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -7,54 +7,42 @@ import numpy as np import gc, weakref app = pg.mkQApp() -def processEvents(): - for i in range(3): - gc.collect() - app.processEvents() - # processEvents ignored DeferredDelete events; we must process these - # manually. - app.sendPostedEvents(None, pg.QtCore.QEvent.DeferredDelete) +def assert_alldead(refs): + for ref in refs: + assert ref() is None -#def test_PlotItem(): - #for i in range(10): - #plt = pg.PlotItem() - #plt.plot(np.random.normal(size=10000)) - #processEvents() - - #ot = pg.debug.ObjTracker() - - #plots = [] - #for i in range(10): - #plt = pg.PlotItem() - #plt.plot(np.random.normal(size=10000)) - #plots.append(plt) - #processEvents() - - #ot.diff() - - #del plots - #processEvents() - - #ot.diff() - - - #return ot +def mkrefs(*objs): + return map(weakref.ref, objs) def test_PlotWidget(): - def mkref(*args, **kwds): - iv = pg.PlotWidget(*args, **kwds) - return weakref.ref(iv) + def mkobjs(*args, **kwds): + w = pg.PlotWidget(*args, **kwds) + data = pg.np.array([1,5,2,4,3]) + c = w.plot(data, name='stuff') + w.addLegend() + + # test that connections do not keep objects alive + w.plotItem.vb.sigRangeChanged.connect(mkrefs) + app.focusChanged.connect(w.plotItem.vb.invertY) + + # return weakrefs to a bunch of objects that should die when the scope exits. + return mkrefs(w, c, data, w.plotItem, w.plotItem.vb, w.plotItem.getMenu(), w.plotItem.getAxis('left')) for i in range(5): - assert mkref()() is None + assert_alldead(mkobjs()) def test_ImageView(): - def mkref(*args, **kwds): - iv = pg.ImageView(*args, **kwds) - return weakref.ref(iv) + def mkobjs(): + iv = pg.ImageView() + data = np.zeros((10,10,5)) + iv.setImage(data) + + return mkrefs(iv, iv.imageItem, iv.view, iv.ui.histogram, data) for i in range(5): - assert mkref()() is None + assert_alldead(mkobjs()) + + if __name__ == '__main__': ot = test_PlotItem() From 27f24d1a6a6aa512a87e7b74517c22962ca958fd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 13:27:25 -0400 Subject: [PATCH 199/268] Expand ref cycle check to include all child QObjects --- pyqtgraph/tests/test_ref_cycles.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 3e78b382..9e3fee19 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -11,8 +11,27 @@ def assert_alldead(refs): for ref in refs: assert ref() is None +def qObjectTree(root): + """Return root and its entire tree of qobject children""" + childs = [root] + for ch in pg.QtCore.QObject.children(root): + childs += qObjectTree(ch) + return childs + def mkrefs(*objs): - return map(weakref.ref, objs) + """Return a list of weakrefs to each object in *objs. + QObject instances are expanded to include all child objects. + """ + allObjs = {} + for obj in objs: + if isinstance(obj, pg.QtCore.QObject): + obj = qObjectTree(obj) + else: + obj = [obj] + for o in obj: + allObjs[id(o)] = o + + return map(weakref.ref, allObjs.values()) def test_PlotWidget(): def mkobjs(*args, **kwds): From 13cca1d9cab3156845efe99b5f81ab8858cdf076 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 13:44:58 -0400 Subject: [PATCH 200/268] Add GraphicsWindow ref check --- pyqtgraph/tests/test_ref_cycles.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 9e3fee19..0284852c 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -61,7 +61,17 @@ def test_ImageView(): for i in range(5): assert_alldead(mkobjs()) +def test_GraphicsWindow(): + def mkobjs(): + w = pg.GraphicsWindow() + p1 = w.addPlot() + v1 = w.addViewBox() + return mkrefs(w, p1, v1) + + for i in range(5): + assert_alldead(mkobjs()) + if __name__ == '__main__': ot = test_PlotItem() From 24c25b604ac29453c9c999b1d347376f9bbdd088 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 14:02:55 -0400 Subject: [PATCH 201/268] disabled stability test (because it is expected to crash) --- pyqtgraph/tests/test_stability.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index 6d9acd0d..a64e30e4 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -36,7 +36,8 @@ widgets = [] items = [] allWidgets = weakref.WeakSet() -def test_stability(): + +def crashtest(): global allWidgets try: gc.disable() @@ -136,12 +137,12 @@ def processEvents(): p('process events') QTest.qWait(25) -class TestException(Exception): +class TstException(Exception): pass def raiseException(): p('raise exception') - raise TestException("A test exception") + raise TstException("A test exception") def addReference(): p('add reference') From c6f2e9c2a92cbdbf1c58738de9771b80cbbe54b9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 28 Apr 2014 07:36:59 -0400 Subject: [PATCH 202/268] Added code for inverting X axis --- CHANGELOG | 1 + pyqtgraph/graphicsItems/AxisItem.py | 5 +++- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 3 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 29 +++++++++++++++++-- .../graphicsItems/ViewBox/ViewBoxMenu.py | 11 +++---- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b9f51c84..6ae13037 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -48,6 +48,7 @@ pyqtgraph-0.9.9 [unreleased] - Added AxisItem.setStyle() - Added configurable formatting for TableWidget - Added 'stepMode' argument to PlotDataItem() + - Added ViewBox.invertX() Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index c25f7a7f..95093251 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -454,7 +454,10 @@ class AxisItem(GraphicsWidget): else: if newRange is None: newRange = view.viewRange()[0] - self.setRange(*newRange) + if view.xInverted(): + self.setRange(*newRange[::-1]) + else: + self.setRange(*newRange) def boundingRect(self): linkedView = self.linkedView() diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 5c102d95..8292875c 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -78,6 +78,7 @@ class PlotItem(GraphicsWidget): :func:`disableAutoRange `, :func:`setAspectLocked `, :func:`invertY `, + :func:`invertX `, :func:`register `, :func:`unregister ` @@ -299,7 +300,7 @@ class PlotItem(GraphicsWidget): for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE: 'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please 'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', # update the class docstring - 'setAspectLocked', 'invertY', 'register', 'unregister']: # as well. + 'setAspectLocked', 'invertY', 'invertX', 'register', 'unregister']: # as well. def _create_method(name): def method(self, *args, **kwargs): diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 46ed2984..0c5792e4 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -104,7 +104,7 @@ class ViewBox(GraphicsWidget): NamedViews = weakref.WeakValueDictionary() # name: ViewBox AllViews = weakref.WeakKeyDictionary() # ViewBox: None - def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None): + def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None, invertX=False): """ ============== ============================================================= **Arguments:** @@ -115,6 +115,7 @@ class ViewBox(GraphicsWidget): coorinates to. (or False to allow the ratio to change) *enableMouse* (bool) Whether mouse can be used to scale/pan the view *invertY* (bool) See :func:`invertY ` + *invertX* (bool) See :func:`invertX ` ============== ============================================================= """ @@ -139,6 +140,7 @@ class ViewBox(GraphicsWidget): 'viewRange': [[0,1], [0,1]], ## actual range viewed 'yInverted': invertY, + 'xInverted': invertX, 'aspectLocked': False, ## False if aspect is unlocked, otherwise float specifies the locked ratio. 'autoRange': [True, True], ## False if auto range is disabled, ## otherwise float gives the fraction of data that is visible @@ -996,7 +998,10 @@ class ViewBox(GraphicsWidget): x2 = vr.right() else: ## views overlap; line them up upp = float(vr.width()) / vg.width() - x1 = vr.left() + (sg.x()-vg.x()) * upp + if self.xInverted(): + x1 = vr.left() + (sg.right()-vg.right()) * upp + else: + x1 = vr.left() + (sg.x()-vg.x()) * upp x2 = x1 + sg.width() * upp self.enableAutoRange(ViewBox.XAxis, False) self.setXRange(x1, x2, padding=0) @@ -1054,10 +1059,27 @@ class ViewBox(GraphicsWidget): #self.updateMatrix(changed=(False, True)) self.updateViewRange() self.sigStateChanged.emit(self) + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) def yInverted(self): return self.state['yInverted'] + def invertX(self, b=True): + """ + By default, the positive x-axis points rightward on the screen. Use invertX(True) to reverse the x-axis. + """ + if self.state['xInverted'] == b: + return + + self.state['xInverted'] = b + #self.updateMatrix(changed=(False, True)) + self.updateViewRange() + self.sigStateChanged.emit(self) + self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) + + def xInverted(self): + return self.state['xInverted'] + def setAspectLocked(self, lock=True, ratio=1): """ If the aspect ratio is locked, view scaling must always preserve the aspect ratio. @@ -1555,6 +1577,7 @@ class ViewBox(GraphicsWidget): if link is not None: link.linkedViewChanged(self, ax) + self.update() self._matrixNeedsUpdate = True def updateMatrix(self, changed=None): @@ -1567,6 +1590,8 @@ class ViewBox(GraphicsWidget): scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height()) if not self.state['yInverted']: scale = scale * Point(1, -1) + if self.state['xInverted']: + scale = scale * Point(-1, 1) m = QtGui.QTransform() ## First center the viewport at 0 diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index af142771..0e7d7912 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -56,7 +56,7 @@ class ViewBoxMenu(QtGui.QMenu): for sig, fn in connects: sig.connect(getattr(self, axis.lower()+fn)) - self.ctrl[0].invertCheck.hide() ## no invert for x-axis + self.ctrl[0].invertCheck.toggled.connect(self.xInvertToggled) self.ctrl[1].invertCheck.toggled.connect(self.yInvertToggled) ## exporting is handled by GraphicsScene now #self.export = QtGui.QMenu("Export") @@ -139,8 +139,9 @@ class ViewBoxMenu(QtGui.QMenu): self.ctrl[i].autoPanCheck.setChecked(state['autoPan'][i]) self.ctrl[i].visibleOnlyCheck.setChecked(state['autoVisibleOnly'][i]) - - self.ctrl[1].invertCheck.setChecked(state['yInverted']) + xy = ['x', 'y'][i] + self.ctrl[i].invertCheck.setChecked(state.get(xy+'Inverted', False)) + self.valid = True def popup(self, *args): @@ -217,19 +218,19 @@ class ViewBoxMenu(QtGui.QMenu): def yInvertToggled(self, b): self.view().invertY(b) + def xInvertToggled(self, b): + self.view().invertX(b) def exportMethod(self): act = self.sender() self.exportMethods[str(act.text())]() - def set3ButtonMode(self): self.view().setLeftButtonAction('pan') def set1ButtonMode(self): self.view().setLeftButtonAction('rect') - def setViewList(self, views): names = [''] self.viewMap.clear() From f202d5ab8b635698c972c0730ee86f7311b36463 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 28 Apr 2014 08:19:53 -0400 Subject: [PATCH 203/268] Update AxisItem.setGrid docstring --- pyqtgraph/graphicsItems/AxisItem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 95093251..af393fdc 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -161,7 +161,11 @@ class AxisItem(GraphicsWidget): self.scene().removeItem(self) def setGrid(self, grid): - """Set the alpha value for the grid, or False to disable.""" + """Set the alpha value (0-255) for the grid, or False to disable. + + When grid lines are enabled, the axis tick lines are extended to cover + the extent of the linked ViewBox, if any. + """ self.grid = grid self.picture = None self.prepareGeometryChange() From 7b862f4f87af830d02a0e544217521d62aaad789 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 28 Apr 2014 08:22:07 -0400 Subject: [PATCH 204/268] docstring indentation fix --- pyqtgraph/graphicsItems/AxisItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index af393fdc..5eef4ae0 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -233,7 +233,7 @@ class AxisItem(GraphicsWidget): without any scaling prefix (eg, 'V' instead of 'mV'). The scaling prefix will be automatically prepended based on the range of data displayed. - **args All extra keyword arguments become CSS style options for + **args All extra keyword arguments become CSS style options for the tag which will surround the axis label and units. ============== ============================================================= From 1729416914707e88607cddcaaf6e88dbdaf333a3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 29 Apr 2014 17:17:05 -0400 Subject: [PATCH 205/268] Updated ImageView and ViewBox documentation --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 23 +++++---- pyqtgraph/imageview/ImageView.py | 55 ++++++++++++++++------ 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 0c5792e4..cf9e7f4a 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -74,12 +74,11 @@ class ViewBox(GraphicsWidget): Features: - - Scaling contents by mouse or auto-scale when contents change - - View linking--multiple views display the same data ranges - - Configurable by context menu - - Item coordinate mapping methods + * Scaling contents by mouse or auto-scale when contents change + * View linking--multiple views display the same data ranges + * Configurable by context menu + * Item coordinate mapping methods - Not really compatible with GraphicsView having the same functionality. """ sigYRangeChanged = QtCore.Signal(object, object) @@ -116,11 +115,15 @@ class ViewBox(GraphicsWidget): *enableMouse* (bool) Whether mouse can be used to scale/pan the view *invertY* (bool) See :func:`invertY ` *invertX* (bool) See :func:`invertX ` + *enableMenu* (bool) Whether to display a context menu when + right-clicking on the ViewBox background. + *name* (str) Used to register this ViewBox so that it appears + in the "Link axis" dropdown inside other ViewBox + context menus. This allows the user to manually link + the axes of any other view to this one. ============== ============================================================= """ - - GraphicsWidget.__init__(self, parent) self.name = None self.linksBlocked = False @@ -220,7 +223,11 @@ class ViewBox(GraphicsWidget): def register(self, name): """ Add this ViewBox to the registered list of views. - *name* will appear in the drop-down lists for axis linking in all other views. + + This allows users to manually link the axes of any other ViewBox to + this one. The specified *name* will appear in the drop-down lists for + axis linking in the context menus of all other views. + The same can be accomplished by initializing the ViewBox with the *name* attribute. """ ViewBox.AllViews[self] = None diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index c7c3206e..c9f421b4 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -12,8 +12,10 @@ Widget used for displaying 2D or 3D data. Features: - ROI plotting - Image normalization through a variety of methods """ -from ..Qt import QtCore, QtGui, USE_PYSIDE +import sys +import numpy as np +from ..Qt import QtCore, QtGui, USE_PYSIDE if USE_PYSIDE: from .ImageViewTemplate_pyside import * else: @@ -24,25 +26,14 @@ from ..graphicsItems.ROI import * from ..graphicsItems.LinearRegionItem import * from ..graphicsItems.InfiniteLine import * from ..graphicsItems.ViewBox import * -#from widgets import ROI -import sys -#from numpy import ndarray from .. import ptime as ptime -import numpy as np from .. import debug as debug - from ..SignalProxy import SignalProxy try: from bottleneck import nanmin, nanmax except ImportError: from numpy import nanmin, nanmax - -#try: - #from .. import metaarray as metaarray - #HAVE_METAARRAY = True -#except: - #HAVE_METAARRAY = False class PlotROI(ROI): @@ -72,6 +63,16 @@ class ImageView(QtGui.QWidget): imv = pg.ImageView() imv.show() imv.setImage(data) + + **Keyboard interaction** + + * left/right arrows step forward/backward 1 frame when pressed, + seek at 20fps when held. + * up/down arrows seek at 100fps + * pgup/pgdn seek at 1000fps + * home/end seek immediately to the first/last frame + * space begins playing frames. If time values (in seconds) are given + for each frame, then playback is in realtime. """ sigTimeChanged = QtCore.Signal(object, object) sigProcessingChanged = QtCore.Signal(object) @@ -79,8 +80,31 @@ class ImageView(QtGui.QWidget): def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args): """ By default, this class creates an :class:`ImageItem ` to display image data - and a :class:`ViewBox ` to contain the ImageItem. Custom items may be given instead - by specifying the *view* and/or *imageItem* arguments. + and a :class:`ViewBox ` to contain the ImageItem. + + ============= ========================================================= + **Arguments** + parent (QWidget) Specifies the parent widget to which + this ImageView will belong. If None, then the ImageView + is created with no parent. + name (str) The name used to register both the internal ViewBox + and the PlotItem used to display ROI data. See the *name* + argument to :func:`ViewBox.__init__() + `. + view (ViewBox or PlotItem) If specified, this will be used + as the display area that contains the displayed image. + Any :class:`ViewBox `, + :class:`PlotItem `, or other + compatible object is acceptable. + imageItem (ImageItem) If specified, this object will be used to + display the image. Must be an instance of ImageItem + or other compatible object. + ============= ========================================================= + + Note: to display axis ticks inside the ImageView, instantiate it + with a PlotItem instance as its view:: + + pg.ImageView(view=pg.PlotItem()) """ QtGui.QWidget.__init__(self, parent, *args) self.levelMax = 4096 @@ -165,6 +189,7 @@ class ImageView(QtGui.QWidget): self.normRoi.sigRegionChangeFinished.connect(self.updateNorm) self.ui.roiPlot.registerPlot(self.name + '_ROI') + self.view.register(self.name) self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown] @@ -318,7 +343,7 @@ class ImageView(QtGui.QWidget): self.ui.histogram.setLevels(min, max) def autoRange(self): - """Auto scale and pan the view around the image.""" + """Auto scale and pan the view around the image such that the image fills the view.""" image = self.getProcessedImage() self.view.autoRange() From f18f2b11c8cd376d9947712c928cd0024f45825f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 30 Apr 2014 13:00:56 -0400 Subject: [PATCH 206/268] Fix: TextParameterItem now obeys 'readonly' option --- pyqtgraph/parametertree/parameterTypes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 1f3eb692..53abe429 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -636,6 +636,7 @@ class TextParameterItem(WidgetParameterItem): def makeWidget(self): self.textBox = QtGui.QTextEdit() self.textBox.setMaximumHeight(100) + self.textBox.setReadOnly(self.param.opts.get('readonly', False)) self.textBox.value = lambda: str(self.textBox.toPlainText()) self.textBox.setValue = self.textBox.setPlainText self.textBox.sigChanged = self.textBox.textChanged From de022be634677cc20f1e94f6b662de1ccb5fbd57 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 4 May 2014 12:24:46 -0400 Subject: [PATCH 207/268] Fixed Parameter 'readonly' option for bool, color, and text parameter types --- CHANGELOG | 1 + pyqtgraph/parametertree/parameterTypes.py | 7 +++++++ .../parametertree/tests/test_parametertypes.py | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 pyqtgraph/parametertree/tests/test_parametertypes.py diff --git a/CHANGELOG b/CHANGELOG index 6ae13037..e0723ca5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -86,6 +86,7 @@ pyqtgraph-0.9.9 [unreleased] - Fixed TableWidget append / sort issues - Fixed AxisItem not resizing text area when setTicks() is used - Removed a few cyclic references + - Fixed Parameter 'readonly' option for bool, color, and text parameter types pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 53abe429..62e935fd 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -125,6 +125,7 @@ class WidgetParameterItem(ParameterItem): w.sigChanged = w.toggled w.value = w.isChecked w.setValue = w.setChecked + w.setEnabled(not opts.get('readonly', False)) self.hideWidget = False elif t == 'str': w = QtGui.QLineEdit() @@ -140,6 +141,7 @@ class WidgetParameterItem(ParameterItem): w.setValue = w.setColor self.hideWidget = False w.setFlat(True) + w.setEnabled(not opts.get('readonly', False)) elif t == 'colormap': from ..widgets.GradientWidget import GradientWidget ## need this here to avoid import loop w = GradientWidget(orientation='bottom') @@ -274,6 +276,8 @@ class WidgetParameterItem(ParameterItem): if 'readonly' in opts: self.updateDefaultBtn() + if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)): + w.setEnabled(not opts['readonly']) ## If widget is a SpinBox, pass options straight through if isinstance(self.widget, SpinBox): @@ -281,6 +285,9 @@ class WidgetParameterItem(ParameterItem): opts['suffix'] = opts['units'] self.widget.setOpts(**opts) self.updateDisplayLabel() + + + class EventProxy(QtCore.QObject): def __init__(self, qobj, callback): diff --git a/pyqtgraph/parametertree/tests/test_parametertypes.py b/pyqtgraph/parametertree/tests/test_parametertypes.py new file mode 100644 index 00000000..c7cd2cb3 --- /dev/null +++ b/pyqtgraph/parametertree/tests/test_parametertypes.py @@ -0,0 +1,18 @@ +import pyqtgraph.parametertree as pt +import pyqtgraph as pg +app = pg.mkQApp() + +def test_opts(): + paramSpec = [ + dict(name='bool', type='bool', readonly=True), + dict(name='color', type='color', readonly=True), + ] + + param = pt.Parameter.create(name='params', type='group', children=paramSpec) + tree = pt.ParameterTree() + tree.setParameters(param) + + assert param.param('bool').items.keys()[0].widget.isEnabled() is False + assert param.param('color').items.keys()[0].widget.isEnabled() is False + + From 1e0034904ef4d48c860eed083f8c01bc04f4178b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 5 May 2014 11:16:00 -0400 Subject: [PATCH 208/268] Initialize drag variable in Dock.py --- pyqtgraph/dockarea/Dock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index d3cfcbb6..99808eee 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -244,6 +244,7 @@ class DockLabel(VerticalLabel): sigClicked = QtCore.Signal(object, object) def __init__(self, text, dock): + self.startedDrag = False self.dim = False self.fixedWidth = False VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False) From 4c37b75afeee0dc5b75e51786191e5f6bd25e9fb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 7 May 2014 12:49:30 -0400 Subject: [PATCH 209/268] Added Dock close button (from Stefan H) --- pyqtgraph/dockarea/Dock.py | 97 ++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 99808eee..b124b125 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -8,11 +8,13 @@ class Dock(QtGui.QWidget, DockDrop): sigStretchChanged = QtCore.Signal() - def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True): + def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closeable = False): QtGui.QWidget.__init__(self) DockDrop.__init__(self) self.area = area - self.label = DockLabel(name, self) + self.label = DockLabel(name, self, closeable) + if closeable: + self.label.sigCloseClicked.connect(self.close) self.labelHidden = False self.moveLabel = True ## If false, the dock is no longer allowed to move the label. self.autoOrient = autoOrientation @@ -35,30 +37,30 @@ class Dock(QtGui.QWidget, DockDrop): #self.titlePos = 'top' self.raiseOverlay() self.hStyle = """ - Dock > QWidget { - border: 1px solid #000; - border-radius: 5px; - border-top-left-radius: 0px; - border-top-right-radius: 0px; + Dock > QWidget { + border: 1px solid #000; + border-radius: 5px; + border-top-left-radius: 0px; + border-top-right-radius: 0px; border-top-width: 0px; }""" self.vStyle = """ - Dock > QWidget { - border: 1px solid #000; - border-radius: 5px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; + Dock > QWidget { + border: 1px solid #000; + border-radius: 5px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; border-left-width: 0px; }""" self.nStyle = """ - Dock > QWidget { - border: 1px solid #000; - border-radius: 5px; + Dock > QWidget { + border: 1px solid #000; + border-radius: 5px; }""" self.dragStyle = """ - Dock > QWidget { - border: 4px solid #00F; - border-radius: 5px; + Dock > QWidget { + border: 4px solid #00F; + border-radius: 5px; }""" self.setAutoFillBackground(False) self.widgetArea.setStyleSheet(self.hStyle) @@ -79,7 +81,7 @@ class Dock(QtGui.QWidget, DockDrop): def setStretch(self, x=None, y=None): """ - Set the 'target' size for this Dock. + Set the 'target' size for this Dock. The actual size will be determined by comparing this Dock's stretch value to the rest of the docks it shares space with. """ @@ -130,7 +132,7 @@ class Dock(QtGui.QWidget, DockDrop): Sets the orientation of the title bar for this Dock. Must be one of 'auto', 'horizontal', or 'vertical'. By default ('auto'), the orientation is determined - based on the aspect ratio of the Dock. + based on the aspect ratio of the Dock. """ #print self.name(), "setOrientation", o, force if o == 'auto' and self.autoOrient: @@ -175,7 +177,7 @@ class Dock(QtGui.QWidget, DockDrop): def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1): """ - Add a new widget to the interior of this Dock. + Add a new widget to the interior of this Dock. Each Dock uses a QGridLayout to arrange widgets within. """ if row is None: @@ -242,9 +244,9 @@ class Dock(QtGui.QWidget, DockDrop): class DockLabel(VerticalLabel): sigClicked = QtCore.Signal(object, object) + sigCloseClicked = QtCore.Signal() - def __init__(self, text, dock): - self.startedDrag = False + def __init__(self, text, dock, showCloseButton): self.dim = False self.fixedWidth = False VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False) @@ -252,6 +254,13 @@ class DockLabel(VerticalLabel): self.dock = dock self.updateStyle() self.setAutoFillBackground(False) + self.startedDrag = False + + self.closeButton = None + if showCloseButton: + self.closeButton = QtGui.QToolButton(self) + self.closeButton.pressed.connect(self.sigCloseClicked) + self.closeButton.setIcon(QtGui.QApplication.style().standardIcon(QtGui.QStyle.SP_TitleBarCloseButton)) #def minimumSizeHint(self): ##sh = QtGui.QWidget.minimumSizeHint(self) @@ -269,28 +278,28 @@ class DockLabel(VerticalLabel): border = '#55B' if self.orientation == 'vertical': - self.vStyle = """DockLabel { - background-color : %s; - color : %s; - border-top-right-radius: 0px; - border-top-left-radius: %s; - border-bottom-right-radius: 0px; - border-bottom-left-radius: %s; - border-width: 0px; + self.vStyle = """DockLabel { + background-color : %s; + color : %s; + border-top-right-radius: 0px; + border-top-left-radius: %s; + border-bottom-right-radius: 0px; + border-bottom-left-radius: %s; + border-width: 0px; border-right: 2px solid %s; padding-top: 3px; padding-bottom: 3px; }""" % (bg, fg, r, r, border) self.setStyleSheet(self.vStyle) else: - self.hStyle = """DockLabel { - background-color : %s; - color : %s; - border-top-right-radius: %s; - border-top-left-radius: %s; - border-bottom-right-radius: 0px; - border-bottom-left-radius: 0px; - border-width: 0px; + self.hStyle = """DockLabel { + background-color : %s; + color : %s; + border-top-right-radius: %s; + border-top-left-radius: %s; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; + border-width: 0px; border-bottom: 2px solid %s; padding-left: 3px; padding-right: 3px; @@ -336,5 +345,11 @@ class DockLabel(VerticalLabel): #VerticalLabel.paintEvent(self, ev) - - + def resizeEvent (self, ev): + if self.closeButton: + if self.orientation == 'vertical': + closeButtonSize = ev.size().width() + else: + closeButtonSize = ev.size().height() + self.closeButton.setFixedSize(QtCore.QSize(closeButtonSize,closeButtonSize)) + super(DockLabel,self).resizeEvent(ev) From 51f0a063ee6ea5e0180bf71f75581f803de7ee65 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 May 2014 09:50:26 -0400 Subject: [PATCH 210/268] minor cleanups --- examples/dockarea.py | 2 +- pyqtgraph/dockarea/Dock.py | 32 +++++++++++--------------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/examples/dockarea.py b/examples/dockarea.py index 2b33048d..9cc79f1b 100644 --- a/examples/dockarea.py +++ b/examples/dockarea.py @@ -35,7 +35,7 @@ win.setWindowTitle('pyqtgraph example: dockarea') ## Note that size arguments are only a suggestion; docks will still have to ## fill the entire dock area and obey the limits of their internal widgets. d1 = Dock("Dock1", size=(1, 1)) ## give this dock the minimum possible size -d2 = Dock("Dock2 - Console", size=(500,300)) +d2 = Dock("Dock2 - Console", size=(500,300), closable=True) d3 = Dock("Dock3", size=(500,400)) d4 = Dock("Dock4 (tabbed) - Plot", size=(500,200)) d5 = Dock("Dock5 - Image", size=(500,200)) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index b124b125..28d4244b 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -8,12 +8,12 @@ class Dock(QtGui.QWidget, DockDrop): sigStretchChanged = QtCore.Signal() - def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closeable = False): + def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closable=False): QtGui.QWidget.__init__(self) DockDrop.__init__(self) self.area = area - self.label = DockLabel(name, self, closeable) - if closeable: + self.label = DockLabel(name, self, closable) + if closable: self.label.sigCloseClicked.connect(self.close) self.labelHidden = False self.moveLabel = True ## If false, the dock is no longer allowed to move the label. @@ -241,6 +241,7 @@ class Dock(QtGui.QWidget, DockDrop): def dropEvent(self, *args): DockDrop.dropEvent(self, *args) + class DockLabel(VerticalLabel): sigClicked = QtCore.Signal(object, object) @@ -259,13 +260,9 @@ class DockLabel(VerticalLabel): self.closeButton = None if showCloseButton: self.closeButton = QtGui.QToolButton(self) - self.closeButton.pressed.connect(self.sigCloseClicked) + self.closeButton.clicked.connect(self.sigCloseClicked) self.closeButton.setIcon(QtGui.QApplication.style().standardIcon(QtGui.QStyle.SP_TitleBarCloseButton)) - #def minimumSizeHint(self): - ##sh = QtGui.QWidget.minimumSizeHint(self) - #return QtCore.QSize(20, 20) - def updateStyle(self): r = '3px' if self.dim: @@ -325,11 +322,9 @@ class DockLabel(VerticalLabel): if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance(): self.dock.startDrag() ev.accept() - #print ev.pos() def mouseReleaseEvent(self, ev): if not self.startedDrag: - #self.emit(QtCore.SIGNAL('clicked'), self, ev) self.sigClicked.emit(self, ev) ev.accept() @@ -337,19 +332,14 @@ class DockLabel(VerticalLabel): if ev.button() == QtCore.Qt.LeftButton: self.dock.float() - #def paintEvent(self, ev): - #p = QtGui.QPainter(self) - ##p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 200))) - #p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 100))) - #p.drawRect(self.rect().adjusted(0, 0, -1, -1)) - - #VerticalLabel.paintEvent(self, ev) - def resizeEvent (self, ev): if self.closeButton: if self.orientation == 'vertical': - closeButtonSize = ev.size().width() + size = ev.size().width() + pos = QtCore.QPoint(0, 0) else: - closeButtonSize = ev.size().height() - self.closeButton.setFixedSize(QtCore.QSize(closeButtonSize,closeButtonSize)) + size = ev.size().height() + pos = QtCore.QPoint(ev.size().width() - size, 0) + self.closeButton.setFixedSize(QtCore.QSize(size, size)) + self.closeButton.move(pos) super(DockLabel,self).resizeEvent(ev) From 23cfdf7239a9afd4501aebf84a404ca2021d3b80 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 May 2014 10:37:23 -0400 Subject: [PATCH 211/268] fixed dock insert order bug --- pyqtgraph/dockarea/Container.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/dockarea/Container.py b/pyqtgraph/dockarea/Container.py index 277375f3..c3225edf 100644 --- a/pyqtgraph/dockarea/Container.py +++ b/pyqtgraph/dockarea/Container.py @@ -22,6 +22,9 @@ class Container(object): return None def insert(self, new, pos=None, neighbor=None): + # remove from existing parent first + new.setParent(None) + if not isinstance(new, list): new = [new] if neighbor is None: From 89b0a91c86419d2d321cd90966ce23e90453a22f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 May 2014 10:40:58 -0400 Subject: [PATCH 212/268] Update contrib list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b585a6bd..2dff1031 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Contributors * Fabio Zadrozny * Mikhail Terekhov * Pietro Zambelli + * Stefan Holzmann Requirements ------------ From f30c1a59d18298c7efb392e0372e68c03fa9f9c1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 May 2014 22:37:08 -0400 Subject: [PATCH 213/268] Fixed GLScatterPlotItem to allow opaque spots --- CHANGELOG | 3 +++ pyqtgraph/opengl/items/GLScatterPlotItem.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index fd6589d4..a31b356f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,8 @@ pyqtgraph-0.9.9 [unreleased] - GLViewWidget.itemsAt() now measures y from top of widget to match mouse event position. - Made setPen() methods consistent throughout the package + - Fix in GLScatterPlotItem requires that points will appear slightly more opaque + (so you may need to adjust to lower alpha to achieve the same results) New Features: - Added ViewBox.setLimits() method @@ -88,6 +90,7 @@ pyqtgraph-0.9.9 [unreleased] - Fixed AxisItem not resizing text area when setTicks() is used - Removed a few cyclic references - Fixed Parameter 'readonly' option for bool, color, and text parameter types + - Fixed alpha on GLScatterPlotItem spots (formerly maxed out at alpha=200) pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index bb2c89a3..6cfcc6aa 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -59,7 +59,7 @@ class GLScatterPlotItem(GLGraphicsItem): w = 64 def fn(x,y): r = ((x-w/2.)**2 + (y-w/2.)**2) ** 0.5 - return 200 * (w/2. - np.clip(r, w/2.-1.0, w/2.)) + return 255 * (w/2. - np.clip(r, w/2.-1.0, w/2.)) pData = np.empty((w, w, 4)) pData[:] = 255 pData[:,:,3] = np.fromfunction(fn, pData.shape[:2]) From 8ef2cb7e48276d23fda5c809525439c1a66141aa Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Thu, 8 May 2014 23:01:53 -0400 Subject: [PATCH 214/268] CSVExporter: fix the case when stepMode=True --- pyqtgraph/exporters/CSVExporter.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index b0cf5af5..8cd089d1 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -53,9 +53,13 @@ class CSVExporter(Exporter): for i in range(numRows): for d in data: if i < len(d[0]): - fd.write(numFormat % d[0][i] + sep + numFormat % d[1][i] + sep) + fd.write(numFormat % d[0][i] + sep) else: - fd.write(' %s %s' % (sep, sep)) + fd.write(' %s' % sep) + if i < len(d[1]): + fd.write(numFormat % d[1][i] + sep) + else: + fd.write(' %s' % sep) fd.write('\n') fd.close() From 6a4a653989a379d3e02728f52797e56fbd99d4a6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 10 May 2014 14:19:27 -0400 Subject: [PATCH 215/268] Added InfiniteLine.setHoverPen --- CHANGELOG | 1 + pyqtgraph/graphicsItems/InfiniteLine.py | 48 ++++++++++++------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a31b356f..1a1ba126 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -52,6 +52,7 @@ pyqtgraph-0.9.9 [unreleased] - Added 'stepMode' argument to PlotDataItem() - Added ViewBox.invertX() - Docks now have optional close button + - Added InfiniteLine.setHoverPen Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index dfe2a4c1..8108c3cf 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -59,7 +59,9 @@ class InfiniteLine(GraphicsObject): if pen is None: pen = (200, 200, 100) + self.setPen(pen) + self.setHoverPen(color=(255,0,0), width=self.pen.width()) self.currentPen = self.pen #self.setFlag(self.ItemSendsScenePositionChanges) @@ -77,8 +79,22 @@ class InfiniteLine(GraphicsObject): """Set the pen for drawing the line. Allowable arguments are any that are valid for :func:`mkPen `.""" self.pen = fn.mkPen(*args, **kwargs) - self.currentPen = self.pen - self.update() + if not self.mouseHovering: + self.currentPen = self.pen + self.update() + + def setHoverPen(self, *args, **kwargs): + """Set the pen for drawing the line while the mouse hovers over it. + Allowable arguments are any that are valid + for :func:`mkPen `. + + If the line is not movable, then hovering is also disabled. + + Added in version 0.9.9.""" + self.hoverPen = fn.mkPen(*args, **kwargs) + if self.mouseHovering: + self.currentPen = self.hoverPen + self.update() def setAngle(self, angle): """ @@ -168,8 +184,9 @@ class InfiniteLine(GraphicsObject): px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line if px is None: px = 0 - br.setBottom(-px*4) - br.setTop(px*4) + w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px + br.setBottom(-w) + br.setTop(w) return br.normalized() def paint(self, p, *args): @@ -183,25 +200,6 @@ class InfiniteLine(GraphicsObject): return None ## x axis should never be auto-scaled else: return (0,0) - - #def mousePressEvent(self, ev): - #if self.movable and ev.button() == QtCore.Qt.LeftButton: - #ev.accept() - #self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) - #else: - #ev.ignore() - - #def mouseMoveEvent(self, ev): - #self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) - ##self.emit(QtCore.SIGNAL('dragged'), self) - #self.sigDragged.emit(self) - #self.hasMoved = True - - #def mouseReleaseEvent(self, ev): - #if self.hasMoved and ev.button() == QtCore.Qt.LeftButton: - #self.hasMoved = False - ##self.emit(QtCore.SIGNAL('positionChangeFinished'), self) - #self.sigPositionChangeFinished.emit(self) def mouseDragEvent(self, ev): if self.movable and ev.button() == QtCore.Qt.LeftButton: @@ -239,12 +237,12 @@ class InfiniteLine(GraphicsObject): self.setMouseHover(False) def setMouseHover(self, hover): - ## Inform the item that the mouse is(not) hovering over it + ## Inform the item that the mouse is (not) hovering over it if self.mouseHovering == hover: return self.mouseHovering = hover if hover: - self.currentPen = fn.mkPen(255, 0,0) + self.currentPen = self.hoverPen else: self.currentPen = self.pen self.update() From 88c55c9f986a11c9787112fe118d76da57a03d2b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 10 May 2014 15:30:51 -0400 Subject: [PATCH 216/268] Docstring updates for ParameterTree --- pyqtgraph/parametertree/ParameterTree.py | 43 +++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/parametertree/ParameterTree.py b/pyqtgraph/parametertree/ParameterTree.py index 953f3bb7..ef7c1030 100644 --- a/pyqtgraph/parametertree/ParameterTree.py +++ b/pyqtgraph/parametertree/ParameterTree.py @@ -7,9 +7,16 @@ from .ParameterItem import ParameterItem class ParameterTree(TreeWidget): - """Widget used to display or control data from a ParameterSet""" + """Widget used to display or control data from a hierarchy of Parameters""" def __init__(self, parent=None, showHeader=True): + """ + ============== ======================================================== + **Arguments:** + parent (QWidget) An optional parent widget + showHeader (bool) If True, then the QTreeView header is displayed. + ============== ======================================================== + """ TreeWidget.__init__(self, parent) self.setVerticalScrollMode(self.ScrollPerPixel) self.setHorizontalScrollMode(self.ScrollPerPixel) @@ -25,10 +32,35 @@ class ParameterTree(TreeWidget): self.setRootIsDecorated(False) def setParameters(self, param, showTop=True): + """ + Set the top-level :class:`Parameter ` + to be displayed in this ParameterTree. + + If *showTop* is False, then the top-level parameter is hidden and only + its children will be visible. This is a convenience method equivalent + to:: + + tree.clear() + tree.addParameters(param, showTop) + """ self.clear() self.addParameters(param, showTop=showTop) def addParameters(self, param, root=None, depth=0, showTop=True): + """ + Adds one top-level :class:`Parameter ` + to the view. + + ============== ========================================================== + **Arguments:** + param The :class:`Parameter ` + to add. + root The item within the tree to which *param* should be added. + By default, *param* is added as a top-level item. + showTop If False, then *param* will be hidden, and only its + children will be visible in the tree. + ============== ========================================================== + """ item = param.makeTreeItem(depth=depth) if root is None: root = self.invisibleRootItem() @@ -45,11 +77,14 @@ class ParameterTree(TreeWidget): self.addParameters(ch, root=item, depth=depth+1) def clear(self): - self.invisibleRootItem().takeChildren() - + """ + Remove all parameters from the tree. + """ + self.invisibleRootItem().takeChildren() def focusNext(self, item, forward=True): - ## Give input focus to the next (or previous) item after 'item' + """Give input focus to the next (or previous) item after *item* + """ while True: parent = item.parent() if parent is None: From ab411012f8d0c8b0e72cfc7c3dc181d11d6fbf47 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 18 May 2014 17:57:16 -0400 Subject: [PATCH 217/268] Added some ViewBox unit tests, fixed minor API bug --- pyqtgraph/Qt.py | 8 ++ pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 10 ++- .../ViewBox/tests/test_ViewBox.py | 85 +++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 4fe8c3ab..efbe66c4 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -33,6 +33,10 @@ else: if USE_PYSIDE: from PySide import QtGui, QtCore, QtOpenGL, QtSvg + try: + from PySide import QtTest + except ImportError: + pass import PySide try: from PySide import shiboken @@ -106,6 +110,10 @@ else: from PyQt4 import QtOpenGL except ImportError: pass + try: + from PyQt4 import QtTest + except ImportError: + pass import sip diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index cf9e7f4a..3fa079f2 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -671,7 +671,10 @@ class ViewBox(GraphicsWidget): Added in version 0.9.9 """ update = False - + allowed = ['xMin', 'xMax', 'yMin', 'yMax', 'minXRange', 'maxXRange', 'minYRange', 'maxYRange'] + for kwd in kwds: + if kwd not in allowed: + raise ValueError("Invalid keyword argument '%s'." % kwd) #for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']: #if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: #self.state['limits'][kwd] = kwds[kwd] @@ -1511,7 +1514,8 @@ class ViewBox(GraphicsWidget): if dx != 0: changed[0] = True viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] - + + # ----------- Make corrections for view limits ----------- limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits']) @@ -1562,7 +1566,7 @@ class ViewBox(GraphicsWidget): changed[axis] = True #print "after applying edge limits:", viewRange[axis] - + changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py new file mode 100644 index 00000000..7cb366c2 --- /dev/null +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -0,0 +1,85 @@ +#import PySide +import pyqtgraph as pg + +app = pg.mkQApp() +qtest = pg.Qt.QtTest.QTest + +def assertMapping(vb, r1, r2): + assert vb.mapFromView(r1.topLeft()) == r2.topLeft() + assert vb.mapFromView(r1.bottomLeft()) == r2.bottomLeft() + assert vb.mapFromView(r1.topRight()) == r2.topRight() + assert vb.mapFromView(r1.bottomRight()) == r2.bottomRight() + +def test_ViewBox(): + global app, win, vb + QRectF = pg.QtCore.QRectF + + win = pg.GraphicsWindow() + win.ci.layout.setContentsMargins(0,0,0,0) + win.resize(200, 200) + win.show() + vb = win.addViewBox() + + vb.setRange(xRange=[0, 10], yRange=[0, 10], padding=0) + + # required to make mapFromView work properly. + qtest.qWaitForWindowShown(win) + vb.update() + + g = pg.GridItem() + vb.addItem(g) + + app.processEvents() + + w = vb.geometry().width() + h = vb.geometry().height() + view1 = QRectF(0, 0, 10, 10) + size1 = QRectF(0, h, w, -h) + assertMapping(vb, view1, size1) + + # test resize + win.resize(400, 400) + app.processEvents() + w = vb.geometry().width() + h = vb.geometry().height() + size1 = QRectF(0, h, w, -h) + assertMapping(vb, view1, size1) + + # now lock aspect + vb.setAspectLocked() + + # test wide resize + win.resize(800, 400) + app.processEvents() + w = vb.geometry().width() + h = vb.geometry().height() + view1 = QRectF(-5, 0, 20, 10) + size1 = QRectF(0, h, w, -h) + assertMapping(vb, view1, size1) + + # test tall resize + win.resize(400, 800) + app.processEvents() + w = vb.geometry().width() + h = vb.geometry().height() + view1 = QRectF(0, -5, 10, 20) + size1 = QRectF(0, h, w, -h) + assertMapping(vb, view1, size1) + + # test limits + resize (aspect ratio constraint has priority over limits + win.resize(400, 400) + app.processEvents() + vb.setLimits(xMin=0, xMax=10, yMin=0, yMax=10) + win.resize(800, 400) + app.processEvents() + w = vb.geometry().width() + h = vb.geometry().height() + view1 = QRectF(-5, 0, 20, 10) + size1 = QRectF(0, h, w, -h) + assertMapping(vb, view1, size1) + + +if __name__ == '__main__': + import user,sys + test_ViewBox() + \ No newline at end of file From 279ad1bee0cf7b1ac8c408a7932062f588c32b5e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 18 May 2014 19:14:57 -0400 Subject: [PATCH 218/268] Fixed ViewBox error when accessing zoom history before having zoomed. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 3fa079f2..d66f32ad 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1312,6 +1312,8 @@ class ViewBox(GraphicsWidget): ev.ignore() def scaleHistory(self, d): + if len(self.axHistory) == 0: + return ptr = max(0, min(len(self.axHistory)-1, self.axHistoryPointer+d)) if ptr != self.axHistoryPointer: self.axHistoryPointer = ptr From 9dbdeaa1e0ccae9d9ce170aed6f1bb36bfa1cb1f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 19 May 2014 18:25:05 -0400 Subject: [PATCH 219/268] Added missing .ui file needed for uic unit test --- pyqtgraph/tests/uictest.ui | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 pyqtgraph/tests/uictest.ui diff --git a/pyqtgraph/tests/uictest.ui b/pyqtgraph/tests/uictest.ui new file mode 100644 index 00000000..25d14f2b --- /dev/null +++ b/pyqtgraph/tests/uictest.ui @@ -0,0 +1,53 @@ + + + Form + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + 10 + 10 + 120 + 80 + + + + + + + 10 + 110 + 120 + 80 + + + + + + + PlotWidget + QWidget +
pyqtgraph
+ 1 +
+ + ImageView + QWidget +
pyqtgraph
+ 1 +
+
+ + +
From c3fdcc9ae55148993ae285543318fdfc17104526 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 21 May 2014 23:25:07 -0400 Subject: [PATCH 220/268] Minor fix in list parameter --- pyqtgraph/parametertree/parameterTypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 62e935fd..8aba4bca 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -539,8 +539,8 @@ class ListParameter(Parameter): self.forward, self.reverse = self.mapping(limits) Parameter.setLimits(self, limits) - #print self.name(), self.value(), limits - if len(self.reverse) > 0 and self.value() not in self.reverse[0]: + #print self.name(), self.value(), limits, self.reverse + if len(self.reverse[0]) > 0 and self.value() not in self.reverse[0]: self.setValue(self.reverse[0][0]) #def addItem(self, name, value=None): From 0524bfa6e8e125a58db45ad4f99b81189baf1a62 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 22 May 2014 01:22:12 -0400 Subject: [PATCH 221/268] Added new demos: - Relativity simulator - Optics simulator - Mechanical chain simulator --- examples/__main__.py | 5 + examples/optics/__init__.py | 1 + examples/optics/pyoptic.py | 582 ++++++++++++++++++++++++++ examples/optics/schott_glasses.csv.gz | Bin 0 -> 37232 bytes examples/optics_demos.py | 170 ++++++++ examples/relativity | 1 + examples/relativity_demo.py | 23 + examples/verlet_chain/__init__.py | 1 + examples/verlet_chain/chain.py | 110 +++++ examples/verlet_chain/make | 3 + examples/verlet_chain/maths.so | Bin 0 -> 8017 bytes examples/verlet_chain/relax.c | 48 +++ examples/verlet_chain/relax.py | 23 + examples/verlet_chain_demo.py | 111 +++++ 14 files changed, 1078 insertions(+) create mode 100644 examples/optics/__init__.py create mode 100644 examples/optics/pyoptic.py create mode 100644 examples/optics/schott_glasses.csv.gz create mode 100644 examples/optics_demos.py create mode 160000 examples/relativity create mode 100644 examples/relativity_demo.py create mode 100644 examples/verlet_chain/__init__.py create mode 100644 examples/verlet_chain/chain.py create mode 100755 examples/verlet_chain/make create mode 100755 examples/verlet_chain/maths.so create mode 100644 examples/verlet_chain/relax.c create mode 100644 examples/verlet_chain/relax.py create mode 100644 examples/verlet_chain_demo.py diff --git a/examples/__main__.py b/examples/__main__.py index e972c60a..6c2e8b97 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -32,6 +32,11 @@ examples = OrderedDict([ ('Auto-range', 'PlotAutoRange.py'), ('Remote Plotting', 'RemoteSpeedTest.py'), ('HDF5 big data', 'hdf5.py'), + ('Demos', OrderedDict([ + ('Optics', 'optics_demos.py'), + ('Special relativity', 'relativity_demo.py'), + ('Verlet chain', 'verlet_chain_demo.py'), + ])), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), diff --git a/examples/optics/__init__.py b/examples/optics/__init__.py new file mode 100644 index 00000000..577c24da --- /dev/null +++ b/examples/optics/__init__.py @@ -0,0 +1 @@ +from pyoptic import * \ No newline at end of file diff --git a/examples/optics/pyoptic.py b/examples/optics/pyoptic.py new file mode 100644 index 00000000..486f653d --- /dev/null +++ b/examples/optics/pyoptic.py @@ -0,0 +1,582 @@ +# -*- coding: utf-8 -*- +from PyQt4 import QtGui, QtCore +import pyqtgraph as pg +#from pyqtgraph.canvas import Canvas, CanvasItem +import numpy as np +import csv, gzip, os +from pyqtgraph import Point + +class GlassDB: + """ + Database of dispersion coefficients for Schott glasses + + Corning 7980 + """ + def __init__(self, fileName='schott_glasses.csv'): + path = os.path.dirname(__file__) + fh = gzip.open(os.path.join(path, 'schott_glasses.csv.gz'), 'rb') + r = csv.reader(fh.readlines()) + lines = [x for x in r] + self.data = {} + header = lines[0] + for l in lines[1:]: + info = {} + for i in range(1, len(l)): + info[header[i]] = l[i] + self.data[l[0]] = info + self.data['Corning7980'] = { ## Thorlabs UV fused silica--not in schott catalog. + 'B1': 0.68374049400, + 'B2': 0.42032361300, + 'B3': 0.58502748000, + 'C1': 0.00460352869, + 'C2': 0.01339688560, + 'C3': 64.49327320000, + 'TAUI25/250': 0.95, ## transmission data is fabricated, but close. + 'TAUI25/1400': 0.98, + } + + for k in self.data: + self.data[k]['ior_cache'] = {} + + + def ior(self, glass, wl): + """ + Return the index of refraction for *glass* at wavelength *wl*. + + The *glass* argument must be a key in self.data. + """ + info = self.data[glass] + cache = info['ior_cache'] + if wl not in cache: + B = map(float, [info['B1'], info['B2'], info['B3']]) + C = map(float, [info['C1'], info['C2'], info['C3']]) + w2 = (wl/1000.)**2 + n = np.sqrt(1.0 + (B[0]*w2 / (w2-C[0])) + (B[1]*w2 / (w2-C[1])) + (B[2]*w2 / (w2-C[2]))) + cache[wl] = n + return cache[wl] + + def transmissionCurve(self, glass): + data = self.data[glass] + keys = [int(x[7:]) for x in data.keys() if 'TAUI25' in x] + keys.sort() + curve = np.empty((2,len(keys))) + for i in range(len(keys)): + curve[0][i] = keys[i] + key = 'TAUI25/%d' % keys[i] + val = data[key] + if val == '': + val = 0 + else: + val = float(val) + curve[1][i] = val + return curve + + +GLASSDB = GlassDB() + + +def wlPen(wl): + """Return a pen representing the given wavelength""" + l1 = 400 + l2 = 700 + hue = np.clip(((l2-l1) - (wl-l1)) * 0.8 / (l2-l1), 0, 0.8) + val = 1.0 + if wl > 700: + val = 1.0 * (((700-wl)/700.) + 1) + elif wl < 400: + val = wl * 1.0/400. + #print hue, val + color = pg.hsvColor(hue, 1.0, val) + pen = pg.mkPen(color) + return pen + + +class ParamObj: + # Just a helper for tracking parameters and responding to changes + def __init__(self): + self.__params = {} + + def __setitem__(self, item, val): + self.setParam(item, val) + + def setParam(self, param, val): + self.setParams(**{param:val}) + + def setParams(self, **params): + """Set parameters for this optic. This is a good function to override for subclasses.""" + self.__params.update(params) + self.paramStateChanged() + + def paramStateChanged(self): + pass + + def __getitem__(self, item): + return self.getParam(item) + + def getParam(self, param): + return self.__params[param] + + +class Optic(pg.GraphicsObject, ParamObj): + + sigStateChanged = QtCore.Signal() + + + def __init__(self, gitem, **params): + ParamObj.__init__(self) + pg.GraphicsObject.__init__(self) #, [0,0], [1,1]) + + self.gitem = gitem + self.surfaces = gitem.surfaces + gitem.setParentItem(self) + + self.roi = pg.ROI([0,0], [1,1]) + self.roi.addRotateHandle([1, 1], [0.5, 0.5]) + self.roi.setParentItem(self) + + defaults = { + 'pos': Point(0,0), + 'angle': 0, + } + defaults.update(params) + self._ior_cache = {} + self.roi.sigRegionChanged.connect(self.roiChanged) + self.setParams(**defaults) + + def updateTransform(self): + self.resetTransform() + self.setPos(0, 0) + self.translate(Point(self['pos'])) + self.rotate(self['angle']) + + def setParam(self, param, val): + ParamObj.setParam(self, param, val) + + def paramStateChanged(self): + """Some parameters of the optic have changed.""" + # Move graphics item + self.gitem.setPos(Point(self['pos'])) + self.gitem.resetTransform() + self.gitem.rotate(self['angle']) + + # Move ROI to match + try: + self.roi.sigRegionChanged.disconnect(self.roiChanged) + br = self.gitem.boundingRect() + o = self.gitem.mapToParent(br.topLeft()) + self.roi.setAngle(self['angle']) + self.roi.setPos(o) + self.roi.setSize([br.width(), br.height()]) + finally: + self.roi.sigRegionChanged.connect(self.roiChanged) + + self.sigStateChanged.emit() + + def roiChanged(self, *args): + pos = self.roi.pos() + # rotate gitem temporarily so we can decide where it will need to move + self.gitem.resetTransform() + self.gitem.rotate(self.roi.angle()) + br = self.gitem.boundingRect() + o1 = self.gitem.mapToParent(br.topLeft()) + self.setParams(angle=self.roi.angle(), pos=pos + (self.gitem.pos() - o1)) + + def boundingRect(self): + return QtCore.QRectF() + + def paint(self, p, *args): + pass + + def ior(self, wavelength): + return GLASSDB.ior(self['glass'], wavelength) + + + +class Lens(Optic): + def __init__(self, **params): + defaults = { + 'dia': 25.4, ## diameter of lens + 'r1': 50., ## positive means convex, use 0 for planar + 'r2': 0, ## negative means convex + 'd': 4.0, + 'glass': 'N-BK7', + 'reflect': False, + } + defaults.update(params) + d = defaults.pop('d') + defaults['x1'] = -d/2. + defaults['x2'] = d/2. + + gitem = CircularSolid(brush=(100, 100, 130, 100), **defaults) + Optic.__init__(self, gitem, **defaults) + + def propagateRay(self, ray): + """Refract, reflect, absorb, and/or scatter ray. This function may create and return new rays""" + + """ + NOTE:: We can probably use this to compute refractions faster: (from GLSL 120 docs) + + For the incident vector I and surface normal N, and the + ratio of indices of refraction eta, return the refraction + vector. The result is computed by + k = 1.0 - eta * eta * (1.0 - dot(N, I) * dot(N, I)) + if (k < 0.0) + return genType(0.0) + else + return eta * I - (eta * dot(N, I) + sqrt(k)) * N + The input parameters for the incident vector I and the + surface normal N must already be normalized to get the + desired results. eta == ratio of IORs + + + For reflection: + For the incident vector I and surface orientation N, + returns the reflection direction: + I – 2 ∗ dot(N, I) ∗ N + N must already be normalized in order to achieve the + desired result. + """ + + + + iors = [self.ior(ray['wl']), 1.0] + for i in [0,1]: + surface = self.surfaces[i] + ior = iors[i] + p1, ai = surface.intersectRay(ray) + #print "surface intersection:", p1, ai*180/3.14159 + #trans = self.sceneTransform().inverted()[0] * surface.sceneTransform() + #p1 = trans.map(p1) + if p1 is None: + ray.setEnd(None) + break + p1 = surface.mapToItem(ray, p1) + + #print "adjusted position:", p1 + #ior = self.ior(ray['wl']) + rd = ray['dir'] + a1 = np.arctan2(rd[1], rd[0]) + ar = a1 - ai + np.arcsin((np.sin(ai) * ray['ior'] / ior)) + #print [x for x in [a1, ai, (np.sin(ai) * ray['ior'] / ior), ar]] + #print ai, np.sin(ai), ray['ior'], ior + ray.setEnd(p1) + dp = Point(np.cos(ar), np.sin(ar)) + #p2 = p1+dp + #p1p = self.mapToScene(p1) + #p2p = self.mapToScene(p2) + #dpp = Point(p2p-p1p) + ray = Ray(parent=ray, ior=ior, dir=dp) + return [ray] + + +class Mirror(Optic): + def __init__(self, **params): + defaults = { + 'r1': 0, + 'r2': 0, + 'd': 0.01, + } + defaults.update(params) + d = defaults.pop('d') + defaults['x1'] = -d/2. + defaults['x2'] = d/2. + gitem = CircularSolid(brush=(100,100,100,255), **defaults) + Optic.__init__(self, gitem, **defaults) + + def propagateRay(self, ray): + """Refract, reflect, absorb, and/or scatter ray. This function may create and return new rays""" + + surface = self.surfaces[0] + p1, ai = surface.intersectRay(ray) + if p1 is not None: + p1 = surface.mapToItem(ray, p1) + rd = ray['dir'] + a1 = np.arctan2(rd[1], rd[0]) + ar = a1 + np.pi - 2*ai + ray.setEnd(p1) + dp = Point(np.cos(ar), np.sin(ar)) + ray = Ray(parent=ray, dir=dp) + else: + ray.setEnd(None) + return [ray] + + +class CircularSolid(pg.GraphicsObject, ParamObj): + """GraphicsObject with two circular or flat surfaces.""" + def __init__(self, pen=None, brush=None, **opts): + """ + Arguments for each surface are: + x1,x2 - position of center of _physical surface_ + r1,r2 - radius of curvature + d1,d2 - diameter of optic + """ + defaults = dict(x1=-2, r1=100, d1=25.4, x2=2, r2=100, d2=25.4) + defaults.update(opts) + ParamObj.__init__(self) + self.surfaces = [CircleSurface(defaults['r1'], defaults['d1']), CircleSurface(-defaults['r2'], defaults['d2'])] + pg.GraphicsObject.__init__(self) + for s in self.surfaces: + s.setParentItem(self) + + if pen is None: + self.pen = pg.mkPen((220,220,255,200), width=1, cosmetic=True) + else: + self.pen = pg.mkPen(pen) + + if brush is None: + self.brush = pg.mkBrush((230, 230, 255, 30)) + else: + self.brush = pg.mkBrush(brush) + + self.setParams(**defaults) + + def paramStateChanged(self): + self.updateSurfaces() + + def updateSurfaces(self): + self.surfaces[0].setParams(self['r1'], self['d1']) + self.surfaces[1].setParams(-self['r2'], self['d2']) + self.surfaces[0].setPos(self['x1'], 0) + self.surfaces[1].setPos(self['x2'], 0) + + self.path = QtGui.QPainterPath() + self.path.connectPath(self.surfaces[0].path.translated(self.surfaces[0].pos())) + self.path.connectPath(self.surfaces[1].path.translated(self.surfaces[1].pos()).toReversed()) + self.path.closeSubpath() + + def boundingRect(self): + return self.path.boundingRect() + + def shape(self): + return self.path + + def paint(self, p, *args): + p.setRenderHints(p.renderHints() | p.Antialiasing) + p.setPen(self.pen) + p.fillPath(self.path, self.brush) + p.drawPath(self.path) + + +class CircleSurface(pg.GraphicsObject): + def __init__(self, radius=None, diameter=None): + """center of physical surface is at 0,0 + radius is the radius of the surface. If radius is None, the surface is flat. + diameter is of the optic's edge.""" + pg.GraphicsObject.__init__(self) + + self.r = radius + self.d = diameter + self.mkPath() + + def setParams(self, r, d): + self.r = r + self.d = d + self.mkPath() + + def mkPath(self): + self.prepareGeometryChange() + r = self.r + d = self.d + h2 = d/2. + self.path = QtGui.QPainterPath() + if r == 0: ## flat surface + self.path.moveTo(0, h2) + self.path.lineTo(0, -h2) + else: + ## half-height of surface can't be larger than radius + h2 = min(h2, abs(r)) + + #dx = abs(r) - (abs(r)**2 - abs(h2)**2)**0.5 + #p.moveTo(-d*w/2.+ d*dx, d*h2) + arc = QtCore.QRectF(0, -r, r*2, r*2) + #self.surfaces.append((arc.center(), r, h2)) + a1 = np.arcsin(h2/r) * 180. / np.pi + a2 = -2*a1 + a1 += 180. + self.path.arcMoveTo(arc, a1) + self.path.arcTo(arc, a1, a2) + #if d == -1: + #p1 = QtGui.QPainterPath() + #p1.addRect(arc) + #self.paths.append(p1) + self.h2 = h2 + + def boundingRect(self): + return self.path.boundingRect() + + def paint(self, p, *args): + return ## usually we let the optic draw. + #p.setPen(pg.mkPen('r')) + #p.drawPath(self.path) + + def intersectRay(self, ray): + ## return the point of intersection and the angle of incidence + #print "intersect ray" + h = self.h2 + r = self.r + p, dir = ray.currentState(relativeTo=self) # position and angle of ray in local coords. + #print " ray: ", p, dir + p = p - Point(r, 0) ## move position so center of circle is at 0,0 + #print " adj: ", p, r + + if r == 0: + #print " flat" + if dir[0] == 0: + y = 0 + else: + y = p[1] - p[0] * dir[1]/dir[0] + if abs(y) > h: + return None, None + else: + return (Point(0, y), np.arctan2(dir[1], dir[0])) + else: + #print " curve" + ## find intersection of circle and line (quadratic formula) + dx = dir[0] + dy = dir[1] + dr = (dx**2 + dy**2) ** 0.5 + D = p[0] * (p[1]+dy) - (p[0]+dx) * p[1] + idr2 = 1.0 / dr**2 + disc = r**2 * dr**2 - D**2 + if disc < 0: + return None, None + disc2 = disc**0.5 + if dy < 0: + sgn = -1 + else: + sgn = 1 + + + br = self.path.boundingRect() + x1 = (D*dy + sgn*dx*disc2) * idr2 + y1 = (-D*dx + abs(dy)*disc2) * idr2 + if br.contains(x1+r, y1): + pt = Point(x1, y1) + else: + x2 = (D*dy - sgn*dx*disc2) * idr2 + y2 = (-D*dx - abs(dy)*disc2) * idr2 + pt = Point(x2, y2) + if not br.contains(x2+r, y2): + return None, None + raise Exception("No intersection!") + + norm = np.arctan2(pt[1], pt[0]) + if r < 0: + norm += np.pi + #print " norm:", norm*180/3.1415 + dp = p - pt + #print " dp:", dp + ang = np.arctan2(dp[1], dp[0]) + #print " ang:", ang*180/3.1415 + #print " ai:", (ang-norm)*180/3.1415 + + #print " intersection:", pt + return pt + Point(r, 0), ang-norm + + +class Ray(pg.GraphicsObject, ParamObj): + """Represents a single straight segment of a ray""" + + sigStateChanged = QtCore.Signal() + + def __init__(self, **params): + ParamObj.__init__(self) + defaults = { + 'ior': 1.0, + 'wl': 500, + 'end': None, + 'dir': Point(1,0), + } + self.params = {} + pg.GraphicsObject.__init__(self) + self.children = [] + parent = params.get('parent', None) + if parent is not None: + defaults['start'] = parent['end'] + defaults['wl'] = parent['wl'] + self['ior'] = parent['ior'] + self['dir'] = parent['dir'] + parent.addChild(self) + + defaults.update(params) + defaults['dir'] = Point(defaults['dir']) + self.setParams(**defaults) + self.mkPath() + + def clearChildren(self): + for c in self.children: + c.clearChildren() + c.setParentItem(None) + self.scene().removeItem(c) + self.children = [] + + def paramStateChanged(self): + pass + + def addChild(self, ch): + self.children.append(ch) + ch.setParentItem(self) + + def currentState(self, relativeTo=None): + pos = self['start'] + dir = self['dir'] + if relativeTo is None: + return pos, dir + else: + trans = self.itemTransform(relativeTo)[0] + p1 = trans.map(pos) + p2 = trans.map(pos + dir) + return Point(p1), Point(p2-p1) + + + def setEnd(self, end): + self['end'] = end + self.mkPath() + + def boundingRect(self): + return self.path.boundingRect() + + def paint(self, p, *args): + #p.setPen(pg.mkPen((255,0,0, 150))) + p.setRenderHints(p.renderHints() | p.Antialiasing) + p.setCompositionMode(p.CompositionMode_Plus) + p.setPen(wlPen(self['wl'])) + p.drawPath(self.path) + + def mkPath(self): + self.prepareGeometryChange() + self.path = QtGui.QPainterPath() + self.path.moveTo(self['start']) + if self['end'] is not None: + self.path.lineTo(self['end']) + else: + self.path.lineTo(self['start']+500*self['dir']) + + +def trace(rays, optics): + if len(optics) < 1 or len(rays) < 1: + return + for r in rays: + r.clearChildren() + o = optics[0] + r2 = o.propagateRay(r) + trace(r2, optics[1:]) + +class Tracer(QtCore.QObject): + """ + Simple ray tracer. + + Initialize with a list of rays and optics; + calling trace() will cause rays to be extended by propagating them through + each optic in sequence. + """ + def __init__(self, rays, optics): + QtCore.QObject.__init__(self) + self.optics = optics + self.rays = rays + for o in self.optics: + o.sigStateChanged.connect(self.trace) + self.trace() + + def trace(self): + trace(self.rays, self.optics) + diff --git a/examples/optics/schott_glasses.csv.gz b/examples/optics/schott_glasses.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..8df4ae14a38b916d5591749d049804097f8e86b8 GIT binary patch literal 37232 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 4*np.pi: + m1['angle'] = 315 + 1.5*np.sin(phase) + m1a['angle'] = 315 + 1.5*np.sin(phase) + else: + m2['angle'] = 135 + 1.5*np.sin(phase) + m2a['angle'] = 135 + 1.5*np.sin(phase) + phase += 0.2 + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(40) + + + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/relativity b/examples/relativity new file mode 160000 index 00000000..876a0a80 --- /dev/null +++ b/examples/relativity @@ -0,0 +1 @@ +Subproject commit 876a0a80b705dad71e5b1addab9b859cfc292f20 diff --git a/examples/relativity_demo.py b/examples/relativity_demo.py new file mode 100644 index 00000000..24a1f476 --- /dev/null +++ b/examples/relativity_demo.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +""" +Special relativity simulation + + + +""" +import initExample ## Add path to library (just for examples; you do not need this) +import pyqtgraph as pg +from relativity import RelativityGUI + +pg.mkQApp() +win = RelativityGUI() +win.setWindowTitle("Relativity!") +win.resize(1100,700) +win.show() +win.loadPreset(None, 'Twin Paradox (grid)') + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(pg.QtCore, 'PYQT_VERSION'): + pg.QtGui.QApplication.instance().exec_() diff --git a/examples/verlet_chain/__init__.py b/examples/verlet_chain/__init__.py new file mode 100644 index 00000000..abd9e103 --- /dev/null +++ b/examples/verlet_chain/__init__.py @@ -0,0 +1 @@ +from chain import ChainSim \ No newline at end of file diff --git a/examples/verlet_chain/chain.py b/examples/verlet_chain/chain.py new file mode 100644 index 00000000..9671e0a5 --- /dev/null +++ b/examples/verlet_chain/chain.py @@ -0,0 +1,110 @@ +import pyqtgraph as pg +import numpy as np +import time +from relax import relax + + +class ChainSim(pg.QtCore.QObject): + + stepped = pg.QtCore.Signal() + relaxed = pg.QtCore.Signal() + + def __init__(self): + pg.QtCore.QObject.__init__(self) + + self.damping = 0.1 # 0=full damping, 1=no damping + self.relaxPerStep = 10 + self.maxTimeStep = 0.01 + + self.pos = None # (Npts, 2) float + self.mass = None # (Npts) float + self.fixed = None # (Npts) bool + self.links = None # (Nlinks, 2), uint + self.lengths = None # (Nlinks), float + self.push = None # (Nlinks), bool + self.pull = None # (Nlinks), bool + + self.initialized = False + self.lasttime = None + self.lastpos = None + + def init(self): + if self.initialized: + return + + assert None not in [self.pos, self.mass, self.links, self.lengths] + + if self.fixed is None: + self.fixed = np.zeros(self.pos.shape[0], dtype=bool) + if self.push is None: + self.push = np.ones(self.links.shape[0], dtype=bool) + if self.pull is None: + self.pull = np.ones(self.links.shape[0], dtype=bool) + + + # precompute relative masses across links + l1 = self.links[:,0] + l2 = self.links[:,1] + m1 = self.mass[l1] + m2 = self.mass[l2] + self.mrel1 = (m1 / (m1+m2))[:,np.newaxis] + self.mrel1[self.fixed[l1]] = 1 # fixed point constraint + self.mrel1[self.fixed[l2]] = 0 + self.mrel2 = 1.0 - self.mrel1 + + for i in range(100): + self.relax(n=10) + + self.initialized = True + + def makeGraph(self): + #g1 = pg.GraphItem(pos=self.pos, adj=self.links[self.rope], pen=0.2, symbol=None) + brushes = np.where(self.fixed, pg.mkBrush(0,0,0,255), pg.mkBrush(50,50,200,255)) + g2 = pg.GraphItem(pos=self.pos, adj=self.links[self.push & self.pull], pen=0.5, brush=brushes, symbol='o', size=(self.mass**0.33), pxMode=False) + p = pg.ItemGroup() + #p.addItem(g1) + p.addItem(g2) + return p + + def update(self): + # approximate physics with verlet integration + + now = pg.ptime.time() + if self.lasttime is None: + dt = 0 + else: + dt = now - self.lasttime + self.lasttime = now + + if self.lastpos is None: + self.lastpos = self.pos + + # remember fixed positions + fixedpos = self.pos[self.fixed] + + while dt > 0: + dt1 = min(self.maxTimeStep, dt) + dt -= dt1 + + # compute motion since last timestep + dx = self.pos - self.lastpos + self.lastpos = self.pos + + # update positions for gravity and inertia + acc = np.array([[0, -5]]) * dt1 + inertia = dx * (self.damping**(dt1/self.mass))[:,np.newaxis] # with mass-dependent damping + self.pos = self.pos + inertia + acc + + self.pos[self.fixed] = fixedpos # fixed point constraint + + # correct for link constraints + self.relax(self.relaxPerStep) + self.stepped.emit() + + + def relax(self, n=50): + # speed up with C magic + relax(self.pos, self.links, self.mrel1, self.mrel2, self.lengths, self.push, self.pull, n) + self.relaxed.emit() + + diff --git a/examples/verlet_chain/make b/examples/verlet_chain/make new file mode 100755 index 00000000..78503f2a --- /dev/null +++ b/examples/verlet_chain/make @@ -0,0 +1,3 @@ +gcc -fPIC -c relax.c +gcc -shared -o maths.so relax.o + diff --git a/examples/verlet_chain/maths.so b/examples/verlet_chain/maths.so new file mode 100755 index 0000000000000000000000000000000000000000..62aff3214ec02e8d87bef57c290d33d7efc7f472 GIT binary patch literal 8017 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 +#include + +void relax( + double* pos, + long* links, + double* mrel1, + double* mrel2, + double* lengths, + char* push, + char* pull, + int nlinks, + int iters) + { + int i, l, p1, p2; + double x1, x2, y1, y2, dx, dy, dist, change; +// printf("%d, %d\n", iters, nlinks); + for( i=0; i lengths[l] ) + dist = lengths[l]; + + change = (lengths[l]-dist) / dist; + dx *= change; + dy *= change; + + pos[p1] -= mrel2[l] * dx; + pos[p1+1] -= mrel2[l] * dy; + pos[p2] += mrel1[l] * dx; + pos[p2+1] += mrel1[l] * dy; + } + } +} \ No newline at end of file diff --git a/examples/verlet_chain/relax.py b/examples/verlet_chain/relax.py new file mode 100644 index 00000000..95a2b7b3 --- /dev/null +++ b/examples/verlet_chain/relax.py @@ -0,0 +1,23 @@ +import ctypes +import os + +so = os.path.join(os.path.dirname(__file__), 'maths.so') +lib = ctypes.CDLL(so) + +lib.relax.argtypes = [ + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_int, + ctypes.c_int, + ] + +def relax(pos, links, mrel1, mrel2, lengths, push, pull, iters): + nlinks = links.shape[0] + lib.relax(pos.ctypes, links.ctypes, mrel1.ctypes, mrel2.ctypes, lengths.ctypes, push.ctypes, pull.ctypes, nlinks, iters) + + diff --git a/examples/verlet_chain_demo.py b/examples/verlet_chain_demo.py new file mode 100644 index 00000000..6ed97d48 --- /dev/null +++ b/examples/verlet_chain_demo.py @@ -0,0 +1,111 @@ +""" +Mechanical simulation of a chain using verlet integration. + + + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +from verlet_chain import ChainSim + +sim = ChainSim() + + +chlen1 = 80 +chlen2 = 60 +npts = chlen1 + chlen2 + +sim.mass = np.ones(npts) +sim.mass[chlen1-15] = 100 +sim.mass[chlen1-1] = 500 +sim.mass[npts-1] = 200 + +sim.fixed = np.zeros(npts, dtype=bool) +sim.fixed[0] = True +sim.fixed[chlen1] = True + +sim.pos = np.empty((npts, 2)) +sim.pos[:chlen1, 0] = 0 +sim.pos[chlen1:, 0] = 10 +sim.pos[:chlen1, 1] = np.arange(chlen1) +sim.pos[chlen1:, 1] = np.arange(chlen2) + +links1 = [(j, i+j+1) for i in range(chlen1) for j in range(chlen1-i-1)] +links2 = [(j, i+j+1) for i in range(chlen2) for j in range(chlen2-i-1)] +sim.links = np.concatenate([np.array(links1), np.array(links2)+chlen1, np.array([[chlen1-1, npts-1]])]) + +p1 = sim.pos[sim.links[:,0]] +p2 = sim.pos[sim.links[:,1]] +dif = p2-p1 +sim.lengths = (dif**2).sum(axis=1) ** 0.5 +sim.lengths[(chlen1-1):len(links1)] *= 1.05 # let auxiliary links stretch a little +sim.lengths[(len(links1)+chlen2-1):] *= 1.05 +sim.lengths[-1] = 7 + +push1 = np.ones(len(links1), dtype=bool) +push1[chlen1:] = False +push2 = np.ones(len(links2), dtype=bool) +push2[chlen2:] = False +sim.push = np.concatenate([push1, push2, np.array([True], dtype=bool)]) + +sim.pull = np.ones(sim.links.shape[0], dtype=bool) +sim.pull[-1] = False + +mousepos = sim.pos[0] + + +def display(): + global view, sim + view.clear() + view.addItem(sim.makeGraph()) + +def relaxed(): + global app + display() + app.processEvents() + +def mouse(pos): + global mousepos + pos = view.mapSceneToView(pos) + mousepos = np.array([pos.x(), pos.y()]) + +def update(): + global mousepos + #sim.pos[0] = sim.pos[0] * 0.9 + mousepos * 0.1 + s = 0.9 + sim.pos[0] = sim.pos[0] * s + mousepos * (1.0-s) + sim.update() + +app = pg.mkQApp() +win = pg.GraphicsLayoutWidget() +win.show() +view = win.addViewBox() +view.setAspectLocked(True) +view.setXRange(-100, 100) +#view.autoRange() + +view.scene().sigMouseMoved.connect(mouse) + +#display() +#app.processEvents() + +sim.relaxed.connect(relaxed) +sim.init() +sim.relaxed.disconnect(relaxed) + +sim.stepped.connect(display) + +timer = pg.QtCore.QTimer() +timer.timeout.connect(update) +timer.start(16) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() From 6e9d5c3cfb4cdc1ef14c6f87a7ff06670cbb4c8f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 22 May 2014 01:30:15 -0400 Subject: [PATCH 222/268] Python 3 fixes for new demos --- examples/optics/__init__.py | 2 +- examples/optics/pyoptic.py | 6 +++--- examples/verlet_chain/__init__.py | 2 +- examples/verlet_chain/chain.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/optics/__init__.py b/examples/optics/__init__.py index 577c24da..b3d31cd0 100644 --- a/examples/optics/__init__.py +++ b/examples/optics/__init__.py @@ -1 +1 @@ -from pyoptic import * \ No newline at end of file +from .pyoptic import * \ No newline at end of file diff --git a/examples/optics/pyoptic.py b/examples/optics/pyoptic.py index 486f653d..dc493568 100644 --- a/examples/optics/pyoptic.py +++ b/examples/optics/pyoptic.py @@ -14,7 +14,7 @@ class GlassDB: def __init__(self, fileName='schott_glasses.csv'): path = os.path.dirname(__file__) fh = gzip.open(os.path.join(path, 'schott_glasses.csv.gz'), 'rb') - r = csv.reader(fh.readlines()) + r = csv.reader(map(str, fh.readlines())) lines = [x for x in r] self.data = {} header = lines[0] @@ -47,8 +47,8 @@ class GlassDB: info = self.data[glass] cache = info['ior_cache'] if wl not in cache: - B = map(float, [info['B1'], info['B2'], info['B3']]) - C = map(float, [info['C1'], info['C2'], info['C3']]) + B = list(map(float, [info['B1'], info['B2'], info['B3']])) + C = list(map(float, [info['C1'], info['C2'], info['C3']])) w2 = (wl/1000.)**2 n = np.sqrt(1.0 + (B[0]*w2 / (w2-C[0])) + (B[1]*w2 / (w2-C[1])) + (B[2]*w2 / (w2-C[2]))) cache[wl] = n diff --git a/examples/verlet_chain/__init__.py b/examples/verlet_chain/__init__.py index abd9e103..f473190f 100644 --- a/examples/verlet_chain/__init__.py +++ b/examples/verlet_chain/__init__.py @@ -1 +1 @@ -from chain import ChainSim \ No newline at end of file +from .chain import ChainSim \ No newline at end of file diff --git a/examples/verlet_chain/chain.py b/examples/verlet_chain/chain.py index 9671e0a5..896505ac 100644 --- a/examples/verlet_chain/chain.py +++ b/examples/verlet_chain/chain.py @@ -1,7 +1,7 @@ import pyqtgraph as pg import numpy as np import time -from relax import relax +from .relax import relax class ChainSim(pg.QtCore.QObject): From 374b5a33ed1167dc949bced2fdf74caeef823a41 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 22 May 2014 01:51:17 -0400 Subject: [PATCH 223/268] Added dialog to hdf5 example prompting user to generate sample data --- examples/hdf5.py | 49 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/examples/hdf5.py b/examples/hdf5.py index 3e239d9f..b43ae24a 100644 --- a/examples/hdf5.py +++ b/examples/hdf5.py @@ -18,14 +18,10 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np import h5py - import sys, os -if len(sys.argv) > 1: - fileName = sys.argv[1] -else: - fileName = 'test.hdf5' - if not os.path.isfile(fileName): - raise Exception("No suitable HDF5 file found. Use createFile() to generate an example file.") + +pg.mkQApp() + plt = pg.plot() plt.setWindowTitle('pyqtgraph example: HDF5 big data') @@ -101,10 +97,6 @@ class HDF5Plot(pg.PlotCurveItem): self.scale(scale, 1) # scale to match downsampling -f = h5py.File(fileName, 'r') -curve = HDF5Plot() -curve.setHDF5(f['data']) -plt.addItem(curve) def createFile(finalSize=2000000000): @@ -118,17 +110,42 @@ def createFile(finalSize=2000000000): f.create_dataset('data', data=chunk, chunks=True, maxshape=(None,)) data = f['data'] - for i in range(finalSize // (chunk.size * chunk.itemsize)): - newshape = [data.shape[0] + chunk.shape[0]] - data.resize(newshape) - data[-chunk.shape[0]:] = chunk - + nChunks = finalSize // (chunk.size * chunk.itemsize) + with pg.ProgressDialog("Generating test.hdf5...", 0, nChunks) as dlg: + for i in range(nChunks): + newshape = [data.shape[0] + chunk.shape[0]] + data.resize(newshape) + data[-chunk.shape[0]:] = chunk + dlg += 1 + if dlg.wasCanceled(): + f.close() + os.remove('test.hdf5') + sys.exit() + dlg += 1 f.close() +if len(sys.argv) > 1: + fileName = sys.argv[1] +else: + fileName = 'test.hdf5' + if not os.path.isfile(fileName): + size, ok = QtGui.QInputDialog.getDouble(None, "Create HDF5 Dataset?", "This demo requires a large HDF5 array. To generate a file, enter the array size (in GB) and press OK.", 2.0) + if not ok: + sys.exit(0) + else: + createFile(int(size*1e9)) + #raise Exception("No suitable HDF5 file found. Use createFile() to generate an example file.") + +f = h5py.File(fileName, 'r') +curve = HDF5Plot() +curve.setHDF5(f['data']) +plt.addItem(curve) ## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': + + import sys if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): QtGui.QApplication.instance().exec_() From d004b133cdead10f3b0653e453beb734ca56ec49 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 27 May 2014 18:46:34 -0400 Subject: [PATCH 224/268] Removed unnecessary file allocation from functions.interpolateArray --- pyqtgraph/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 2325186c..77643c99 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -538,7 +538,6 @@ def interpolateArray(data, x, default=0.0): prof = debug.Profiler() - result = np.empty(x.shape[:-1] + data.shape, dtype=data.dtype) nd = data.ndim md = x.shape[-1] From 35856ccaee679ace59572774b7c814bae696d144 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 6 Jun 2014 15:53:17 -0600 Subject: [PATCH 225/268] Added cx_freeze example (thanks Jerry!) --- examples/cx_freeze/plotTest.py | 20 +++++++++++++++++++ examples/cx_freeze/setup.py | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 examples/cx_freeze/plotTest.py create mode 100644 examples/cx_freeze/setup.py diff --git a/examples/cx_freeze/plotTest.py b/examples/cx_freeze/plotTest.py new file mode 100644 index 00000000..1a53a984 --- /dev/null +++ b/examples/cx_freeze/plotTest.py @@ -0,0 +1,20 @@ +import sys +from PyQt4 import QtGui +import pyqtgraph as pg +from pyqtgraph.graphicsItems import TextItem +# For packages that require scipy, these may be needed: +# from scipy.stats import futil +# from scipy.sparse.csgraph import _validation + +from pyqtgraph import setConfigOption +pg.setConfigOption('background','w') +pg.setConfigOption('foreground','k') +app = QtGui.QApplication(sys.argv) + +pw = pg.plot(x = [0, 1, 2, 4], y = [4, 5, 9, 6]) +pw.showGrid(x=True,y=True) +text = pg.TextItem(html='
%s
' % "here",anchor=(0.0, 0.0)) +text.setPos(1.0, 5.0) +pw.addItem(text) +status = app.exec_() +sys.exit(status) diff --git a/examples/cx_freeze/setup.py b/examples/cx_freeze/setup.py new file mode 100644 index 00000000..bdace733 --- /dev/null +++ b/examples/cx_freeze/setup.py @@ -0,0 +1,36 @@ +# Build with `python setup.py build_exe` +from cx_Freeze import setup, Executable + +import shutil +from glob import glob +# Remove the build folder +shutil.rmtree("build", ignore_errors=True) +shutil.rmtree("dist", ignore_errors=True) +import sys + +includes = ['PyQt4.QtCore', 'PyQt4.QtGui', 'sip', 'pyqtgraph.graphicsItems', + 'numpy', 'atexit'] +excludes = ['cvxopt','_gtkagg', '_tkagg', 'bsddb', 'curses', 'email', 'pywin.debugger', + 'pywin.debugger.dbgcon', 'pywin.dialogs', 'tcl','tables', + 'Tkconstants', 'Tkinter', 'zmq','PySide','pysideuic','scipy','matplotlib'] + +if sys.version[0] == '2': + # causes syntax error on py2 + excludes.append('PyQt4.uic.port_v3') + +base = None +if sys.platform == "win32": + base = "Win32GUI" + +build_exe_options = {'excludes': excludes, + 'includes':includes, 'include_msvcr':True, + 'compressed':True, 'copy_dependent_files':True, 'create_shared_zip':True, + 'include_in_shared_zip':True, 'optimize':2} + +setup(name = "cx_freeze plot test", + version = "0.1", + description = "cx_freeze plot test", + options = {"build_exe": build_exe_options}, + executables = [Executable("plotTest.py", base=base)]) + + From 04f1b0e6776d9c6c1d72749b903875432ed5ec66 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 13 Jun 2014 14:19:10 -0600 Subject: [PATCH 226/268] Added scrolling plot examples --- examples/__main__.py | 1 + examples/scrollingPlots.py | 118 ++++++++++++++++++++++++ pyqtgraph/graphicsItems/PlotDataItem.py | 2 +- 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 examples/scrollingPlots.py diff --git a/examples/__main__.py b/examples/__main__.py index 6c2e8b97..ea948bf4 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -31,6 +31,7 @@ examples = OrderedDict([ ('Histograms', 'histogram.py'), ('Auto-range', 'PlotAutoRange.py'), ('Remote Plotting', 'RemoteSpeedTest.py'), + ('Scrolling plots', 'scrollingPlots.py'), ('HDF5 big data', 'hdf5.py'), ('Demos', OrderedDict([ ('Optics', 'optics_demos.py'), diff --git a/examples/scrollingPlots.py b/examples/scrollingPlots.py new file mode 100644 index 00000000..623b9ab1 --- /dev/null +++ b/examples/scrollingPlots.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +""" +Various methods of drawing scrolling plots. +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +win = pg.GraphicsWindow() +win.setWindowTitle('pyqtgraph example: Scrolling Plots') + + +# 1) Simplest approach -- update data in the array such that plot appears to scroll +# In these examples, the array size is fixed. +p1 = win.addPlot() +p2 = win.addPlot() +data1 = np.random.normal(size=300) +curve1 = p1.plot(data1) +curve2 = p2.plot(data1) +ptr1 = 0 +def update1(): + global data1, curve1, ptr1 + data1[:-1] = data1[1:] # shift data in the array one sample left + # (see also: np.roll) + data1[-1] = np.random.normal() + curve1.setData(data1) + + ptr1 += 1 + curve2.setData(data1) + curve2.setPos(ptr1, 0) + + +# 2) Allow data to accumulate. In these examples, the array doubles in length +# whenever it is full. +win.nextRow() +p3 = win.addPlot() +p4 = win.addPlot() +# Use automatic downsampling and clipping to reduce the drawing load +p3.setDownsampling(mode='peak') +p4.setDownsampling(mode='peak') +p3.setClipToView(True) +p4.setClipToView(True) +p3.setRange(xRange=[-100, 0]) +p3.setLimits(xMax=0) +curve3 = p3.plot() +curve4 = p4.plot() + +data3 = np.empty(100) +ptr3 = 0 + +def update2(): + global data3, ptr3 + data3[ptr3] = np.random.normal() + ptr3 += 1 + if ptr3 >= data3.shape[0]: + tmp = data3 + data3 = np.empty(data3.shape[0] * 2) + data3[:tmp.shape[0]] = tmp + curve3.setData(data3[:ptr3]) + curve3.setPos(-ptr3, 0) + curve4.setData(data3[:ptr3]) + + +# 3) Plot in chunks, adding one new plot curve for every 100 samples +chunkSize = 100 +# Remove chunks after we have 10 +maxChunks = 10 +startTime = pg.ptime.time() +win.nextRow() +p5 = win.addPlot(colspan=2) +p5.setLabel('bottom', 'Time', 's') +p5.setXRange(-10, 0) +curves = [] +data5 = np.empty((chunkSize+1,2)) +ptr5 = 0 + +def update3(): + global p5, data5, ptr5, curves + now = pg.ptime.time() + for c in curves: + c.setPos(-(now-startTime), 0) + + i = ptr5 % chunkSize + if i == 0: + curve = p5.plot() + curves.append(curve) + last = data5[-1] + data5 = np.empty((chunkSize+1,2)) + data5[0] = last + while len(curves) > maxChunks: + c = curves.pop(0) + p5.removeItem(c) + else: + curve = curves[-1] + data5[i+1,0] = now - startTime + data5[i+1,1] = np.random.normal() + curve.setData(x=data5[:i+2, 0], y=data5[:i+2, 1]) + ptr5 += 1 + + +# update all plots +def update(): + update1() + update2() + update3() +timer = pg.QtCore.QTimer() +timer.timeout.connect(update) +timer.start(50) + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 3e760ce1..befc5783 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -546,7 +546,7 @@ class PlotDataItem(GraphicsObject): 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: + if range is not None and len(x) > 1: 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) From ba4f4e51058c6af1554e8a5bfc5bd15a045ae4d5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 13 Jun 2014 18:02:39 -0600 Subject: [PATCH 227/268] Added image analysis example --- examples/__main__.py | 1 + examples/imageAnalysis.py | 98 +++++++++++++++++++++++++ pyqtgraph/graphicsItems/IsocurveItem.py | 6 +- 3 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 examples/imageAnalysis.py diff --git a/examples/__main__.py b/examples/__main__.py index ea948bf4..cb1b87a1 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -26,6 +26,7 @@ examples = OrderedDict([ ('Crosshair / Mouse interaction', 'crosshair.py'), ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), + ('Image Analysis', 'imageAnalysis.py'), ('Dock widgets', 'dockarea.py'), ('Console', 'ConsoleWidget.py'), ('Histograms', 'histogram.py'), diff --git a/examples/imageAnalysis.py b/examples/imageAnalysis.py new file mode 100644 index 00000000..8283144e --- /dev/null +++ b/examples/imageAnalysis.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" +Demonstrates common image analysis tools. + +Many of the features demonstrated here are already provided by the ImageView +widget, but here we present a lower-level approach that provides finer control +over the user interface. +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +pg.mkQApp() + +win = pg.GraphicsLayoutWidget() +win.setWindowTitle('pyqtgraph example: Image Analysis') + +# A plot area (ViewBox + axes) for displaying the image +p1 = win.addPlot() + +# Item for displaying image data +img = pg.ImageItem() +p1.addItem(img) + +# Custom ROI for selecting an image region +roi = pg.ROI([-8, 14], [6, 5]) +roi.addScaleHandle([0.5, 1], [0.5, 0.5]) +roi.addScaleHandle([0, 0.5], [0.5, 0.5]) +p1.addItem(roi) +roi.setZValue(10) # make sure ROI is drawn above image + +# Isocurve drawing +iso = pg.IsocurveItem(level=0.8, pen='g') +iso.setParentItem(img) +iso.setZValue(5) + +# Contrast/color control +hist = pg.HistogramLUTItem() +hist.setImageItem(img) +win.addItem(hist) + +# Draggable line for setting isocurve level +isoLine = pg.InfiniteLine(angle=0, movable=True, pen='g') +hist.vb.addItem(isoLine) +hist.vb.setMouseEnabled(y=False) # makes user interaction a little easier +isoLine.setValue(0.8) +isoLine.setZValue(1000) # bring iso line above contrast controls + +# Another plot area for displaying ROI data +win.nextRow() +p2 = win.addPlot(colspan=2) +p2.setMaximumHeight(250) +win.resize(800, 800) +win.show() + + +# Generate image data +data = np.random.normal(size=(100, 200)) +data[20:80, 20:80] += 2. +data = pg.gaussianFilter(data, (3, 3)) +data += np.random.normal(size=(100, 200)) * 0.1 +img.setImage(data) +hist.setLevels(data.min(), data.max()) + +# build isocurves from smoothed data +iso.setData(pg.gaussianFilter(data, (2, 2))) + +# set position and scale of image +img.scale(0.2, 0.2) +img.translate(-50, 0) + +# zoom to fit imageo +p1.autoRange() + + +# Callbacks for handling user interaction +def updatePlot(): + global img, roi, data, p2 + selected = roi.getArrayRegion(data, img) + p2.plot(selected.mean(axis=1), clear=True) + +roi.sigRegionChanged.connect(updatePlot) +updatePlot() + +def updateIsocurve(): + global isoLine, iso + iso.setLevel(isoLine.value()) + +isoLine.sigDragged.connect(updateIsocurve) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/pyqtgraph/graphicsItems/IsocurveItem.py b/pyqtgraph/graphicsItems/IsocurveItem.py index 897df999..4474e29a 100644 --- a/pyqtgraph/graphicsItems/IsocurveItem.py +++ b/pyqtgraph/graphicsItems/IsocurveItem.py @@ -35,11 +35,6 @@ class IsocurveItem(GraphicsObject): self.setPen(pen) self.setData(data, level) - - - #if data is not None and level is not None: - #self.updateLines(data, level) - def setData(self, data, level=None): """ @@ -65,6 +60,7 @@ class IsocurveItem(GraphicsObject): """Set the level at which the isocurve is drawn.""" self.level = level self.path = None + self.prepareGeometryChange() self.update() From 274c76559415a1786742a3c4c950968a2bc2ea13 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 14 Jun 2014 11:35:00 -0600 Subject: [PATCH 228/268] Fixed relativity example --- examples/relativity | 1 - examples/relativity/__init__.py | 1 + .../relativity/presets/Grid Expansion.cfg | 411 ++++++++++ .../presets/Twin Paradox (grid).cfg | 667 +++++++++++++++ examples/relativity/presets/Twin Paradox.cfg | 538 ++++++++++++ examples/relativity/relativity.py | 773 ++++++++++++++++++ 6 files changed, 2390 insertions(+), 1 deletion(-) delete mode 160000 examples/relativity create mode 100644 examples/relativity/__init__.py create mode 100644 examples/relativity/presets/Grid Expansion.cfg create mode 100644 examples/relativity/presets/Twin Paradox (grid).cfg create mode 100644 examples/relativity/presets/Twin Paradox.cfg create mode 100644 examples/relativity/relativity.py diff --git a/examples/relativity b/examples/relativity deleted file mode 160000 index 876a0a80..00000000 --- a/examples/relativity +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 876a0a80b705dad71e5b1addab9b859cfc292f20 diff --git a/examples/relativity/__init__.py b/examples/relativity/__init__.py new file mode 100644 index 00000000..093806ef --- /dev/null +++ b/examples/relativity/__init__.py @@ -0,0 +1 @@ +from relativity import * diff --git a/examples/relativity/presets/Grid Expansion.cfg b/examples/relativity/presets/Grid Expansion.cfg new file mode 100644 index 00000000..0ab77795 --- /dev/null +++ b/examples/relativity/presets/Grid Expansion.cfg @@ -0,0 +1,411 @@ +name: 'params' +strictNaming: False +default: None +renamable: False +enabled: True +value: None +visible: True +readonly: False +removable: False +type: 'group' +children: + Load Preset..: + name: 'Load Preset..' + limits: ['', 'Twin Paradox (grid)', 'Twin Paradox'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Twin Paradox (grid)' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Duration: + name: 'Duration' + limits: [0.1, None] + strictNaming: False + default: 10.0 + renamable: False + enabled: True + readonly: False + value: 20.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Reference Frame: + name: 'Reference Frame' + limits: ['Grid00', 'Grid01', 'Grid02', 'Grid03', 'Grid04'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Grid02' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Animate: + name: 'Animate' + strictNaming: False + default: True + renamable: False + enabled: True + value: True + visible: True + readonly: False + removable: False + type: 'bool' + children: + Animation Speed: + name: 'Animation Speed' + limits: [0.0001, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + dec: True + type: 'float' + children: + Recalculate Worldlines: + name: 'Recalculate Worldlines' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Save: + name: 'Save' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Load: + name: 'Load' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Objects: + name: 'Objects' + strictNaming: False + default: None + renamable: False + addText: 'Add New..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: None + children: + Grid: + name: 'Grid' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Grid' + autoIncrementName: True + children: + Number of Clocks: + name: 'Number of Clocks' + limits: [1, None] + strictNaming: False + default: 5 + renamable: False + enabled: True + value: 5 + visible: True + readonly: False + removable: False + type: 'int' + children: + Spacing: + name: 'Spacing' + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + ClockTemplate: + name: 'ClockTemplate' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -2.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Command: + name: 'Command' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + value: 1.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command2: + name: 'Command2' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 2.0 + renamable: False + enabled: True + value: 3.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command3: + name: 'Command3' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 4.0 + renamable: False + enabled: True + value: 11.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command4: + name: 'Command4' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 8.0 + renamable: False + enabled: True + value: 13.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (100, 100, 150, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 0.5 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + addList: ['Clock', 'Grid'] diff --git a/examples/relativity/presets/Twin Paradox (grid).cfg b/examples/relativity/presets/Twin Paradox (grid).cfg new file mode 100644 index 00000000..ebe366bf --- /dev/null +++ b/examples/relativity/presets/Twin Paradox (grid).cfg @@ -0,0 +1,667 @@ +name: 'params' +strictNaming: False +default: None +renamable: False +enabled: True +value: None +visible: True +readonly: False +removable: False +type: 'group' +children: + Load Preset..: + name: 'Load Preset..' + limits: ['', 'Twin Paradox (grid)', 'Twin Paradox'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Twin Paradox (grid)' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Duration: + name: 'Duration' + limits: [0.1, None] + strictNaming: False + default: 10.0 + renamable: False + enabled: True + readonly: False + value: 27.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Reference Frame: + name: 'Reference Frame' + limits: ['Grid00', 'Grid01', 'Grid02', 'Grid03', 'Grid04', 'Grid05', 'Grid06', 'Grid07', 'Grid08', 'Grid09', 'Grid10', 'Alice', 'Bob'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Alice' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Animate: + name: 'Animate' + strictNaming: False + default: True + renamable: False + enabled: True + value: True + visible: True + readonly: False + removable: False + type: 'bool' + children: + Animation Speed: + name: 'Animation Speed' + limits: [0.0001, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + dec: True + type: 'float' + children: + Recalculate Worldlines: + name: 'Recalculate Worldlines' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Save: + name: 'Save' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Load: + name: 'Load' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Objects: + name: 'Objects' + strictNaming: False + default: None + renamable: False + addText: 'Add New..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: None + children: + Grid: + name: 'Grid' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Grid' + autoIncrementName: True + children: + Number of Clocks: + name: 'Number of Clocks' + limits: [1, None] + strictNaming: False + default: 5 + renamable: False + enabled: True + value: 11 + visible: True + readonly: False + removable: False + type: 'int' + children: + Spacing: + name: 'Spacing' + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 2.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + ClockTemplate: + name: 'ClockTemplate' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -10.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (77, 77, 77, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 1.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -2.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Alice: + name: 'Alice' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Command: + name: 'Command' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + value: 1.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command2: + name: 'Command2' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 2.0 + renamable: False + enabled: True + value: 3.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command3: + name: 'Command3' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 3.0 + renamable: False + enabled: True + value: 8.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command4: + name: 'Command4' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 4.0 + renamable: False + enabled: True + value: 12.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command5: + name: 'Command5' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 6.0 + renamable: False + enabled: True + value: 17.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command6: + name: 'Command6' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 7.0 + renamable: False + enabled: True + value: 19.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (82, 123, 44, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 1.5 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 3.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Bob: + name: 'Bob' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (69, 69, 126, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 1.5 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + addList: ['Clock', 'Grid'] diff --git a/examples/relativity/presets/Twin Paradox.cfg b/examples/relativity/presets/Twin Paradox.cfg new file mode 100644 index 00000000..569c3a04 --- /dev/null +++ b/examples/relativity/presets/Twin Paradox.cfg @@ -0,0 +1,538 @@ +name: 'params' +strictNaming: False +default: None +renamable: False +enabled: True +value: None +visible: True +readonly: False +removable: False +type: 'group' +children: + Load Preset..: + name: 'Load Preset..' + limits: ['', 'Twin Paradox', 'test'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Twin Paradox' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Duration: + name: 'Duration' + limits: [0.1, None] + strictNaming: False + default: 10.0 + renamable: False + enabled: True + readonly: False + value: 27.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Reference Frame: + name: 'Reference Frame' + limits: ['Alice', 'Bob'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Alice' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Animate: + name: 'Animate' + strictNaming: False + default: True + renamable: False + enabled: True + value: True + visible: True + readonly: False + removable: False + type: 'bool' + children: + Animation Speed: + name: 'Animation Speed' + limits: [0.0001, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + dec: True + type: 'float' + children: + Recalculate Worldlines: + name: 'Recalculate Worldlines' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Save: + name: 'Save' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Load: + name: 'Load' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Objects: + name: 'Objects' + strictNaming: False + default: None + renamable: False + addText: 'Add New..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: None + children: + Alice: + name: 'Alice' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Command: + name: 'Command' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + value: 1.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command2: + name: 'Command2' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 2.0 + renamable: False + enabled: True + value: 3.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command3: + name: 'Command3' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 3.0 + renamable: False + enabled: True + value: 8.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command4: + name: 'Command4' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 4.0 + renamable: False + enabled: True + value: 12.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command5: + name: 'Command5' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 6.0 + renamable: False + enabled: True + value: 17.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command6: + name: 'Command6' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 7.0 + renamable: False + enabled: True + value: 19.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (82, 123, 44, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 0.5 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Bob: + name: 'Bob' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (69, 69, 126, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 0.5 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + addList: ['Clock', 'Grid'] diff --git a/examples/relativity/relativity.py b/examples/relativity/relativity.py new file mode 100644 index 00000000..80a56d64 --- /dev/null +++ b/examples/relativity/relativity.py @@ -0,0 +1,773 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.parametertree import Parameter, ParameterTree +from pyqtgraph.parametertree import types as pTypes +import pyqtgraph.configfile +import numpy as np +import user +import collections +import sys, os + + + +class RelativityGUI(QtGui.QWidget): + def __init__(self): + QtGui.QWidget.__init__(self) + + self.animations = [] + self.animTimer = QtCore.QTimer() + self.animTimer.timeout.connect(self.stepAnimation) + self.animTime = 0 + self.animDt = .016 + self.lastAnimTime = 0 + + self.setupGUI() + + self.objectGroup = ObjectGroupParam() + + self.params = Parameter.create(name='params', type='group', children=[ + dict(name='Load Preset..', type='list', values=[]), + #dict(name='Unit System', type='list', values=['', 'MKS']), + dict(name='Duration', type='float', value=10.0, step=0.1, limits=[0.1, None]), + dict(name='Reference Frame', type='list', values=[]), + dict(name='Animate', type='bool', value=True), + dict(name='Animation Speed', type='float', value=1.0, dec=True, step=0.1, limits=[0.0001, None]), + dict(name='Recalculate Worldlines', type='action'), + dict(name='Save', type='action'), + dict(name='Load', type='action'), + self.objectGroup, + ]) + self.tree.setParameters(self.params, showTop=False) + self.params.param('Recalculate Worldlines').sigActivated.connect(self.recalculate) + self.params.param('Save').sigActivated.connect(self.save) + self.params.param('Load').sigActivated.connect(self.load) + self.params.param('Load Preset..').sigValueChanged.connect(self.loadPreset) + self.params.sigTreeStateChanged.connect(self.treeChanged) + + ## read list of preset configs + presetDir = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'presets') + if os.path.exists(presetDir): + presets = [os.path.splitext(p)[0] for p in os.listdir(presetDir)] + self.params.param('Load Preset..').setLimits(['']+presets) + + + + + def setupGUI(self): + self.layout = QtGui.QVBoxLayout() + self.layout.setContentsMargins(0,0,0,0) + self.setLayout(self.layout) + self.splitter = QtGui.QSplitter() + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.layout.addWidget(self.splitter) + + self.tree = ParameterTree(showHeader=False) + self.splitter.addWidget(self.tree) + + self.splitter2 = QtGui.QSplitter() + self.splitter2.setOrientation(QtCore.Qt.Vertical) + self.splitter.addWidget(self.splitter2) + + self.worldlinePlots = pg.GraphicsLayoutWidget() + self.splitter2.addWidget(self.worldlinePlots) + + self.animationPlots = pg.GraphicsLayoutWidget() + self.splitter2.addWidget(self.animationPlots) + + self.splitter2.setSizes([int(self.height()*0.8), int(self.height()*0.2)]) + + self.inertWorldlinePlot = self.worldlinePlots.addPlot() + self.refWorldlinePlot = self.worldlinePlots.addPlot() + + self.inertAnimationPlot = self.animationPlots.addPlot() + self.inertAnimationPlot.setAspectLocked(1) + self.refAnimationPlot = self.animationPlots.addPlot() + self.refAnimationPlot.setAspectLocked(1) + + self.inertAnimationPlot.setXLink(self.inertWorldlinePlot) + self.refAnimationPlot.setXLink(self.refWorldlinePlot) + + def recalculate(self): + ## build 2 sets of clocks + clocks1 = collections.OrderedDict() + clocks2 = collections.OrderedDict() + for cl in self.params.param('Objects'): + clocks1.update(cl.buildClocks()) + clocks2.update(cl.buildClocks()) + + ## Inertial simulation + dt = self.animDt * self.params['Animation Speed'] + sim1 = Simulation(clocks1, ref=None, duration=self.params['Duration'], dt=dt) + sim1.run() + sim1.plot(self.inertWorldlinePlot) + self.inertWorldlinePlot.autoRange(padding=0.1) + + ## reference simulation + ref = self.params['Reference Frame'] + dur = clocks1[ref].refData['pt'][-1] ## decide how long to run the reference simulation + sim2 = Simulation(clocks2, ref=clocks2[ref], duration=dur, dt=dt) + sim2.run() + sim2.plot(self.refWorldlinePlot) + self.refWorldlinePlot.autoRange(padding=0.1) + + + ## create animations + self.refAnimationPlot.clear() + self.inertAnimationPlot.clear() + self.animTime = 0 + + self.animations = [Animation(sim1), Animation(sim2)] + self.inertAnimationPlot.addItem(self.animations[0]) + self.refAnimationPlot.addItem(self.animations[1]) + + ## create lines representing all that is visible to a particular reference + #self.inertSpaceline = Spaceline(sim1, ref) + #self.refSpaceline = Spaceline(sim2) + self.inertWorldlinePlot.addItem(self.animations[0].items[ref].spaceline()) + self.refWorldlinePlot.addItem(self.animations[1].items[ref].spaceline()) + + + + + def setAnimation(self, a): + if a: + self.lastAnimTime = pg.ptime.time() + self.animTimer.start(self.animDt*1000) + else: + self.animTimer.stop() + + def stepAnimation(self): + now = pg.ptime.time() + dt = (now-self.lastAnimTime) * self.params['Animation Speed'] + self.lastAnimTime = now + self.animTime += dt + if self.animTime > self.params['Duration']: + self.animTime = 0 + for a in self.animations: + a.restart() + + for a in self.animations: + a.stepTo(self.animTime) + + + def treeChanged(self, *args): + clocks = [] + for c in self.params.param('Objects'): + clocks.extend(c.clockNames()) + #for param, change, data in args[1]: + #if change == 'childAdded': + self.params.param('Reference Frame').setLimits(clocks) + self.setAnimation(self.params['Animate']) + + def save(self): + fn = str(pg.QtGui.QFileDialog.getSaveFileName(self, "Save State..", "untitled.cfg", "Config Files (*.cfg)")) + if fn == '': + return + state = self.params.saveState() + pg.configfile.writeConfigFile(state, fn) + + def load(self): + fn = str(pg.QtGui.QFileDialog.getOpenFileName(self, "Save State..", "", "Config Files (*.cfg)")) + if fn == '': + return + state = pg.configfile.readConfigFile(fn) + self.loadState(state) + + def loadPreset(self, param, preset): + if preset == '': + return + path = os.path.abspath(os.path.dirname(__file__)) + fn = os.path.join(path, 'presets', preset+".cfg") + state = pg.configfile.readConfigFile(fn) + self.loadState(state) + + def loadState(self, state): + if 'Load Preset..' in state['children']: + del state['children']['Load Preset..']['limits'] + del state['children']['Load Preset..']['value'] + self.params.param('Objects').clearChildren() + self.params.restoreState(state, removeChildren=False) + self.recalculate() + + +class ObjectGroupParam(pTypes.GroupParameter): + def __init__(self): + pTypes.GroupParameter.__init__(self, name="Objects", addText="Add New..", addList=['Clock', 'Grid']) + + def addNew(self, typ): + if typ == 'Clock': + self.addChild(ClockParam()) + elif typ == 'Grid': + self.addChild(GridParam()) + +class ClockParam(pTypes.GroupParameter): + def __init__(self, **kwds): + defs = dict(name="Clock", autoIncrementName=True, renamable=True, removable=True, children=[ + dict(name='Initial Position', type='float', value=0.0, step=0.1), + #dict(name='V0', type='float', value=0.0, step=0.1), + AccelerationGroup(), + + dict(name='Rest Mass', type='float', value=1.0, step=0.1, limits=[1e-9, None]), + dict(name='Color', type='color', value=(100,100,150)), + dict(name='Size', type='float', value=0.5), + dict(name='Vertical Position', type='float', value=0.0, step=0.1), + ]) + #defs.update(kwds) + pTypes.GroupParameter.__init__(self, **defs) + self.restoreState(kwds, removeChildren=False) + + def buildClocks(self): + x0 = self['Initial Position'] + y0 = self['Vertical Position'] + color = self['Color'] + m = self['Rest Mass'] + size = self['Size'] + prog = self.param('Acceleration').generate() + c = Clock(x0=x0, m0=m, y0=y0, color=color, prog=prog, size=size) + return {self.name(): c} + + def clockNames(self): + return [self.name()] + +pTypes.registerParameterType('Clock', ClockParam) + +class GridParam(pTypes.GroupParameter): + def __init__(self, **kwds): + defs = dict(name="Grid", autoIncrementName=True, renamable=True, removable=True, children=[ + dict(name='Number of Clocks', type='int', value=5, limits=[1, None]), + dict(name='Spacing', type='float', value=1.0, step=0.1), + ClockParam(name='ClockTemplate'), + ]) + #defs.update(kwds) + pTypes.GroupParameter.__init__(self, **defs) + self.restoreState(kwds, removeChildren=False) + + def buildClocks(self): + clocks = {} + template = self.param('ClockTemplate') + spacing = self['Spacing'] + for i in range(self['Number of Clocks']): + c = template.buildClocks().values()[0] + c.x0 += i * spacing + clocks[self.name() + '%02d' % i] = c + return clocks + + def clockNames(self): + return [self.name() + '%02d' % i for i in range(self['Number of Clocks'])] + +pTypes.registerParameterType('Grid', GridParam) + +class AccelerationGroup(pTypes.GroupParameter): + def __init__(self, **kwds): + defs = dict(name="Acceleration", addText="Add Command..") + pTypes.GroupParameter.__init__(self, **defs) + self.restoreState(kwds, removeChildren=False) + + def addNew(self): + nextTime = 0.0 + if self.hasChildren(): + nextTime = self.children()[-1]['Proper Time'] + 1 + self.addChild(Parameter.create(name='Command', autoIncrementName=True, type=None, renamable=True, removable=True, children=[ + dict(name='Proper Time', type='float', value=nextTime), + dict(name='Acceleration', type='float', value=0.0, step=0.1), + ])) + + def generate(self): + prog = [] + for cmd in self: + prog.append((cmd['Proper Time'], cmd['Acceleration'])) + return prog + +pTypes.registerParameterType('AccelerationGroup', AccelerationGroup) + + +class Clock(object): + nClocks = 0 + + def __init__(self, x0=0.0, y0=0.0, m0=1.0, v0=0.0, t0=0.0, color=None, prog=None, size=0.5): + Clock.nClocks += 1 + self.pen = pg.mkPen(color) + self.brush = pg.mkBrush(color) + self.y0 = y0 + self.x0 = x0 + self.v0 = v0 + self.m0 = m0 + self.t0 = t0 + self.prog = prog + self.size = size + + def init(self, nPts): + ## Keep records of object from inertial frame as well as reference frame + self.inertData = np.empty(nPts, dtype=[('x', float), ('t', float), ('v', float), ('pt', float), ('m', float), ('f', float)]) + self.refData = np.empty(nPts, dtype=[('x', float), ('t', float), ('v', float), ('pt', float), ('m', float), ('f', float)]) + + ## Inertial frame variables + self.x = self.x0 + self.v = self.v0 + self.m = self.m0 + self.t = 0.0 ## reference clock always starts at 0 + self.pt = self.t0 ## proper time starts at t0 + + ## reference frame variables + self.refx = None + self.refv = None + self.refm = None + self.reft = None + + self.recordFrame(0) + + def recordFrame(self, i): + f = self.force() + self.inertData[i] = (self.x, self.t, self.v, self.pt, self.m, f) + self.refData[i] = (self.refx, self.reft, self.refv, self.pt, self.refm, f) + + def force(self, t=None): + if len(self.prog) == 0: + return 0.0 + if t is None: + t = self.pt + + ret = 0.0 + for t1,f in self.prog: + if t >= t1: + ret = f + return ret + + def acceleration(self, t=None): + return self.force(t) / self.m0 + + def accelLimits(self): + ## return the proper time values which bound the current acceleration command + if len(self.prog) == 0: + return -np.inf, np.inf + t = self.pt + ind = -1 + for i, v in enumerate(self.prog): + t1,f = v + if t >= t1: + ind = i + + if ind == -1: + return -np.inf, self.prog[0][0] + elif ind == len(self.prog)-1: + return self.prog[-1][0], np.inf + else: + return self.prog[ind][0], self.prog[ind+1][0] + + + def getCurve(self, ref=True): + + if ref is False: + data = self.inertData + else: + data = self.refData[1:] + + x = data['x'] + y = data['t'] + + curve = pg.PlotCurveItem(x=x, y=y, pen=self.pen) + #x = self.data['x'] - ref.data['x'] + #y = self.data['t'] + + step = 1.0 + #mod = self.data['pt'] % step + #inds = np.argwhere(abs(mod[1:] - mod[:-1]) > step*0.9) + inds = [0] + pt = data['pt'] + for i in range(1,len(pt)): + diff = pt[i] - pt[inds[-1]] + if abs(diff) >= step: + inds.append(i) + inds = np.array(inds) + + #t = self.data['t'][inds] + #x = self.data['x'][inds] + pts = [] + for i in inds: + x = data['x'][i] + y = data['t'][i] + if i+1 < len(data): + dpt = data['pt'][i+1]-data['pt'][i] + dt = data['t'][i+1]-data['t'][i] + else: + dpt = 1 + + if dpt > 0: + c = pg.mkBrush((0,0,0)) + else: + c = pg.mkBrush((200,200,200)) + pts.append({'pos': (x, y), 'brush': c}) + + points = pg.ScatterPlotItem(pts, pen=self.pen, size=7) + + return curve, points + + +class Simulation: + def __init__(self, clocks, ref, duration, dt): + self.clocks = clocks + self.ref = ref + self.duration = duration + self.dt = dt + + @staticmethod + def hypTStep(dt, v0, x0, tau0, g): + ## Hyperbolic step. + ## If an object has proper acceleration g and starts at position x0 with speed v0 and proper time tau0 + ## as seen from an inertial frame, then return the new v, x, tau after time dt has elapsed. + if g == 0: + return v0, x0 + v0*dt, tau0 + dt * (1. - v0**2)**0.5 + v02 = v0**2 + g2 = g**2 + + tinit = v0 / (g * (1 - v02)**0.5) + + B = (1 + (g2 * (dt+tinit)**2))**0.5 + + v1 = g * (dt+tinit) / B + + dtau = (np.arcsinh(g * (dt+tinit)) - np.arcsinh(g * tinit)) / g + + tau1 = tau0 + dtau + + x1 = x0 + (1.0 / g) * ( B - 1. / (1.-v02)**0.5 ) + + return v1, x1, tau1 + + + @staticmethod + def tStep(dt, v0, x0, tau0, g): + ## Linear step. + ## Probably not as accurate as hyperbolic step, but certainly much faster. + gamma = (1. - v0**2)**-0.5 + dtau = dt / gamma + return v0 + dtau * g, x0 + v0*dt, tau0 + dtau + + @staticmethod + def tauStep(dtau, v0, x0, t0, g): + ## linear step in proper time of clock. + ## If an object has proper acceleration g and starts at position x0 with speed v0 at time t0 + ## as seen from an inertial frame, then return the new v, x, t after proper time dtau has elapsed. + + + ## Compute how much t will change given a proper-time step of dtau + gamma = (1. - v0**2)**-0.5 + if g == 0: + dt = dtau * gamma + else: + v0g = v0 * gamma + dt = (np.sinh(dtau * g + np.arcsinh(v0g)) - v0g) / g + + #return v0 + dtau * g, x0 + v0*dt, t0 + dt + v1, x1, t1 = Simulation.hypTStep(dt, v0, x0, t0, g) + return v1, x1, t0+dt + + @staticmethod + def hypIntersect(x0r, t0r, vr, x0, t0, v0, g): + ## given a reference clock (seen from inertial frame) has rx, rt, and rv, + ## and another clock starts at x0, t0, and v0, with acceleration g, + ## compute the intersection time of the object clock's hyperbolic path with + ## the reference plane. + + ## I'm sure we can simplify this... + + if g == 0: ## no acceleration, path is linear (and hyperbola is undefined) + #(-t0r + t0 v0 vr - vr x0 + vr x0r)/(-1 + v0 vr) + + t = (-t0r + t0 *v0 *vr - vr *x0 + vr *x0r)/(-1 + v0 *vr) + return t + + gamma = (1.0-v0**2)**-0.5 + sel = (1 if g>0 else 0) + (1 if vr<0 else 0) + sel = sel%2 + if sel == 0: + #(1/(g^2 (-1 + vr^2)))(-g^2 t0r + g gamma vr + g^2 t0 vr^2 - + #g gamma v0 vr^2 - g^2 vr x0 + + #g^2 vr x0r + \[Sqrt](g^2 vr^2 (1 + gamma^2 (v0 - vr)^2 - vr^2 + + #2 g gamma (v0 - vr) (-t0 + t0r + vr (x0 - x0r)) + + #g^2 (t0 - t0r + vr (-x0 + x0r))^2))) + + t = (1./(g**2 *(-1. + vr**2)))*(-g**2 *t0r + g *gamma *vr + g**2 *t0 *vr**2 - g *gamma *v0 *vr**2 - g**2 *vr *x0 + g**2 *vr *x0r + np.sqrt(g**2 *vr**2 *(1. + gamma**2 *(v0 - vr)**2 - vr**2 + 2 *g *gamma *(v0 - vr)* (-t0 + t0r + vr *(x0 - x0r)) + g**2 *(t0 - t0r + vr* (-x0 + x0r))**2))) + + else: + + #-(1/(g^2 (-1 + vr^2)))(g^2 t0r - g gamma vr - g^2 t0 vr^2 + + #g gamma v0 vr^2 + g^2 vr x0 - + #g^2 vr x0r + \[Sqrt](g^2 vr^2 (1 + gamma^2 (v0 - vr)^2 - vr^2 + + #2 g gamma (v0 - vr) (-t0 + t0r + vr (x0 - x0r)) + + #g^2 (t0 - t0r + vr (-x0 + x0r))^2))) + + t = -(1./(g**2 *(-1. + vr**2)))*(g**2 *t0r - g *gamma* vr - g**2 *t0 *vr**2 + g *gamma *v0 *vr**2 + g**2* vr* x0 - g**2 *vr *x0r + np.sqrt(g**2* vr**2 *(1. + gamma**2 *(v0 - vr)**2 - vr**2 + 2 *g *gamma *(v0 - vr) *(-t0 + t0r + vr *(x0 - x0r)) + g**2 *(t0 - t0r + vr *(-x0 + x0r))**2))) + return t + + def run(self): + nPts = int(self.duration/self.dt)+1 + for cl in self.clocks.itervalues(): + cl.init(nPts) + + if self.ref is None: + self.runInertial(nPts) + else: + self.runReference(nPts) + + def runInertial(self, nPts): + clocks = self.clocks + dt = self.dt + tVals = np.linspace(0, dt*(nPts-1), nPts) + for cl in self.clocks.itervalues(): + for i in xrange(1,nPts): + nextT = tVals[i] + while True: + tau1, tau2 = cl.accelLimits() + x = cl.x + v = cl.v + tau = cl.pt + g = cl.acceleration() + + v1, x1, tau1 = self.hypTStep(dt, v, x, tau, g) + if tau1 > tau2: + dtau = tau2-tau + cl.v, cl.x, cl.t = self.tauStep(dtau, v, x, cl.t, g) + cl.pt = tau2 + else: + cl.v, cl.x, cl.pt = v1, x1, tau1 + cl.t += dt + + if cl.t >= nextT: + cl.refx = cl.x + cl.refv = cl.v + cl.reft = cl.t + cl.recordFrame(i) + break + + + def runReference(self, nPts): + clocks = self.clocks + ref = self.ref + dt = self.dt + dur = self.duration + + ## make sure reference clock is not present in the list of clocks--this will be handled separately. + clocks = clocks.copy() + for k,v in clocks.iteritems(): + if v is ref: + del clocks[k] + break + + ref.refx = 0 + ref.refv = 0 + ref.refm = ref.m0 + + ## These are the set of proper times (in the reference frame) that will be simulated + ptVals = np.linspace(ref.pt, ref.pt + dt*(nPts-1), nPts) + + for i in xrange(1,nPts): + + ## step reference clock ahead one time step in its proper time + nextPt = ptVals[i] ## this is where (when) we want to end up + while True: + tau1, tau2 = ref.accelLimits() + dtau = min(nextPt-ref.pt, tau2-ref.pt) ## do not step past the next command boundary + g = ref.acceleration() + v, x, t = Simulation.tauStep(dtau, ref.v, ref.x, ref.t, g) + ref.pt += dtau + ref.v = v + ref.x = x + ref.t = t + ref.reft = ref.pt + if ref.pt >= nextPt: + break + #else: + #print "Stepped to", tau2, "instead of", nextPt + ref.recordFrame(i) + + ## determine plane visible to reference clock + ## this plane goes through the point ref.x, ref.t and has slope = ref.v + + + ## update all other clocks + for cl in clocks.itervalues(): + while True: + g = cl.acceleration() + tau1, tau2 = cl.accelLimits() + ##Given current position / speed of clock, determine where it will intersect reference plane + #t1 = (ref.v * (cl.x - cl.v * cl.t) + (ref.t - ref.v * ref.x)) / (1. - cl.v) + t1 = Simulation.hypIntersect(ref.x, ref.t, ref.v, cl.x, cl.t, cl.v, g) + dt1 = t1 - cl.t + + ## advance clock by correct time step + v, x, tau = Simulation.hypTStep(dt1, cl.v, cl.x, cl.pt, g) + + ## check to see whether we have gone past an acceleration command boundary. + ## if so, we must instead advance the clock to the boundary and start again + if tau < tau1: + dtau = tau1 - cl.pt + cl.v, cl.x, cl.t = Simulation.tauStep(dtau, cl.v, cl.x, cl.t, g) + cl.pt = tau1-0.000001 + continue + if tau > tau2: + dtau = tau2 - cl.pt + cl.v, cl.x, cl.t = Simulation.tauStep(dtau, cl.v, cl.x, cl.t, g) + cl.pt = tau2 + continue + + ## Otherwise, record the new values and exit the loop + cl.v = v + cl.x = x + cl.pt = tau + cl.t = t1 + cl.m = None + break + + ## transform position into reference frame + x = cl.x - ref.x + t = cl.t - ref.t + gamma = (1.0 - ref.v**2) ** -0.5 + vg = -ref.v * gamma + + cl.refx = gamma * (x - ref.v * t) + cl.reft = ref.pt # + gamma * (t - ref.v * x) # this term belongs here, but it should always be equal to 0. + cl.refv = (cl.v - ref.v) / (1.0 - cl.v * ref.v) + cl.refm = None + cl.recordFrame(i) + + t += dt + + def plot(self, plot): + plot.clear() + for cl in self.clocks.itervalues(): + c, p = cl.getCurve() + plot.addItem(c) + plot.addItem(p) + +class Animation(pg.ItemGroup): + def __init__(self, sim): + pg.ItemGroup.__init__(self) + self.sim = sim + self.clocks = sim.clocks + + self.items = {} + for name, cl in self.clocks.items(): + item = ClockItem(cl) + self.addItem(item) + self.items[name] = item + + #self.timer = timer + #self.timer.timeout.connect(self.step) + + #def run(self, run): + #if not run: + #self.timer.stop() + #else: + #self.timer.start(self.dt) + + def restart(self): + for cl in self.items.values(): + cl.reset() + + def stepTo(self, t): + for i in self.items.values(): + i.stepTo(t) + + +class ClockItem(pg.ItemGroup): + def __init__(self, clock): + pg.ItemGroup.__init__(self) + self.size = clock.size + self.item = QtGui.QGraphicsEllipseItem(QtCore.QRectF(0, 0, self.size, self.size)) + self.item.translate(-self.size*0.5, -self.size*0.5) + self.item.setPen(pg.mkPen(100,100,100)) + self.item.setBrush(clock.brush) + self.hand = QtGui.QGraphicsLineItem(0, 0, 0, self.size*0.5) + self.hand.setPen(pg.mkPen('w')) + self.hand.setZValue(10) + self.flare = QtGui.QGraphicsPolygonItem(QtGui.QPolygonF([ + QtCore.QPointF(0, -self.size*0.25), + QtCore.QPointF(0, self.size*0.25), + QtCore.QPointF(self.size*1.5, 0), + QtCore.QPointF(0, -self.size*0.25), + ])) + self.flare.setPen(pg.mkPen('y')) + self.flare.setBrush(pg.mkBrush(255,150,0)) + self.flare.setZValue(-10) + self.addItem(self.hand) + self.addItem(self.item) + self.addItem(self.flare) + + self.clock = clock + self.i = 1 + + self._spaceline = None + + + def spaceline(self): + if self._spaceline is None: + self._spaceline = pg.InfiniteLine() + self._spaceline.setPen(self.clock.pen) + return self._spaceline + + def stepTo(self, t): + data = self.clock.refData + + while self.i < len(data)-1 and data['t'][self.i] < t: + self.i += 1 + while self.i > 1 and data['t'][self.i-1] >= t: + self.i -= 1 + + self.setPos(data['x'][self.i], self.clock.y0) + + t = data['pt'][self.i] + self.hand.setRotation(-0.25 * t * 360.) + + self.resetTransform() + v = data['v'][self.i] + gam = (1.0 - v**2)**0.5 + self.scale(gam, 1.0) + + f = data['f'][self.i] + self.flare.resetTransform() + if f < 0: + self.flare.translate(self.size*0.4, 0) + else: + self.flare.translate(-self.size*0.4, 0) + + self.flare.scale(-f * (0.5+np.random.random()*0.1), 1.0) + + if self._spaceline is not None: + self._spaceline.setPos(pg.Point(data['x'][self.i], data['t'][self.i])) + self._spaceline.setAngle(data['v'][self.i] * 45.) + + + def reset(self): + self.i = 1 + + +#class Spaceline(pg.InfiniteLine): + #def __init__(self, sim, frame): + #self.sim = sim + #self.frame = frame + #pg.InfiniteLine.__init__(self) + #self.setPen(sim.clocks[frame].pen) + + #def stepTo(self, t): + #self.setAngle(0) + + #pass + +if __name__ == '__main__': + pg.mkQApp() + #import pyqtgraph.console + #cw = pyqtgraph.console.ConsoleWidget() + #cw.show() + #cw.catchNextException() + win = RelativityGUI() + win.setWindowTitle("Relativity!") + win.show() + win.resize(1100,700) + + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() + + + #win.params.param('Objects').restoreState(state, removeChildren=False) + From 9a5e526c616762b5d4a0d6a39fc64d4ec1b1cbaa Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 16 Jun 2014 12:41:36 -0600 Subject: [PATCH 229/268] Update CONTRIB with instructions on checking style --- CONTRIBUTING.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index f0ab3416..0b4b1beb 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -28,6 +28,9 @@ Please use the following guidelines when preparing changes: * PyQtGraph prefers PEP8 for most style issues, but this is not enforced rigorously as long as the code is clean and readable. + * Use `python setup.py style` to see whether your code follows + the mandatory style guidelines checked by flake8. + * Exception 1: All variable names should use camelCase rather than underscore_separation. This is done for consistency with Qt From 3c2d1d4a0dcee2ad315b20448d2d4fa36c1dc2ec Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 20 Jun 2014 23:04:21 -0400 Subject: [PATCH 230/268] Added GLVolumeItem.setData --- CHANGELOG | 1 + pyqtgraph/opengl/items/GLVolumeItem.py | 27 +++++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1a1ba126..3f5ddb07 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -53,6 +53,7 @@ pyqtgraph-0.9.9 [unreleased] - Added ViewBox.invertX() - Docks now have optional close button - Added InfiniteLine.setHoverPen + - Added GLVolumeItem.setData Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/opengl/items/GLVolumeItem.py b/pyqtgraph/opengl/items/GLVolumeItem.py index 84f23e12..cbe22db9 100644 --- a/pyqtgraph/opengl/items/GLVolumeItem.py +++ b/pyqtgraph/opengl/items/GLVolumeItem.py @@ -2,6 +2,7 @@ from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem from ...Qt import QtGui import numpy as np +from ... import debug __all__ = ['GLVolumeItem'] @@ -25,13 +26,22 @@ class GLVolumeItem(GLGraphicsItem): self.sliceDensity = sliceDensity self.smooth = smooth - self.data = data + self.data = None + self._needUpload = False + self.texture = None GLGraphicsItem.__init__(self) self.setGLOptions(glOptions) + self.setData(data) - def initializeGL(self): + def setData(self, data): + self.data = data + self._needUpload = True + self.update() + + def _uploadData(self): glEnable(GL_TEXTURE_3D) - self.texture = glGenTextures(1) + if self.texture is None: + self.texture = glGenTextures(1) glBindTexture(GL_TEXTURE_3D, self.texture) if self.smooth: glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) @@ -60,9 +70,16 @@ class GLVolumeItem(GLGraphicsItem): glNewList(l, GL_COMPILE) self.drawVolume(ax, d) glEndList() - - + + self._needUpload = False + def paint(self): + if self.data is None: + return + + if self._needUpload: + self._uploadData() + self.setupGLState() glEnable(GL_TEXTURE_3D) From b20dc0cf6f0f5ee28576c39251f42225a3a25d05 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 22 Jun 2014 21:27:48 -0400 Subject: [PATCH 231/268] Added methods for saving / restoring state of PloyLineROI --- CHANGELOG | 1 + pyqtgraph/graphicsItems/ROI.py | 49 ++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3f5ddb07..f9f9466f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -54,6 +54,7 @@ pyqtgraph-0.9.9 [unreleased] - Docks now have optional close button - Added InfiniteLine.setHoverPen - Added GLVolumeItem.setData + - Added PolyLineROI.setPoints, clearPoints, saveState, setState Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index d51f75df..f3ebd992 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1804,13 +1804,56 @@ class PolyLineROI(ROI): self.segments = [] ROI.__init__(self, pos, size=[1,1], **args) - for p in positions: - self.addFreeHandle(p) + self.setPoints(positions) + #for p in positions: + #self.addFreeHandle(p) + #start = -1 if self.closed else 0 + #for i in range(start, len(self.handles)-1): + #self.addSegment(self.handles[i]['item'], self.handles[i+1]['item']) + + def setPoints(self, points, closed=None): + """ + Set the complete sequence of points displayed by this ROI. + + ============= ========================================================= + **Arguments** + points List of (x,y) tuples specifying handle locations to set. + closed If bool, then this will set whether the ROI is closed + (the last point is connected to the first point). If + None, then the closed mode is left unchanged. + ============= ========================================================= + + """ + if closed is not None: + self.closed = closed + + for p in points: + self.addFreeHandle(p) + start = -1 if self.closed else 0 for i in range(start, len(self.handles)-1): self.addSegment(self.handles[i]['item'], self.handles[i+1]['item']) + + + def clearPoints(self): + """ + Remove all handles and segments. + """ + while len(self.handles) > 0: + self.removeHandle(self.handles[0]['item']) + def saveState(self): + state = ROI.saveState(self) + state['closed'] = self.closed + state['points'] = [tuple(h.pos()) for h in self.getHandles()] + return state + + def setState(self, state): + ROI.setState(self, state) + self.clearPoints() + self.setPoints(state['points'], closed=state['closed']) + def addSegment(self, h1, h2, index=None): seg = LineSegmentROI(handles=(h1, h2), pen=self.pen, parent=self, movable=False) if index is None: @@ -1936,6 +1979,8 @@ class PolyLineROI(ROI): for seg in self.segments: seg.setPen(*args, **kwds) + + class LineSegmentROI(ROI): """ ROI subclass with two freely-moving handles defining a line. From 8b0a866ad9a0a4807c4836c577ce65ecac5fd283 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Jun 2014 10:55:55 -0400 Subject: [PATCH 232/268] Add ErrorBarItem.setData --- CHANGELOG | 1 + examples/ErrorBarItem.py | 2 +- pyqtgraph/graphicsItems/ErrorBarItem.py | 35 +++++++++++++++++-------- pyqtgraph/graphicsItems/GraphicsItem.py | 2 ++ 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f9f9466f..e574e479 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -55,6 +55,7 @@ pyqtgraph-0.9.9 [unreleased] - Added InfiniteLine.setHoverPen - Added GLVolumeItem.setData - Added PolyLineROI.setPoints, clearPoints, saveState, setState + - Added ErrorBarItem.setData Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/examples/ErrorBarItem.py b/examples/ErrorBarItem.py index 3bbf06d1..cd576d51 100644 --- a/examples/ErrorBarItem.py +++ b/examples/ErrorBarItem.py @@ -7,7 +7,7 @@ Demonstrates basic use of ErrorBarItem import initExample ## Add path to library (just for examples; you do not need this) import pyqtgraph as pg -from pyqtgraph.Qt import QtGui +from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 7b681389..d7cb06db 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -8,15 +8,7 @@ __all__ = ['ErrorBarItem'] class ErrorBarItem(GraphicsObject): def __init__(self, **opts): """ - Valid keyword options are: - x, y, height, width, top, bottom, left, right, beam, pen - - x and y must be numpy arrays specifying the coordinates of data points. - height, width, top, bottom, left, right, and beam may be numpy arrays, - single values, or None to disable. All values should be positive. - - If height is specified, it overrides top and bottom. - If width is specified, it overrides left and right. + All keyword arguments are passed to setData(). """ GraphicsObject.__init__(self) self.opts = dict( @@ -31,14 +23,35 @@ class ErrorBarItem(GraphicsObject): beam=None, pen=None ) - self.setOpts(**opts) + self.setData(**opts) + + def setData(self, **opts): + """ + Update the data in the item. All arguments are optional. - def setOpts(self, **opts): + Valid keyword options are: + x, y, height, width, top, bottom, left, right, beam, pen + + * x and y must be numpy arrays specifying the coordinates of data points. + * height, width, top, bottom, left, right, and beam may be numpy arrays, + single values, or None to disable. All values should be positive. + * top, bottom, left, and right specify the lengths of bars extending + in each direction. + * If height is specified, it overrides top and bottom. + * If width is specified, it overrides left and right. + * beam specifies the width of the beam at the end of each bar. + * pen may be any single argument accepted by pg.mkPen(). + """ self.opts.update(opts) self.path = None self.update() + self.prepareGeometryChange() self.informViewBoundsChanged() + def setOpts(self, **opts): + # for backward compatibility + self.setData(**opts) + def drawPath(self): p = QtGui.QPainterPath() diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 9fa323e2..c1a96a62 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -318,6 +318,8 @@ class GraphicsItem(object): vt = self.deviceTransform() if vt is None: return None + if isinstance(obj, QtCore.QPoint): + obj = QtCore.QPointF(obj) vt = fn.invertQTransform(vt) return vt.map(obj) From d7a1ae1d5265b1d3cbc1d94871ea490050f2c67f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Jun 2014 10:57:52 -0400 Subject: [PATCH 233/268] Note about version method was added --- pyqtgraph/graphicsItems/ErrorBarItem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index d7cb06db..986c5140 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -41,6 +41,8 @@ class ErrorBarItem(GraphicsObject): * If width is specified, it overrides left and right. * beam specifies the width of the beam at the end of each bar. * pen may be any single argument accepted by pg.mkPen(). + + This method was added in version 0.9.9. For prior versions, use setOpts. """ self.opts.update(opts) self.path = None From 26730ad94757554ffb55411ada41f92cfa175c61 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Jun 2014 11:05:05 -0400 Subject: [PATCH 234/268] Link ErrorBarItem in documentation --- doc/source/graphicsItems/errorbaritem.rst | 8 ++++++++ doc/source/graphicsItems/index.rst | 1 + 2 files changed, 9 insertions(+) create mode 100644 doc/source/graphicsItems/errorbaritem.rst diff --git a/doc/source/graphicsItems/errorbaritem.rst b/doc/source/graphicsItems/errorbaritem.rst new file mode 100644 index 00000000..be68a5dd --- /dev/null +++ b/doc/source/graphicsItems/errorbaritem.rst @@ -0,0 +1,8 @@ +ErrorBarItem +============ + +.. autoclass:: pyqtgraph.ErrorBarItem + :members: + + .. automethod:: pyqtgraph.ErrorBarItem.__init__ + diff --git a/doc/source/graphicsItems/index.rst b/doc/source/graphicsItems/index.rst index 970e9500..7042d27e 100644 --- a/doc/source/graphicsItems/index.rst +++ b/doc/source/graphicsItems/index.rst @@ -23,6 +23,7 @@ Contents: isocurveitem axisitem textitem + errorbaritem arrowitem fillbetweenitem curvepoint From 6e4b2f4ff4240df2bcad1f9cd8cc75c7d92b5ae6 Mon Sep 17 00:00:00 2001 From: Thomas Feldmann Date: Sat, 5 Jul 2014 17:00:53 +0200 Subject: [PATCH 235/268] Fixed typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2dff1031..990664c0 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Installation Methods 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) + * For installation packages, see the website (pyqtgraph.org) * On debian-like systems, pyqtgraph requires the following packages: python-numpy, python-qt4 | python-pyside For 3D support: python-opengl, python-qt4-gl | python-pyside.qtopengl From ed0b95602adf9322564be63f782c495b98b2dea6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 6 Jul 2014 11:00:38 -0400 Subject: [PATCH 236/268] Correct LegendItem addItem to use next available row --- pyqtgraph/graphicsItems/LegendItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index ea6798fb..20d6416e 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -75,7 +75,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): sample = item else: sample = ItemSample(item) - row = len(self.items) + row = self.layout.rowCount() self.items.append((sample, label)) self.layout.addItem(sample, row, 0) self.layout.addItem(label, row, 1) From 8268ccfa655feef7e2f6b5128b012f53201474d1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 6 Jul 2014 11:44:26 -0400 Subject: [PATCH 237/268] Added GLImageItem.setData() --- pyqtgraph/opengl/items/GLImageItem.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py index 2cab23a3..59ddaf6f 100644 --- a/pyqtgraph/opengl/items/GLImageItem.py +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -25,13 +25,21 @@ class GLImageItem(GLGraphicsItem): """ self.smooth = smooth - self.data = data + self._needUpdate = False GLGraphicsItem.__init__(self) + self.setData(data) self.setGLOptions(glOptions) def initializeGL(self): glEnable(GL_TEXTURE_2D) self.texture = glGenTextures(1) + + def setData(self, data): + self.data = data + self._needUpdate = True + self.update() + + def _updateTexture(self): glBindTexture(GL_TEXTURE_2D, self.texture) if self.smooth: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) @@ -63,7 +71,8 @@ class GLImageItem(GLGraphicsItem): def paint(self): - + if self._needUpdate: + self._updateTexture() glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.texture) From 74b5ba6f7e97d9b6d1b64dfe384a26999fa2e072 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 25 Jul 2014 07:52:58 -0400 Subject: [PATCH 238/268] Add AxisItem.setTickSpacing() --- pyqtgraph/graphicsItems/AxisItem.py | 57 +++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 5eef4ae0..ededed56 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -55,6 +55,8 @@ class AxisItem(GraphicsWidget): ], 'showValues': showValues, 'tickLength': maxTickLength, + 'maxTickLevel': 2, + 'maxTextLevel': 2, } self.textWidth = 30 ## Keeps track of maximum width / height of tick text @@ -68,6 +70,7 @@ class AxisItem(GraphicsWidget): self.tickFont = None self._tickLevels = None ## used to override the automatic ticking system with explicit ticks + self._tickSpacing = None # used to override default tickSpacing method self.scale = 1.0 self.autoSIPrefix = True self.autoSIPrefixScale = 1.0 @@ -517,6 +520,37 @@ class AxisItem(GraphicsWidget): self.picture = None self.update() + def setTickSpacing(self, major=None, minor=None, levels=None): + """ + Explicitly determine the spacing of major and minor ticks. This + overrides the default behavior of the tickSpacing method, and disables + the effect of setTicks(). Arguments may be either *major* and *minor*, + or *levels* which is a list of (spacing, offset) tuples for each + tick level desired. + + If no arguments are given, then the default behavior of tickSpacing + is enabled. + + Examples:: + + # two levels, all offsets = 0 + axis.setTickSpacing(5, 1) + # three levels, all offsets = 0 + axis.setTickSpacing([(3, 0), (1, 0), (0.25, 0)]) + # reset to default + axis.setTickSpacing() + """ + + if levels is None: + if major is None: + levels = None + else: + levels = [(major, 0), (minor, 0)] + self._tickSpacing = levels + self.picture = None + self.update() + + def tickSpacing(self, minVal, maxVal, size): """Return values describing the desired spacing and offset of ticks. @@ -532,6 +566,10 @@ class AxisItem(GraphicsWidget): ... ] """ + # First check for override tick spacing + if self._tickSpacing is not None: + return self._tickSpacing + dif = abs(maxVal - minVal) if dif == 0: return [] @@ -557,12 +595,13 @@ class AxisItem(GraphicsWidget): #(intervals[minorIndex], 0) ## Pretty, but eats up CPU ] - ## decide whether to include the last level of ticks - minSpacing = min(size / 20., 30.) - maxTickCount = size / minSpacing - if dif / intervals[minorIndex] <= maxTickCount: - levels.append((intervals[minorIndex], 0)) - return levels + if self.style['maxTickLevel'] >= 2: + ## decide whether to include the last level of ticks + minSpacing = min(size / 20., 30.) + maxTickCount = size / minSpacing + if dif / intervals[minorIndex] <= maxTickCount: + levels.append((intervals[minorIndex], 0)) + return levels @@ -588,8 +627,6 @@ class AxisItem(GraphicsWidget): #(intervals[intIndexes[0]], 0) #] - - def tickValues(self, minVal, maxVal, size): """ Return the values and spacing of ticks to draw:: @@ -763,8 +800,6 @@ class AxisItem(GraphicsWidget): values.append(val) strings.append(strn) - textLevel = 1 ## draw text at this scale level - ## determine mapping between tick values and local coordinates dif = self.range[1] - self.range[0] if dif == 0: @@ -853,7 +888,7 @@ class AxisItem(GraphicsWidget): if not self.style['showValues']: return (axisSpec, tickSpecs, textSpecs) - for i in range(len(tickLevels)): + for i in range(min(len(tickLevels), self.style['maxTextLevel']+1)): ## Get the list of strings to display for this level if tickStrings is None: spacing, values = tickLevels[i] From 55a07b0bec998adc2f63a87befeab0435679961e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 29 Jul 2014 23:57:34 -0400 Subject: [PATCH 239/268] Correction in GraphicsItem.deviceTransform() during export. This fixes some (all?) issues with exporting ScatterPlotItem. --- pyqtgraph/graphicsItems/GraphicsItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index c1a96a62..2ca35193 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -102,7 +102,7 @@ class GraphicsItem(object): Extends deviceTransform to automatically determine the viewportTransform. """ if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different. - return self._exportOpts['painter'].deviceTransform() + return self._exportOpts['painter'].deviceTransform() * self.sceneTransform() if viewportTransform is None: view = self.getViewWidget() From 753ac9b4c4e19e417b32b7d82d056e4bc0117b54 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 7 Aug 2014 09:03:26 -0400 Subject: [PATCH 240/268] Squashed commit of the following: commit ca3fbe2ff9d07162e4cc4488f4280f0b189f86e8 Author: Luke Campagnola Date: Thu Aug 7 08:41:30 2014 -0400 Merged numerous updates from acq4: * Added HDF5 exporter * CSV exporter gets (x,y,y,y) export mode * Updates to SVG, Matplotlib exporter * Console can filter exceptions by string * Added tick context menu to GradientEditorItem * Added export feature to imageview * Parameter trees: - Option to save only user-editable values - Option to set visible title of parameters separately from name - Added experimental ParameterSystem for handling large systems of interdependent parameters - Auto-select editable portion of spinbox when editing * Added Vector.__abs__ * Added replacement garbage collector for avoiding crashes on multithreaded Qt * Fixed "illegal instruction" caused by closing file handle 7 on OSX * configfile now reloads QtCore objects, Point, ColorMap, numpy arrays * Avoid triggering recursion issues in exception handler * Various bugfies and performance enhancements --- pyqtgraph/Vector.py | 2 + pyqtgraph/__init__.py | 9 +- pyqtgraph/canvas/Canvas.py | 18 +- pyqtgraph/canvas/CanvasTemplate.ui | 28 +- pyqtgraph/canvas/CanvasTemplate_pyqt.py | 53 +-- pyqtgraph/colormap.py | 5 +- pyqtgraph/configfile.py | 17 +- pyqtgraph/console/Console.py | 11 + pyqtgraph/console/template.ui | 42 +- pyqtgraph/console/template_pyqt.py | 35 +- pyqtgraph/debug.py | 127 +++++- pyqtgraph/exceptionHandling.py | 56 ++- pyqtgraph/exporters/CSVExporter.py | 32 +- pyqtgraph/exporters/HDF5Exporter.py | 58 +++ pyqtgraph/exporters/Matplotlib.py | 61 ++- pyqtgraph/exporters/SVGExporter.py | 33 +- pyqtgraph/exporters/__init__.py | 2 +- pyqtgraph/flowchart/Flowchart.py | 57 +-- pyqtgraph/flowchart/library/Data.py | 4 +- pyqtgraph/flowchart/library/Filters.py | 70 +++- pyqtgraph/flowchart/library/common.py | 36 ++ pyqtgraph/flowchart/library/functions.py | 2 +- pyqtgraph/functions.py | 92 +++++ pyqtgraph/graphicsItems/AxisItem.py | 16 +- pyqtgraph/graphicsItems/GradientEditorItem.py | 127 +++--- pyqtgraph/graphicsItems/ImageItem.py | 11 +- pyqtgraph/graphicsItems/PlotDataItem.py | 18 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 14 +- pyqtgraph/graphicsItems/ROI.py | 112 ++++- pyqtgraph/graphicsItems/ScaleBar.py | 61 +-- pyqtgraph/graphicsItems/ScatterPlotItem.py | 10 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 33 +- pyqtgraph/imageview/ImageView.py | 51 ++- pyqtgraph/imageview/ImageViewTemplate.ui | 7 +- pyqtgraph/imageview/ImageViewTemplate_pyqt.py | 19 +- .../imageview/ImageViewTemplate_pyside.py | 19 +- pyqtgraph/metaarray/MetaArray.py | 72 ++-- pyqtgraph/multiprocess/remoteproxy.py | 223 ++++++---- pyqtgraph/parametertree/Parameter.py | 104 +++-- pyqtgraph/parametertree/ParameterItem.py | 24 +- pyqtgraph/parametertree/ParameterSystem.py | 127 ++++++ pyqtgraph/parametertree/SystemSolver.py | 381 ++++++++++++++++++ pyqtgraph/parametertree/__init__.py | 2 +- pyqtgraph/parametertree/parameterTypes.py | 13 +- pyqtgraph/tests/test_functions.py | 13 + pyqtgraph/util/garbage_collector.py | 50 +++ pyqtgraph/widgets/ColorMapWidget.py | 47 ++- pyqtgraph/widgets/ComboBox.py | 5 + pyqtgraph/widgets/DataTreeWidget.py | 2 +- pyqtgraph/widgets/SpinBox.py | 30 +- pyqtgraph/widgets/TableWidget.py | 13 +- 51 files changed, 1913 insertions(+), 541 deletions(-) create mode 100644 pyqtgraph/exporters/HDF5Exporter.py create mode 100644 pyqtgraph/parametertree/ParameterSystem.py create mode 100644 pyqtgraph/parametertree/SystemSolver.py create mode 100644 pyqtgraph/util/garbage_collector.py diff --git a/pyqtgraph/Vector.py b/pyqtgraph/Vector.py index b18b3091..f2898e80 100644 --- a/pyqtgraph/Vector.py +++ b/pyqtgraph/Vector.py @@ -81,5 +81,7 @@ class Vector(QtGui.QVector3D): # ang *= -1. return ang * 180. / np.pi + def __abs__(self): + return Vector(abs(self.x()), abs(self.y()), abs(self.z())) \ No newline at end of file diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 01e84c49..f8983455 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -325,8 +325,13 @@ def exit(): atexit._run_exitfuncs() ## close file handles - os.closerange(3, 4096) ## just guessing on the maximum descriptor count.. - + if sys.platform == 'darwin': + for fd in xrange(3, 4096): + if fd not in [7]: # trying to close 7 produces an illegal instruction on the Mac. + os.close(fd) + else: + os.closerange(3, 4096) ## just guessing on the maximum descriptor count.. + os._exit(0) diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index d07b3428..4de891f7 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -67,8 +67,8 @@ class Canvas(QtGui.QWidget): self.ui.itemList.sigItemMoved.connect(self.treeItemMoved) self.ui.itemList.itemSelectionChanged.connect(self.treeItemSelected) self.ui.autoRangeBtn.clicked.connect(self.autoRange) - self.ui.storeSvgBtn.clicked.connect(self.storeSvg) - self.ui.storePngBtn.clicked.connect(self.storePng) + #self.ui.storeSvgBtn.clicked.connect(self.storeSvg) + #self.ui.storePngBtn.clicked.connect(self.storePng) self.ui.redirectCheck.toggled.connect(self.updateRedirect) self.ui.redirectCombo.currentIndexChanged.connect(self.updateRedirect) self.multiSelectBox.sigRegionChanged.connect(self.multiSelectBoxChanged) @@ -94,11 +94,13 @@ class Canvas(QtGui.QWidget): self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent - def storeSvg(self): - self.ui.view.writeSvg() + #def storeSvg(self): + #from pyqtgraph.GraphicsScene.exportDialog import ExportDialog + #ex = ExportDialog(self.ui.view) + #ex.show() - def storePng(self): - self.ui.view.writeImage() + #def storePng(self): + #self.ui.view.writeImage() def splitterMoved(self): self.resizeEvent() @@ -571,7 +573,9 @@ class Canvas(QtGui.QWidget): self.menu.popup(ev.globalPos()) def removeClicked(self): - self.removeItem(self.menuItem) + #self.removeItem(self.menuItem) + for item in self.selectedItems(): + self.removeItem(item) self.menuItem = None import gc gc.collect() diff --git a/pyqtgraph/canvas/CanvasTemplate.ui b/pyqtgraph/canvas/CanvasTemplate.ui index 218cf48d..9bea8f89 100644 --- a/pyqtgraph/canvas/CanvasTemplate.ui +++ b/pyqtgraph/canvas/CanvasTemplate.ui @@ -28,21 +28,7 @@ - - - - Store SVG - - - - - - - Store PNG - - - - + @@ -55,7 +41,7 @@ - + 0 @@ -75,7 +61,7 @@ - + @@ -93,28 +79,28 @@ - + 0 - + Reset Transforms - + Mirror Selection - + MirrorXY diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/pyqtgraph/canvas/CanvasTemplate_pyqt.py index c809cb1d..557354e0 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'acq4/pyqtgraph/canvas/CanvasTemplate.ui' # -# Created: Mon Dec 23 10:10:52 2013 -# by: PyQt4 UI code generator 4.10 +# Created: Thu Jan 2 11:13:07 2014 +# by: PyQt4 UI code generator 4.9 # # WARNING! All changes made in this file will be lost! @@ -12,16 +12,7 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - def _fromUtf8(s): - return s - -try: - _encoding = QtGui.QApplication.UnicodeUTF8 - def _translate(context, text, disambig): - return QtGui.QApplication.translate(context, text, disambig, _encoding) -except AttributeError: - def _translate(context, text, disambig): - return QtGui.QApplication.translate(context, text, disambig) + _fromUtf8 = lambda s: s class Ui_Form(object): def setupUi(self, Form): @@ -41,12 +32,6 @@ class Ui_Form(object): self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) self.gridLayout_2.setMargin(0) self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) - self.storeSvgBtn = QtGui.QPushButton(self.layoutWidget) - self.storeSvgBtn.setObjectName(_fromUtf8("storeSvgBtn")) - self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1) - self.storePngBtn = QtGui.QPushButton(self.layoutWidget) - self.storePngBtn.setObjectName(_fromUtf8("storePngBtn")) - self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1) self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -54,7 +39,7 @@ class Ui_Form(object): sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setObjectName(_fromUtf8("autoRangeBtn")) - self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2) + self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) @@ -64,7 +49,7 @@ class Ui_Form(object): self.redirectCombo = CanvasCombo(self.layoutWidget) self.redirectCombo.setObjectName(_fromUtf8("redirectCombo")) self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2) + self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) self.itemList = TreeWidget(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) @@ -74,35 +59,33 @@ class Ui_Form(object): self.itemList.setHeaderHidden(True) self.itemList.setObjectName(_fromUtf8("itemList")) self.itemList.headerItem().setText(0, _fromUtf8("1")) - self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2) + self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) self.ctrlLayout = QtGui.QGridLayout() self.ctrlLayout.setSpacing(0) self.ctrlLayout.setObjectName(_fromUtf8("ctrlLayout")) - self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2) + self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget) self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn")) - self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1) + self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn")) - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget) self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn")) - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) - self.storeSvgBtn.setText(_translate("Form", "Store SVG", None)) - self.storePngBtn.setText(_translate("Form", "Store PNG", None)) - self.autoRangeBtn.setText(_translate("Form", "Auto Range", None)) - self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas.", None)) - self.redirectCheck.setText(_translate("Form", "Redirect", None)) - self.resetTransformsBtn.setText(_translate("Form", "Reset Transforms", None)) - self.mirrorSelectionBtn.setText(_translate("Form", "Mirror Selection", None)) - self.reflectSelectionBtn.setText(_translate("Form", "MirrorXY", None)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8)) + self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8)) + self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8)) + self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8)) + self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) + self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) from ..widgets.TreeWidget import TreeWidget from CanvasManager import CanvasCombo diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index 446044e1..c0033708 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -244,4 +244,7 @@ class ColorMap(object): else: return np.all(self.color == np.array([[0,0,0,255], [255,255,255,255]])) - + def __repr__(self): + pos = repr(self.pos).replace('\n', '') + color = repr(self.color).replace('\n', '') + return "ColorMap(%s, %s)" % (pos, color) diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index f709c786..c095bba3 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -14,6 +14,10 @@ from .pgcollections import OrderedDict GLOBAL_PATH = None # so not thread safe. from . import units from .python2_3 import asUnicode +from .Qt import QtCore +from .Point import Point +from .colormap import ColorMap +import numpy class ParseError(Exception): def __init__(self, message, lineNum, line, fileName=None): @@ -46,7 +50,7 @@ def readConfigFile(fname): fname2 = os.path.join(GLOBAL_PATH, fname) if os.path.exists(fname2): fname = fname2 - + GLOBAL_PATH = os.path.dirname(os.path.abspath(fname)) try: @@ -135,6 +139,17 @@ def parseString(lines, start=0): local = units.allUnits.copy() local['OrderedDict'] = OrderedDict local['readConfigFile'] = readConfigFile + local['Point'] = Point + local['QtCore'] = QtCore + local['ColorMap'] = ColorMap + # Needed for reconstructing numpy arrays + local['array'] = numpy.array + for dtype in ['int8', 'uint8', + 'int16', 'uint16', 'float16', + 'int32', 'uint32', 'float32', + 'int64', 'uint64', 'float64']: + local[dtype] = getattr(numpy, dtype) + if len(k) < 1: raise ParseError('Missing name preceding colon', ln+1, l) if k[0] == '(' and k[-1] == ')': ## If the key looks like a tuple, try evaluating it. diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 6d77c4cf..896de924 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -341,6 +341,17 @@ class ConsoleWidget(QtGui.QWidget): filename = tb.tb_frame.f_code.co_filename function = tb.tb_frame.f_code.co_name + filterStr = str(self.ui.filterText.text()) + if filterStr != '': + if isinstance(exc, Exception): + msg = exc.message + elif isinstance(exc, basestring): + msg = exc + else: + msg = repr(exc) + match = re.search(filterStr, "%s:%s:%s" % (filename, function, msg)) + return match is not None + ## Go through a list of common exception points we like to ignore: if excType is GeneratorExit or excType is StopIteration: return False diff --git a/pyqtgraph/console/template.ui b/pyqtgraph/console/template.ui index 6e5c5be3..1a672c5e 100644 --- a/pyqtgraph/console/template.ui +++ b/pyqtgraph/console/template.ui @@ -6,7 +6,7 @@ 0 0 - 710 + 694 497 @@ -89,6 +89,16 @@ 0 + + + + false + + + Clear Exception + + + @@ -109,7 +119,7 @@ - + Only Uncaught Exceptions @@ -119,14 +129,14 @@ - + true - + Run commands in selected stack frame @@ -136,24 +146,14 @@ - + Exception Info - - - - false - - - Clear Exception - - - - + Qt::Horizontal @@ -166,6 +166,16 @@ + + + + Filter (regex): + + + + + + diff --git a/pyqtgraph/console/template_pyqt.py b/pyqtgraph/console/template_pyqt.py index e0852c93..354fb1d6 100644 --- a/pyqtgraph/console/template_pyqt.py +++ b/pyqtgraph/console/template_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/console/template.ui' +# Form implementation generated from reading ui file 'template.ui' # -# Created: Mon Dec 23 10:10:53 2013 -# by: PyQt4 UI code generator 4.10 +# Created: Fri May 02 18:55:28 2014 +# by: PyQt4 UI code generator 4.10.4 # # WARNING! All changes made in this file will be lost! @@ -26,7 +26,7 @@ except AttributeError: class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) - Form.resize(710, 497) + Form.resize(694, 497) self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout.setMargin(0) self.gridLayout.setSpacing(0) @@ -71,6 +71,10 @@ class Ui_Form(object): self.gridLayout_2.setSpacing(0) self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup) + self.clearExceptionBtn.setEnabled(False) + self.clearExceptionBtn.setObjectName(_fromUtf8("clearExceptionBtn")) + self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) self.catchAllExceptionsBtn = QtGui.QPushButton(self.exceptionGroup) self.catchAllExceptionsBtn.setCheckable(True) self.catchAllExceptionsBtn.setObjectName(_fromUtf8("catchAllExceptionsBtn")) @@ -82,24 +86,26 @@ class Ui_Form(object): self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup) self.onlyUncaughtCheck.setChecked(True) self.onlyUncaughtCheck.setObjectName(_fromUtf8("onlyUncaughtCheck")) - self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1) + self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) self.exceptionStackList = QtGui.QListWidget(self.exceptionGroup) self.exceptionStackList.setAlternatingRowColors(True) self.exceptionStackList.setObjectName(_fromUtf8("exceptionStackList")) - self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5) + self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) self.runSelectedFrameCheck = QtGui.QCheckBox(self.exceptionGroup) self.runSelectedFrameCheck.setChecked(True) self.runSelectedFrameCheck.setObjectName(_fromUtf8("runSelectedFrameCheck")) - self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5) + self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup) self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel")) - self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) - self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup) - self.clearExceptionBtn.setEnabled(False) - self.clearExceptionBtn.setObjectName(_fromUtf8("clearExceptionBtn")) - self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1) + self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1) + self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) + self.label = QtGui.QLabel(self.exceptionGroup) + self.label.setObjectName(_fromUtf8("label")) + self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) + self.filterText = QtGui.QLineEdit(self.exceptionGroup) + self.filterText.setObjectName(_fromUtf8("filterText")) + self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) @@ -110,11 +116,12 @@ class Ui_Form(object): self.historyBtn.setText(_translate("Form", "History..", None)) self.exceptionBtn.setText(_translate("Form", "Exceptions..", None)) self.exceptionGroup.setTitle(_translate("Form", "Exception Handling", None)) + self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None)) self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions", None)) self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception", None)) self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions", None)) self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame", None)) self.exceptionInfoLabel.setText(_translate("Form", "Exception Info", None)) - self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None)) + self.label.setText(_translate("Form", "Filter (regex):", None)) from .CmdInput import CmdInput diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 0deae0e0..57c71bc8 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -32,6 +32,57 @@ def ftrace(func): return rv return w + +class Tracer(object): + """ + Prints every function enter/exit. Useful for debugging crashes / lockups. + """ + def __init__(self): + self.count = 0 + self.stack = [] + + def trace(self, frame, event, arg): + self.count += 1 + # If it has been a long time since we saw the top of the stack, + # print a reminder + if self.count % 1000 == 0: + print("----- current stack: -----") + for line in self.stack: + print(line) + if event == 'call': + line = " " * len(self.stack) + ">> " + self.frameInfo(frame) + print(line) + self.stack.append(line) + elif event == 'return': + self.stack.pop() + line = " " * len(self.stack) + "<< " + self.frameInfo(frame) + print(line) + if len(self.stack) == 0: + self.count = 0 + + return self.trace + + def stop(self): + sys.settrace(None) + + def start(self): + sys.settrace(self.trace) + + def frameInfo(self, fr): + filename = fr.f_code.co_filename + funcname = fr.f_code.co_name + lineno = fr.f_lineno + callfr = sys._getframe(3) + callline = "%s %d" % (callfr.f_code.co_name, callfr.f_lineno) + args, _, _, value_dict = inspect.getargvalues(fr) + if len(args) and args[0] == 'self': + instance = value_dict.get('self', None) + if instance is not None: + cls = getattr(instance, '__class__', None) + if cls is not None: + funcname = cls.__name__ + "." + funcname + return "%s: %s %s: %s" % (callline, filename, lineno, funcname) + def warnOnException(func): """Decorator which catches/ignores exceptions and prints a stack trace.""" def w(*args, **kwds): @@ -41,17 +92,22 @@ def warnOnException(func): printExc('Ignored exception:') return w -def getExc(indent=4, prefix='| '): - tb = traceback.format_exc() - lines = [] - for l in tb.split('\n'): - lines.append(" "*indent + prefix + l) - return '\n'.join(lines) +def getExc(indent=4, prefix='| ', skip=1): + lines = (traceback.format_stack()[:-skip] + + [" ---- exception caught ---->\n"] + + traceback.format_tb(sys.exc_info()[2]) + + traceback.format_exception_only(*sys.exc_info()[:2])) + lines2 = [] + for l in lines: + lines2.extend(l.strip('\n').split('\n')) + lines3 = [" "*indent + prefix + l for l in lines2] + return '\n'.join(lines3) + def printExc(msg='', indent=4, prefix='|'): """Print an error message followed by an indented exception backtrace (This function is intended to be called within except: blocks)""" - exc = getExc(indent, prefix + ' ') + exc = getExc(indent, prefix + ' ', skip=2) print("[%s] %s\n" % (time.strftime("%H:%M:%S"), msg)) print(" "*indent + prefix + '='*30 + '>>') print(exc) @@ -407,6 +463,7 @@ class Profiler(object): _depth = 0 _msgs = [] + disable = False # set this flag to disable all or individual profilers at runtime class DisabledProfiler(object): def __init__(self, *args, **kwds): @@ -418,12 +475,11 @@ class Profiler(object): def mark(self, msg=None): pass _disabledProfiler = DisabledProfiler() - - + def __new__(cls, msg=None, disabled='env', delayed=True): """Optionally create a new profiler based on caller's qualname. """ - if disabled is True or (disabled=='env' and len(cls._profilers) == 0): + if disabled is True or (disabled == 'env' and len(cls._profilers) == 0): return cls._disabledProfiler # determine the qualified name of the caller function @@ -431,11 +487,11 @@ class Profiler(object): try: caller_object_type = type(caller_frame.f_locals["self"]) except KeyError: # we are in a regular function - qualifier = caller_frame.f_globals["__name__"].split(".", 1)[-1] + qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1] else: # we are in a method qualifier = caller_object_type.__name__ func_qualname = qualifier + "." + caller_frame.f_code.co_name - if disabled=='env' and func_qualname not in cls._profilers: # don't do anything + if disabled == 'env' and func_qualname not in cls._profilers: # don't do anything return cls._disabledProfiler # create an actual profiling object cls._depth += 1 @@ -447,13 +503,12 @@ class Profiler(object): obj._firstTime = obj._lastTime = ptime.time() obj._newMsg("> Entering " + obj._name) return obj - #else: - #def __new__(cls, delayed=True): - #return lambda msg=None: None def __call__(self, msg=None): """Register or print a new message with timing information. """ + if self.disable: + return if msg is None: msg = str(self._markCount) self._markCount += 1 @@ -479,7 +534,7 @@ class Profiler(object): def finish(self, msg=None): """Add a final message; flush the message list if no parent profiler. """ - if self._finished: + if self._finished or self.disable: return self._finished = True if msg is not None: @@ -984,6 +1039,7 @@ def qObjectReport(verbose=False): class PrintDetector(object): + """Find code locations that print to stdout.""" def __init__(self): self.stdout = sys.stdout sys.stdout = self @@ -1002,6 +1058,45 @@ class PrintDetector(object): self.stdout.flush() +def listQThreads(): + """Prints Thread IDs (Qt's, not OS's) for all QThreads.""" + thr = findObj('[Tt]hread') + thr = [t for t in thr if isinstance(t, QtCore.QThread)] + import sip + for t in thr: + print("--> ", t) + print(" Qt ID: 0x%x" % sip.unwrapinstance(t)) + + +def pretty(data, indent=''): + """Format nested dict/list/tuple structures into a more human-readable string + This function is a bit better than pprint for displaying OrderedDicts. + """ + ret = "" + ind2 = indent + " " + if isinstance(data, dict): + ret = indent+"{\n" + for k, v in data.iteritems(): + ret += ind2 + repr(k) + ": " + pretty(v, ind2).strip() + "\n" + ret += indent+"}\n" + elif isinstance(data, list) or isinstance(data, tuple): + s = repr(data) + if len(s) < 40: + ret += indent + s + else: + if isinstance(data, list): + d = '[]' + else: + d = '()' + ret = indent+d[0]+"\n" + for i, v in enumerate(data): + ret += ind2 + str(i) + ": " + pretty(v, ind2).strip() + "\n" + ret += indent+d[1]+"\n" + else: + ret += indent + repr(data) + return ret + + class PeriodicTrace(object): """ Used to debug freezing by starting a new thread that reports on the diff --git a/pyqtgraph/exceptionHandling.py b/pyqtgraph/exceptionHandling.py index daa821b7..3182b7eb 100644 --- a/pyqtgraph/exceptionHandling.py +++ b/pyqtgraph/exceptionHandling.py @@ -49,29 +49,45 @@ def setTracebackClearing(clear=True): class ExceptionHandler(object): def __call__(self, *args): - ## call original exception handler first (prints exception) - global original_excepthook, callbacks, clear_tracebacks - print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time())))) - ret = original_excepthook(*args) + ## Start by extending recursion depth just a bit. + ## If the error we are catching is due to recursion, we don't want to generate another one here. + recursionLimit = sys.getrecursionlimit() + try: + sys.setrecursionlimit(recursionLimit+100) - for cb in callbacks: + + ## call original exception handler first (prints exception) + global original_excepthook, callbacks, clear_tracebacks try: - cb(*args) - except: - print(" --------------------------------------------------------------") - print(" Error occurred during exception callback %s" % str(cb)) - print(" --------------------------------------------------------------") - traceback.print_exception(*sys.exc_info()) - - - ## Clear long-term storage of last traceback to prevent memory-hogging. - ## (If an exception occurs while a lot of data is present on the stack, - ## such as when loading large files, the data would ordinarily be kept - ## until the next exception occurs. We would rather release this memory - ## as soon as possible.) - if clear_tracebacks is True: - sys.last_traceback = None + print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time())))) + except Exception: + sys.stderr.write("Warning: stdout is broken! Falling back to stderr.\n") + sys.stdout = sys.stderr + ret = original_excepthook(*args) + + for cb in callbacks: + try: + cb(*args) + except Exception: + print(" --------------------------------------------------------------") + print(" Error occurred during exception callback %s" % str(cb)) + print(" --------------------------------------------------------------") + traceback.print_exception(*sys.exc_info()) + + + ## Clear long-term storage of last traceback to prevent memory-hogging. + ## (If an exception occurs while a lot of data is present on the stack, + ## such as when loading large files, the data would ordinarily be kept + ## until the next exception occurs. We would rather release this memory + ## as soon as possible.) + if clear_tracebacks is True: + sys.last_traceback = None + + finally: + sys.setrecursionlimit(recursionLimit) + + def implements(self, interface=None): ## this just makes it easy for us to detect whether an ExceptionHook is already installed. if interface is None: diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index 6ed4cf07..b87f0182 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -14,6 +14,7 @@ class CSVExporter(Exporter): self.params = Parameter(name='params', type='group', children=[ {'name': 'separator', 'type': 'list', 'value': 'comma', 'values': ['comma', 'tab']}, {'name': 'precision', 'type': 'int', 'value': 10, 'limits': [0, None]}, + {'name': 'columnMode', 'type': 'list', 'values': ['(x,y) per plot', '(x,y,y,y) for all plots']} ]) def parameters(self): @@ -31,15 +32,24 @@ class CSVExporter(Exporter): fd = open(fileName, 'w') data = [] header = [] - for c in self.item.curves: + + appendAllX = self.params['columnMode'] == '(x,y) per plot' + + for i, c in enumerate(self.item.curves): cd = c.getData() if cd[0] is None: continue data.append(cd) - name = '' if hasattr(c, 'implements') and c.implements('plotData') and c.name() is not None: name = c.name().replace('"', '""') + '_' - header.extend(['"'+name+'x"', '"'+name+'y"']) + xName, yName = '"'+name+'x"', '"'+name+'y"' + else: + xName = 'x%04d' % i + yName = 'y%04d' % i + if appendAllX or i == 0: + header.extend([xName, yName]) + else: + header.extend([yName]) if self.params['separator'] == 'comma': sep = ',' @@ -51,12 +61,20 @@ class CSVExporter(Exporter): numFormat = '%%0.%dg' % self.params['precision'] numRows = max([len(d[0]) for d in data]) for i in range(numRows): - for d in data: - for j in [0, 1]: - if i < len(d[j]): - fd.write(numFormat % d[j][i] + sep) + for j, d in enumerate(data): + # write x value if this is the first column, or if we want x + # for all rows + if appendAllX or j == 0: + if d is not None and i < len(d[0]): + fd.write(numFormat % d[0][i] + sep) else: fd.write(' %s' % sep) + + # write y value + if d is not None and i < len(d[1]): + fd.write(numFormat % d[1][i] + sep) + else: + fd.write(' %s' % sep) fd.write('\n') fd.close() diff --git a/pyqtgraph/exporters/HDF5Exporter.py b/pyqtgraph/exporters/HDF5Exporter.py new file mode 100644 index 00000000..cc8b5733 --- /dev/null +++ b/pyqtgraph/exporters/HDF5Exporter.py @@ -0,0 +1,58 @@ +from ..Qt import QtGui, QtCore +from .Exporter import Exporter +from ..parametertree import Parameter +from .. import PlotItem + +import numpy +try: + import h5py + HAVE_HDF5 = True +except ImportError: + HAVE_HDF5 = False + +__all__ = ['HDF5Exporter'] + + +class HDF5Exporter(Exporter): + Name = "HDF5 Export: plot (x,y)" + windows = [] + allowCopy = False + + def __init__(self, item): + Exporter.__init__(self, item) + self.params = Parameter(name='params', type='group', children=[ + {'name': 'Name', 'type': 'str', 'value': 'Export',}, + {'name': 'columnMode', 'type': 'list', 'values': ['(x,y) per plot', '(x,y,y,y) for all plots']}, + ]) + + def parameters(self): + return self.params + + def export(self, fileName=None): + if not HAVE_HDF5: + raise RuntimeError("This exporter requires the h5py package, " + "but it was not importable.") + + if not isinstance(self.item, PlotItem): + raise Exception("Must have a PlotItem selected for HDF5 export.") + + if fileName is None: + self.fileSaveDialog(filter=["*.h5", "*.hdf", "*.hd5"]) + return + dsname = self.params['Name'] + fd = h5py.File(fileName, 'a') # forces append to file... 'w' doesn't seem to "delete/overwrite" + data = [] + + appendAllX = self.params['columnMode'] == '(x,y) per plot' + for i,c in enumerate(self.item.curves): + d = c.getData() + if appendAllX or i == 0: + data.append(d[0]) + data.append(d[1]) + + fdata = numpy.array(data).astype('double') + dset = fd.create_dataset(dsname, data=fdata) + fd.close() + +if HAVE_HDF5: + HDF5Exporter.register() diff --git a/pyqtgraph/exporters/Matplotlib.py b/pyqtgraph/exporters/Matplotlib.py index 57c4cfdb..8cec1cef 100644 --- a/pyqtgraph/exporters/Matplotlib.py +++ b/pyqtgraph/exporters/Matplotlib.py @@ -4,7 +4,29 @@ from .. import PlotItem from .. import functions as fn __all__ = ['MatplotlibExporter'] - + +""" +It is helpful when using the matplotlib Exporter if your +.matplotlib/matplotlibrc file is configured appropriately. +The following are suggested for getting usable PDF output that +can be edited in Illustrator, etc. + +backend : Qt4Agg +text.usetex : True # Assumes you have a findable LaTeX installation +interactive : False +font.family : sans-serif +font.sans-serif : 'Arial' # (make first in list) +mathtext.default : sf +figure.facecolor : white # personal preference +# next setting allows pdf font to be readable in Adobe Illustrator +pdf.fonttype : 42 # set fonts to TrueType (otherwise it will be 3 + # and the text will be vectorized. +text.dvipnghack : True # primarily to clean up font appearance on Mac + +The advantage is that there is less to do to get an exported file cleaned and ready for +publication. Fonts are not vectorized (outlined), and window colors are white. + +""" class MatplotlibExporter(Exporter): Name = "Matplotlib Window" @@ -14,18 +36,43 @@ class MatplotlibExporter(Exporter): def parameters(self): return None + + def cleanAxes(self, axl): + if type(axl) is not list: + axl = [axl] + for ax in axl: + if ax is None: + continue + for loc, spine in ax.spines.iteritems(): + if loc in ['left', 'bottom']: + pass + elif loc in ['right', 'top']: + spine.set_color('none') + # do not draw the spine + else: + raise ValueError('Unknown spine location: %s' % loc) + # turn off ticks when there is no spine + ax.xaxis.set_ticks_position('bottom') def export(self, fileName=None): if isinstance(self.item, PlotItem): mpw = MatplotlibWindow() MatplotlibExporter.windows.append(mpw) + + stdFont = 'Arial' + fig = mpw.getFigure() - ax = fig.add_subplot(111) + # get labels from the graphic item + xlabel = self.item.axes['bottom']['item'].label.toPlainText() + ylabel = self.item.axes['left']['item'].label.toPlainText() + title = self.item.titleLabel.text + + ax = fig.add_subplot(111, title=title) ax.clear() + self.cleanAxes(ax) #ax.grid(True) - for item in self.item.curves: x, y = item.getData() opts = item.opts @@ -42,17 +89,21 @@ class MatplotlibExporter(Exporter): symbolBrush = fn.mkBrush(opts['symbolBrush']) markeredgecolor = tuple([c/255. for c in fn.colorTuple(symbolPen.color())]) markerfacecolor = tuple([c/255. for c in fn.colorTuple(symbolBrush.color())]) + markersize = opts['symbolSize'] if opts['fillLevel'] is not None and opts['fillBrush'] is not None: fillBrush = fn.mkBrush(opts['fillBrush']) fillcolor = tuple([c/255. for c in fn.colorTuple(fillBrush.color())]) ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor) - ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(), linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor) - + pl = ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(), + linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor, + markersize=markersize) xr, yr = self.item.viewRange() ax.set_xbound(*xr) ax.set_ybound(*yr) + ax.set_xlabel(xlabel) # place the labels. + ax.set_ylabel(ylabel) mpw.draw() else: raise Exception("Matplotlib export currently only works with plot items") diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index e46c9981..a91466c8 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -102,14 +102,12 @@ xmlHeader = """\ pyqtgraph SVG export Generated with Qt and pyqtgraph - - """ def generateSvg(item): global xmlHeader try: - node = _generateItemSvg(item) + node, defs = _generateItemSvg(item) finally: ## reset export mode for all items in the tree if isinstance(item, QtGui.QGraphicsScene): @@ -124,7 +122,11 @@ def generateSvg(item): cleanXml(node) - return xmlHeader + node.toprettyxml(indent=' ') + "\n\n" + defsXml = "\n" + for d in defs: + defsXml += d.toprettyxml(indent=' ') + defsXml += "\n" + return xmlHeader + defsXml + node.toprettyxml(indent=' ') + "\n\n" def _generateItemSvg(item, nodes=None, root=None): @@ -230,6 +232,10 @@ def _generateItemSvg(item, nodes=None, root=None): g1 = doc.getElementsByTagName('g')[0] ## get list of sub-groups g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g'] + + defs = doc.getElementsByTagName('defs') + if len(defs) > 0: + defs = [n for n in defs[0].childNodes if isinstance(n, xml.Element)] except: print(doc.toxml()) raise @@ -238,7 +244,7 @@ def _generateItemSvg(item, nodes=None, root=None): ## Get rid of group transformation matrices by applying ## transformation to inner coordinates - correctCoordinates(g1, item) + correctCoordinates(g1, defs, item) profiler('correct') ## make sure g1 has the transformation matrix #m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32()) @@ -275,7 +281,9 @@ def _generateItemSvg(item, nodes=None, root=None): path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape())) item.scene().addItem(path) try: - pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0] + #pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0] + pathNode = _generateItemSvg(path, root=root)[0].getElementsByTagName('path')[0] + # assume for this path is empty.. possibly problematic. finally: item.scene().removeItem(path) @@ -294,14 +302,19 @@ def _generateItemSvg(item, nodes=None, root=None): ## Add all child items as sub-elements. childs.sort(key=lambda c: c.zValue()) for ch in childs: - cg = _generateItemSvg(ch, nodes, root) - if cg is None: + csvg = _generateItemSvg(ch, nodes, root) + if csvg is None: continue + cg, cdefs = csvg childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now) + defs.extend(cdefs) + profiler('children') - return g1 + return g1, defs -def correctCoordinates(node, item): +def correctCoordinates(node, defs, item): + # TODO: correct gradient coordinates inside defs + ## Remove transformation matrices from tags by applying matrix to coordinates inside. ## Each item is represented by a single top-level group with one or more groups inside. ## Each inner group contains one or more drawing primitives, possibly of different types. diff --git a/pyqtgraph/exporters/__init__.py b/pyqtgraph/exporters/__init__.py index 8be1c3b6..62ab1331 100644 --- a/pyqtgraph/exporters/__init__.py +++ b/pyqtgraph/exporters/__init__.py @@ -4,7 +4,7 @@ from .SVGExporter import * from .Matplotlib import * from .CSVExporter import * from .PrintExporter import * - +from .HDF5Exporter import * def listExporters(): return Exporter.Exporters[:] diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 48357b30..878f86ae 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -20,41 +20,12 @@ from ..debug import printExc from .. import configfile as configfile from .. import dockarea as dockarea from . import FlowchartGraphicsView +from .. import functions as fn def strDict(d): return dict([(str(k), v) for k, v in d.items()]) -def toposort(deps, nodes=None, seen=None, stack=None, depth=0): - """Topological sort. Arguments are: - deps dictionary describing dependencies where a:[b,c] means "a depends on b and c" - nodes optional, specifies list of starting nodes (these should be the nodes - which are not depended on by any other nodes) - """ - - if nodes is None: - ## run through deps to find nodes that are not depended upon - rem = set() - for dep in deps.values(): - rem |= set(dep) - nodes = set(deps.keys()) - rem - if seen is None: - seen = set() - stack = [] - sorted = [] - #print " "*depth, "Starting from", nodes - for n in nodes: - if n in stack: - raise Exception("Cyclic dependency detected", stack + [n]) - if n in seen: - continue - seen.add(n) - #print " "*depth, " descending into", n, deps[n] - sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1)) - #print " "*depth, " Added", n - sorted.append(n) - #print " "*depth, " ", sorted - return sorted class Flowchart(Node): @@ -278,9 +249,10 @@ class Flowchart(Node): ## Record inputs given to process() for n, t in self.inputNode.outputs().items(): - if n not in args: - raise Exception("Parameter %s required to process this chart." % n) - data[t] = args[n] + # if n not in args: + # raise Exception("Parameter %s required to process this chart." % n) + if n in args: + data[t] = args[n] ret = {} @@ -305,7 +277,7 @@ class Flowchart(Node): if len(inputs) == 0: continue if inp.isMultiValue(): ## multi-input terminals require a dict of all inputs - args[inp.name()] = dict([(i, data[i]) for i in inputs]) + args[inp.name()] = dict([(i, data[i]) for i in inputs if i in data]) else: ## single-inputs terminals only need the single input value available args[inp.name()] = data[inputs[0]] @@ -325,9 +297,8 @@ class Flowchart(Node): #print out.name() try: data[out] = result[out.name()] - except: - print(out, out.name()) - raise + except KeyError: + pass elif c == 'd': ## delete a terminal result (no longer needed; may be holding a lot of memory) #print "===> delete", arg if arg in data: @@ -352,7 +323,7 @@ class Flowchart(Node): #print "DEPS:", deps ## determine correct node-processing order #deps[self] = [] - order = toposort(deps) + order = fn.toposort(deps) #print "ORDER1:", order ## construct list of operations @@ -401,7 +372,7 @@ class Flowchart(Node): deps[node].extend(t.dependentNodes()) ## determine order of updates - order = toposort(deps, nodes=[startNode]) + order = fn.toposort(deps, nodes=[startNode]) order.reverse() ## keep track of terminals that have been updated @@ -542,7 +513,7 @@ class Flowchart(Node): return ## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs.. #fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") - fileName = str(fileName) + fileName = unicode(fileName) state = configfile.readConfigFile(fileName) self.restoreState(state, clear=True) self.viewBox.autoRange() @@ -563,7 +534,7 @@ class Flowchart(Node): self.fileDialog.fileSelected.connect(self.saveFile) return #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") - fileName = str(fileName) + fileName = unicode(fileName) configfile.writeConfigFile(self.saveState(), fileName) self.sigFileSaved.emit(fileName) @@ -685,7 +656,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): #self.setCurrentFile(newFile) def fileSaved(self, fileName): - self.setCurrentFile(str(fileName)) + self.setCurrentFile(unicode(fileName)) self.ui.saveBtn.success("Saved.") def saveClicked(self): @@ -714,7 +685,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): #self.setCurrentFile(newFile) def setCurrentFile(self, fileName): - self.currentFileName = str(fileName) + self.currentFileName = unicode(fileName) if fileName is None: self.ui.fileNameLabel.setText("[ new ]") else: diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py index 532f6c5b..5236de8d 100644 --- a/pyqtgraph/flowchart/library/Data.py +++ b/pyqtgraph/flowchart/library/Data.py @@ -182,8 +182,8 @@ class EvalNode(Node): def __init__(self, name): Node.__init__(self, name, terminals = { - 'input': {'io': 'in', 'renamable': True}, - 'output': {'io': 'out', 'renamable': True}, + 'input': {'io': 'in', 'renamable': True, 'multiable': True}, + 'output': {'io': 'out', 'renamable': True, 'multiable': True}, }, allowAddInput=True, allowAddOutput=True) diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py index b72fbca5..88a2f6c5 100644 --- a/pyqtgraph/flowchart/library/Filters.py +++ b/pyqtgraph/flowchart/library/Filters.py @@ -6,6 +6,8 @@ from ... import functions as pgfn from .common import * import numpy as np +from ... import PolyLineROI +from ... import Point from ... import metaarray as metaarray @@ -201,6 +203,72 @@ class Detrend(CtrlNode): raise Exception("DetrendFilter node requires the package scipy.signal.") return detrend(data) +class RemoveBaseline(PlottingCtrlNode): + """Remove an arbitrary, graphically defined baseline from the data.""" + nodeName = 'RemoveBaseline' + + def __init__(self, name): + ## define inputs and outputs (one output needs to be a plot) + PlottingCtrlNode.__init__(self, name) + self.line = PolyLineROI([[0,0],[1,0]]) + self.line.sigRegionChanged.connect(self.changed) + + ## create a PolyLineROI, add it to a plot -- actually, I think we want to do this after the node is connected to a plot (look at EventDetection.ThresholdEvents node for ideas), and possible after there is data. We will need to update the end positions of the line each time the input data changes + #self.line = None ## will become a PolyLineROI + + def connectToPlot(self, node): + """Define what happens when the node is connected to a plot""" + + if node.plot is None: + return + node.getPlot().addItem(self.line) + + def disconnectFromPlot(self, plot): + """Define what happens when the node is disconnected from a plot""" + plot.removeItem(self.line) + + def processData(self, data): + ## get array of baseline (from PolyLineROI) + h0 = self.line.getHandles()[0] + h1 = self.line.getHandles()[-1] + + timeVals = data.xvals(0) + h0.setPos(timeVals[0], h0.pos()[1]) + h1.setPos(timeVals[-1], h1.pos()[1]) + + pts = self.line.listPoints() ## lists line handles in same coordinates as data + pts, indices = self.adjustXPositions(pts, timeVals) ## maxe sure x positions match x positions of data points + + ## construct an array that represents the baseline + arr = np.zeros(len(data), dtype=float) + n = 1 + arr[0] = pts[0].y() + for i in range(len(pts)-1): + x1 = pts[i].x() + x2 = pts[i+1].x() + y1 = pts[i].y() + y2 = pts[i+1].y() + m = (y2-y1)/(x2-x1) + b = y1 + + times = timeVals[(timeVals > x1)*(timeVals <= x2)] + arr[n:n+len(times)] = (m*(times-times[0]))+b + n += len(times) + + return data - arr ## subract baseline from data + + def adjustXPositions(self, pts, data): + """Return a list of Point() where the x position is set to the nearest x value in *data* for each point in *pts*.""" + points = [] + timeIndices = [] + for p in pts: + x = np.argwhere(abs(data - p.x()) == abs(data - p.x()).min()) + points.append(Point(data[x], p.y())) + timeIndices.append(x) + + return points, timeIndices + + class AdaptiveDetrend(CtrlNode): """Removes baseline from data, ignoring anomalous events""" @@ -275,4 +343,4 @@ class RemovePeriodic(CtrlNode): return ma - \ No newline at end of file + diff --git a/pyqtgraph/flowchart/library/common.py b/pyqtgraph/flowchart/library/common.py index 548dc440..425fe86c 100644 --- a/pyqtgraph/flowchart/library/common.py +++ b/pyqtgraph/flowchart/library/common.py @@ -131,6 +131,42 @@ class CtrlNode(Node): l.show() +class PlottingCtrlNode(CtrlNode): + """Abstract class for CtrlNodes that can connect to plots.""" + + def __init__(self, name, ui=None, terminals=None): + #print "PlottingCtrlNode.__init__ called." + CtrlNode.__init__(self, name, ui=ui, terminals=terminals) + self.plotTerminal = self.addOutput('plot', optional=True) + + def connected(self, term, remote): + CtrlNode.connected(self, term, remote) + if term is not self.plotTerminal: + return + node = remote.node() + node.sigPlotChanged.connect(self.connectToPlot) + self.connectToPlot(node) + + def disconnected(self, term, remote): + CtrlNode.disconnected(self, term, remote) + if term is not self.plotTerminal: + return + remote.node().sigPlotChanged.disconnect(self.connectToPlot) + self.disconnectFromPlot(remote.node().getPlot()) + + def connectToPlot(self, node): + """Define what happens when the node is connected to a plot""" + raise Exception("Must be re-implemented in subclass") + + def disconnectFromPlot(self, plot): + """Define what happens when the node is disconnected from a plot""" + raise Exception("Must be re-implemented in subclass") + + def process(self, In, display=True): + out = CtrlNode.process(self, In, display) + out['plot'] = None + return out + def metaArrayWrapper(fn): def newFn(self, data, *args, **kargs): diff --git a/pyqtgraph/flowchart/library/functions.py b/pyqtgraph/flowchart/library/functions.py index 027e1386..338d25c4 100644 --- a/pyqtgraph/flowchart/library/functions.py +++ b/pyqtgraph/flowchart/library/functions.py @@ -206,7 +206,7 @@ def adaptiveDetrend(data, x=None, threshold=3.0): #d3 = where(mask, 0, d2) #d4 = d2 - lowPass(d3, cutoffs[1], dt=dt) - lr = stats.linregress(x[mask], d[mask]) + lr = scipy.stats.linregress(x[mask], d[mask]) base = lr[1] + lr[0]*x d4 = d - base diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 77643c99..6ae2f65b 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -591,6 +591,50 @@ def interpolateArray(data, x, default=0.0): return result +def subArray(data, offset, shape, stride): + """ + Unpack a sub-array from *data* using the specified offset, shape, and stride. + + Note that *stride* is specified in array elements, not bytes. + For example, we have a 2x3 array packed in a 1D array as follows:: + + data = [_, _, 00, 01, 02, _, 10, 11, 12, _] + + Then we can unpack the sub-array with this call:: + + subArray(data, offset=2, shape=(2, 3), stride=(4, 1)) + + ..which returns:: + + [[00, 01, 02], + [10, 11, 12]] + + This function operates only on the first axis of *data*. So changing + the input in the example above to have shape (10, 7) would cause the + output to have shape (2, 3, 7). + """ + #data = data.flatten() + data = data[offset:] + shape = tuple(shape) + stride = tuple(stride) + extraShape = data.shape[1:] + #print data.shape, offset, shape, stride + for i in range(len(shape)): + mask = (slice(None),) * i + (slice(None, shape[i] * stride[i]),) + newShape = shape[:i+1] + if i < len(shape)-1: + newShape += (stride[i],) + newShape += extraShape + #print i, mask, newShape + #print "start:\n", data.shape, data + data = data[mask] + #print "mask:\n", data.shape, data + data = data.reshape(newShape) + #print "reshape:\n", data.shape, data + + return data + + def transformToArray(tr): """ Given a QTransform, return a 3x3 numpy array. @@ -2156,3 +2200,51 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): yvals[i] = y return yvals[np.argsort(inds)] ## un-shuffle values before returning + + + +def toposort(deps, nodes=None, seen=None, stack=None, depth=0): + """Topological sort. Arguments are: + deps dictionary describing dependencies where a:[b,c] means "a depends on b and c" + nodes optional, specifies list of starting nodes (these should be the nodes + which are not depended on by any other nodes). Other candidate starting + nodes will be ignored. + + Example:: + + # Sort the following graph: + # + # B ──┬─────> C <── D + # │ │ + # E <─┴─> A <─┘ + # + deps = {'a': ['b', 'c'], 'c': ['b', 'd'], 'e': ['b']} + toposort(deps) + => ['b', 'd', 'c', 'a', 'e'] + """ + # fill in empty dep lists + deps = deps.copy() + for k,v in list(deps.items()): + for k in v: + if k not in deps: + deps[k] = [] + + if nodes is None: + ## run through deps to find nodes that are not depended upon + rem = set() + for dep in deps.values(): + rem |= set(dep) + nodes = set(deps.keys()) - rem + if seen is None: + seen = set() + stack = [] + sorted = [] + for n in nodes: + if n in stack: + raise Exception("Cyclic dependency detected", stack + [n]) + if n in seen: + continue + seen.add(n) + sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1)) + sorted.append(n) + return sorted diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index ededed56..e5b9e3f5 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -918,13 +918,17 @@ class AxisItem(GraphicsWidget): rects.append(br) textRects.append(rects[-1]) - ## measure all text, make sure there's enough room - if axis == 0: - textSize = np.sum([r.height() for r in textRects]) - textSize2 = np.max([r.width() for r in textRects]) if textRects else 0 + if len(textRects) > 0: + ## measure all text, make sure there's enough room + if axis == 0: + textSize = np.sum([r.height() for r in textRects]) + textSize2 = np.max([r.width() for r in textRects]) + else: + textSize = np.sum([r.width() for r in textRects]) + textSize2 = np.max([r.height() for r in textRects]) else: - textSize = np.sum([r.width() for r in textRects]) - textSize2 = np.max([r.height() for r in textRects]) if textRects else 0 + textSize = 0 + textSize2 = 0 if i > 0: ## always draw top level ## If the strings are too crowded, stop drawing text now. diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index e16370f5..a151798a 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -3,6 +3,7 @@ from ..python2_3 import sortList from .. import functions as fn from .GraphicsObject import GraphicsObject from .GraphicsWidget import GraphicsWidget +from ..widgets.SpinBox import SpinBox import weakref from ..pgcollections import OrderedDict from ..colormap import ColorMap @@ -300,6 +301,7 @@ class TickSliderItem(GraphicsWidget): pos.setX(x) tick.setPos(pos) self.ticks[tick] = val + self.updateGradient() def tickValue(self, tick): ## public @@ -537,23 +539,22 @@ class GradientEditorItem(TickSliderItem): def tickClicked(self, tick, ev): #private if ev.button() == QtCore.Qt.LeftButton: - if not tick.colorChangeAllowed: - return - self.currentTick = tick - self.currentTickColor = tick.color - self.colorDialog.setCurrentColor(tick.color) - self.colorDialog.open() - #color = QtGui.QColorDialog.getColor(tick.color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) - #if color.isValid(): - #self.setTickColor(tick, color) - #self.updateGradient() + self.raiseColorDialog(tick) elif ev.button() == QtCore.Qt.RightButton: - if not tick.removeAllowed: - return - if len(self.ticks) > 2: - self.removeTick(tick) - self.updateGradient() - + self.raiseTickContextMenu(tick, ev) + + def raiseColorDialog(self, tick): + if not tick.colorChangeAllowed: + return + self.currentTick = tick + self.currentTickColor = tick.color + self.colorDialog.setCurrentColor(tick.color) + self.colorDialog.open() + + def raiseTickContextMenu(self, tick, ev): + self.tickMenu = TickMenu(tick, self) + self.tickMenu.popup(ev.screenPos().toQPoint()) + def tickMoved(self, tick, pos): #private TickSliderItem.tickMoved(self, tick, pos) @@ -726,6 +727,7 @@ class GradientEditorItem(TickSliderItem): def removeTick(self, tick, finish=True): TickSliderItem.removeTick(self, tick) if finish: + self.updateGradient() self.sigGradientChangeFinished.emit(self) @@ -867,44 +869,59 @@ class Tick(QtGui.QGraphicsObject): ## NOTE: Making this a subclass of GraphicsO self.currentPen = self.pen self.update() - #def mouseMoveEvent(self, ev): - ##print self, "move", ev.scenePos() - #if not self.movable: - #return - #if not ev.buttons() & QtCore.Qt.LeftButton: - #return - - - #newPos = ev.scenePos() + self.mouseOffset - #newPos.setY(self.pos().y()) - ##newPos.setX(min(max(newPos.x(), 0), 100)) - #self.setPos(newPos) - #self.view().tickMoved(self, newPos) - #self.movedSincePress = True - ##self.emit(QtCore.SIGNAL('tickChanged'), self) - #ev.accept() - #def mousePressEvent(self, ev): - #self.movedSincePress = False - #if ev.button() == QtCore.Qt.LeftButton: - #ev.accept() - #self.mouseOffset = self.pos() - ev.scenePos() - #self.pressPos = ev.scenePos() - #elif ev.button() == QtCore.Qt.RightButton: - #ev.accept() - ##if self.endTick: - ##return - ##self.view.tickChanged(self, delete=True) - - #def mouseReleaseEvent(self, ev): - ##print self, "release", ev.scenePos() - #if not self.movedSincePress: - #self.view().tickClicked(self, ev) +class TickMenu(QtGui.QMenu): + + def __init__(self, tick, sliderItem): + QtGui.QMenu.__init__(self) - ##if ev.button() == QtCore.Qt.LeftButton and ev.scenePos() == self.pressPos: - ##color = QtGui.QColorDialog.getColor(self.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) - ##if color.isValid(): - ##self.color = color - ##self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) - ###self.emit(QtCore.SIGNAL('tickChanged'), self) - ##self.view.tickChanged(self) + self.tick = weakref.ref(tick) + self.sliderItem = weakref.ref(sliderItem) + + self.removeAct = self.addAction("Remove Tick", lambda: self.sliderItem().removeTick(tick)) + if (not self.tick().removeAllowed) or len(self.sliderItem().ticks) < 3: + self.removeAct.setEnabled(False) + + positionMenu = self.addMenu("Set Position") + w = QtGui.QWidget() + l = QtGui.QGridLayout() + w.setLayout(l) + + value = sliderItem.tickValue(tick) + self.fracPosSpin = SpinBox() + self.fracPosSpin.setOpts(value=value, bounds=(0.0, 1.0), step=0.01, decimals=2) + #self.dataPosSpin = SpinBox(value=dataVal) + #self.dataPosSpin.setOpts(decimals=3, siPrefix=True) + + l.addWidget(QtGui.QLabel("Position:"), 0,0) + l.addWidget(self.fracPosSpin, 0, 1) + #l.addWidget(QtGui.QLabel("Position (data units):"), 1, 0) + #l.addWidget(self.dataPosSpin, 1,1) + + #if self.sliderItem().dataParent is None: + # self.dataPosSpin.setEnabled(False) + + a = QtGui.QWidgetAction(self) + a.setDefaultWidget(w) + positionMenu.addAction(a) + + self.fracPosSpin.sigValueChanging.connect(self.fractionalValueChanged) + #self.dataPosSpin.valueChanged.connect(self.dataValueChanged) + + colorAct = self.addAction("Set Color", lambda: self.sliderItem().raiseColorDialog(self.tick())) + if not self.tick().colorChangeAllowed: + colorAct.setEnabled(False) + + def fractionalValueChanged(self, x): + self.sliderItem().setTickValue(self.tick(), self.fracPosSpin.value()) + #if self.sliderItem().dataParent is not None: + # self.dataPosSpin.blockSignals(True) + # self.dataPosSpin.setValue(self.sliderItem().tickDataValue(self.tick())) + # self.dataPosSpin.blockSignals(False) + + #def dataValueChanged(self, val): + # self.sliderItem().setTickValue(self.tick(), val, dataUnits=True) + # self.fracPosSpin.blockSignals(True) + # self.fracPosSpin.setValue(self.sliderItem().tickValue(self.tick())) + # self.fracPosSpin.blockSignals(False) + diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index f5c2d248..5c39627c 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -177,6 +177,12 @@ class ImageItem(GraphicsObject): self.translate(rect.left(), rect.top()) self.scale(rect.width() / self.width(), rect.height() / self.height()) + def clear(self): + self.image = None + self.prepareGeometryChange() + self.informViewBoundsChanged() + self.update() + def setImage(self, image=None, autoLevels=None, **kargs): """ Update the image displayed by this item. For more information on how the image @@ -512,6 +518,9 @@ class ImageItem(GraphicsObject): def removeClicked(self): ## Send remove event only after we have exited the menu event handler self.removeTimer = QtCore.QTimer() - self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self)) + self.removeTimer.timeout.connect(self.emitRemoveRequested) self.removeTimer.start(0) + def emitRemoveRequested(self): + self.removeTimer.timeout.disconnect(self.emitRemoveRequested) + self.sigRemoveRequested.emit(self) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index befc5783..6148989d 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -168,6 +168,7 @@ class PlotDataItem(GraphicsObject): 'downsample': 1, 'autoDownsample': False, 'downsampleMethod': 'peak', + 'autoDownsampleFactor': 5., # draw ~5 samples per pixel 'clipToView': False, 'data': None, @@ -380,14 +381,23 @@ class PlotDataItem(GraphicsObject): elif len(args) == 2: seq = ('listOfValues', 'MetaArray', 'empty') - if dataType(args[0]) not in seq or dataType(args[1]) not in seq: + dtyp = dataType(args[0]), dataType(args[1]) + if dtyp[0] not in seq or dtyp[1] not in seq: raise Exception('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1])))) if not isinstance(args[0], np.ndarray): - x = np.array(args[0]) + #x = np.array(args[0]) + if dtyp[0] == 'MetaArray': + x = args[0].asarray() + else: + x = np.array(args[0]) else: x = args[0].view(np.ndarray) if not isinstance(args[1], np.ndarray): - y = np.array(args[1]) + #y = np.array(args[1]) + if dtyp[1] == 'MetaArray': + y = args[1].asarray() + else: + y = np.array(args[1]) else: y = args[1].view(np.ndarray) @@ -538,7 +548,7 @@ class PlotDataItem(GraphicsObject): x1 = (range.right()-x[0]) / dx width = self.getViewBox().width() if width != 0.0: - ds = int(max(1, int(0.2 * (x1-x0) / width))) + ds = int(max(1, int((x1-x0) / (width*self.opts['autoDownsampleFactor'])))) ## downsampling is expensive; delay until after clipping. if self.opts['clipToView']: diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 8292875c..f8959e22 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -469,7 +469,8 @@ class PlotItem(GraphicsWidget): ### Average data together (x, y) = curve.getData() - if plot.yData is not None: + if plot.yData is not None and y.shape == plot.yData.shape: + # note that if shapes do not match, then the average resets. newData = plot.yData * (n-1) / float(n) + y * 1.0 / float(n) plot.setData(plot.xData, newData) else: @@ -1207,10 +1208,13 @@ class PlotItem(GraphicsWidget): self.updateButtons() def updateButtons(self): - if self._exportOpts is False and self.mouseHovering and not self.buttonsHidden and not all(self.vb.autoRangeEnabled()): - self.autoBtn.show() - else: - self.autoBtn.hide() + try: + if self._exportOpts is False and self.mouseHovering and not self.buttonsHidden and not all(self.vb.autoRangeEnabled()): + self.autoBtn.show() + else: + self.autoBtn.hide() + except RuntimeError: + pass # this can happen if the plot has been deleted. def _plotArray(self, arr, x=None, **kargs): if arr.ndim != 1: diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index f3ebd992..7707466a 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -25,7 +25,7 @@ from .UIGraphicsItem import UIGraphicsItem __all__ = [ 'ROI', 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', - 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', + 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', 'CrosshairROI', ] @@ -862,8 +862,10 @@ class ROI(GraphicsObject): elif h['type'] == 'sr': if h['center'][0] == h['pos'][0]: scaleAxis = 1 + nonScaleAxis=0 else: scaleAxis = 0 + nonScaleAxis=1 try: if lp1.length() == 0 or lp0.length() == 0: @@ -885,6 +887,8 @@ class ROI(GraphicsObject): newState['size'][scaleAxis] = round(newState['size'][scaleAxis] / self.snapSize) * self.snapSize if newState['size'][scaleAxis] == 0: newState['size'][scaleAxis] = 1 + if self.aspectLocked: + newState['size'][nonScaleAxis] = newState['size'][scaleAxis] c1 = c * newState['size'] tr = QtGui.QTransform() @@ -972,14 +976,16 @@ class ROI(GraphicsObject): return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() def paint(self, p, opt, widget): - p.save() - r = self.boundingRect() + # p.save() + # Note: don't use self.boundingRect here, because subclasses may need to redefine it. + r = QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() + p.setRenderHint(QtGui.QPainter.Antialiasing) p.setPen(self.currentPen) p.translate(r.left(), r.top()) p.scale(r.width(), r.height()) p.drawRect(0, 0, 1, 1) - p.restore() + # p.restore() def getArraySlice(self, data, img, axes=(0,1), returnSlice=True): """Return a tuple of slice objects that can be used to slice the region from data covered by this ROI. @@ -2139,6 +2145,102 @@ class SpiralROI(ROI): p.drawRect(self.boundingRect()) +class CrosshairROI(ROI): + """A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable.""" + + def __init__(self, pos=None, size=None, **kargs): + if size == None: + #size = [100e-6,100e-6] + size=[1,1] + if pos == None: + pos = [0,0] + self._shape = None + ROI.__init__(self, pos, size, **kargs) + + self.sigRegionChanged.connect(self.invalidate) + self.addScaleRotateHandle(Point(1, 0), Point(0, 0)) + self.aspectLocked = True + def invalidate(self): + self._shape = None + self.prepareGeometryChange() + + def boundingRect(self): + #size = self.size() + #return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized() + return self.shape().boundingRect() + + #def getRect(self): + ### same as boundingRect -- for internal use so that boundingRect can be re-implemented in subclasses + #size = self.size() + #return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized() + + + def shape(self): + if self._shape is None: + radius = self.getState()['size'][1] + p = QtGui.QPainterPath() + p.moveTo(Point(0, -radius)) + p.lineTo(Point(0, radius)) + p.moveTo(Point(-radius, 0)) + p.lineTo(Point(radius, 0)) + p = self.mapToDevice(p) + stroker = QtGui.QPainterPathStroker() + stroker.setWidth(10) + outline = stroker.createStroke(p) + self._shape = self.mapFromDevice(outline) - + + ##h1 = self.handles[0]['item'].pos() + ##h2 = self.handles[1]['item'].pos() + #w1 = Point(-0.5, 0)*self.size() + #w2 = Point(0.5, 0)*self.size() + #h1 = Point(0, -0.5)*self.size() + #h2 = Point(0, 0.5)*self.size() + + #dh = h2-h1 + #dw = w2-w1 + #if dh.length() == 0 or dw.length() == 0: + #return p + #pxv = self.pixelVectors(dh)[1] + #if pxv is None: + #return p + + #pxv *= 4 + + #p.moveTo(h1+pxv) + #p.lineTo(h2+pxv) + #p.lineTo(h2-pxv) + #p.lineTo(h1-pxv) + #p.lineTo(h1+pxv) + + #pxv = self.pixelVectors(dw)[1] + #if pxv is None: + #return p + + #pxv *= 4 + + #p.moveTo(w1+pxv) + #p.lineTo(w2+pxv) + #p.lineTo(w2-pxv) + #p.lineTo(w1-pxv) + #p.lineTo(w1+pxv) + + return self._shape + + def paint(self, p, *args): + #p.save() + #r = self.getRect() + radius = self.getState()['size'][1] + p.setRenderHint(QtGui.QPainter.Antialiasing) + p.setPen(self.currentPen) + #p.translate(r.left(), r.top()) + #p.scale(r.width()/10., r.height()/10.) ## need to scale up a little because drawLine has trouble dealing with 0.5 + #p.drawLine(0,5, 10,5) + #p.drawLine(5,0, 5,10) + #p.restore() + + p.drawLine(Point(0, -radius), Point(0, radius)) + p.drawLine(Point(-radius, 0), Point(radius, 0)) + + diff --git a/pyqtgraph/graphicsItems/ScaleBar.py b/pyqtgraph/graphicsItems/ScaleBar.py index 66258678..8ba546f7 100644 --- a/pyqtgraph/graphicsItems/ScaleBar.py +++ b/pyqtgraph/graphicsItems/ScaleBar.py @@ -5,6 +5,7 @@ from .TextItem import TextItem import numpy as np from .. import functions as fn from .. import getConfigOption +from ..Point import Point __all__ = ['ScaleBar'] @@ -12,7 +13,7 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor): """ Displays a rectangular bar to indicate the relative scale of objects on the view. """ - def __init__(self, size, width=5, brush=None, pen=None, suffix='m'): + def __init__(self, size, width=5, brush=None, pen=None, suffix='m', offset=None): GraphicsObject.__init__(self) GraphicsWidgetAnchor.__init__(self) self.setFlag(self.ItemHasNoContents) @@ -24,6 +25,9 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor): self.pen = fn.mkPen(pen) self._width = width self.size = size + if offset == None: + offset = (0,0) + self.offset = offset self.bar = QtGui.QGraphicsRectItem() self.bar.setPen(self.pen) @@ -54,51 +58,14 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor): def boundingRect(self): return QtCore.QRectF() + def setParentItem(self, p): + ret = GraphicsObject.setParentItem(self, p) + if self.offset is not None: + offset = Point(self.offset) + anchorx = 1 if offset[0] <= 0 else 0 + anchory = 1 if offset[1] <= 0 else 0 + anchor = (anchorx, anchory) + self.anchor(itemPos=anchor, parentPos=anchor, offset=offset) + return ret - - -#class ScaleBar(UIGraphicsItem): - #""" - #Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view. - #""" - #def __init__(self, size, width=5, color=(100, 100, 255)): - #UIGraphicsItem.__init__(self) - #self.setAcceptedMouseButtons(QtCore.Qt.NoButton) - - #self.brush = fn.mkBrush(color) - #self.pen = fn.mkPen((0,0,0)) - #self._width = width - #self.size = size - - #def paint(self, p, opt, widget): - #UIGraphicsItem.paint(self, p, opt, widget) - - #rect = self.boundingRect() - #unit = self.pixelSize() - #y = rect.top() + (rect.bottom()-rect.top()) * 0.02 - #y1 = y + unit[1]*self._width - #x = rect.right() + (rect.left()-rect.right()) * 0.02 - #x1 = x - self.size - - #p.setPen(self.pen) - #p.setBrush(self.brush) - #rect = QtCore.QRectF( - #QtCore.QPointF(x1, y1), - #QtCore.QPointF(x, y) - #) - #p.translate(x1, y1) - #p.scale(rect.width(), rect.height()) - #p.drawRect(0, 0, 1, 1) - - #alpha = np.clip(((self.size/unit[0]) - 40.) * 255. / 80., 0, 255) - #p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha))) - #for i in range(1, 10): - ##x2 = x + (x1-x) * 0.1 * i - #x2 = 0.1 * i - #p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1)) - - - #def setSize(self, s): - #self.size = s - diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index bdf89c45..e39b535a 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -68,10 +68,12 @@ def renderSymbol(symbol, size, pen, brush, device=None): device = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32) device.fill(0) p = QtGui.QPainter(device) - p.setRenderHint(p.Antialiasing) - p.translate(device.width()*0.5, device.height()*0.5) - drawSymbol(p, symbol, size, pen, brush) - p.end() + try: + p.setRenderHint(p.Antialiasing) + p.translate(device.width()*0.5, device.height()*0.5) + drawSymbol(p, symbol, size, pen, brush) + finally: + p.end() return device def makeSymbolPixmap(size, pen, brush, symbol): diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index d66f32ad..542bbc1a 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -760,7 +760,8 @@ class ViewBox(GraphicsWidget): x = vr.left()+x, vr.right()+x if y is not None: y = vr.top()+y, vr.bottom()+y - self.setRange(xRange=x, yRange=y, padding=0) + if x is not None or y is not None: + self.setRange(xRange=x, yRange=y, padding=0) @@ -902,6 +903,14 @@ class ViewBox(GraphicsWidget): return args['padding'] = 0 args['disableAutoRange'] = False + + # check for and ignore bad ranges + for k in ['xRange', 'yRange']: + if k in args: + if not np.all(np.isfinite(args[k])): + r = args.pop(k) + print "Warning: %s is invalid: %s" % (k, str(r)) + self.setRange(**args) finally: self._autoRangeNeedsUpdate = False @@ -1066,7 +1075,7 @@ class ViewBox(GraphicsWidget): return self.state['yInverted'] = b - #self.updateMatrix(changed=(False, True)) + self._matrixNeedsUpdate = True # updateViewRange won't detect this for us self.updateViewRange() self.sigStateChanged.emit(self) self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) @@ -1485,7 +1494,7 @@ class ViewBox(GraphicsWidget): aspect = self.state['aspectLocked'] # size ratio / view ratio tr = self.targetRect() bounds = self.rect() - if aspect is not False and aspect != 0 and tr.height() != 0 and bounds.height() != 0: + if aspect is not False and 0 not in [aspect, tr.height(), bounds.height(), bounds.width()]: ## This is the view range aspect ratio we have requested targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1 @@ -1581,18 +1590,16 @@ class ViewBox(GraphicsWidget): if any(changed): self.sigRangeChanged.emit(self, self.state['viewRange']) self.update() + self._matrixNeedsUpdate = True - # Inform linked views that the range has changed - for ax in [0, 1]: - if not changed[ax]: - continue - link = self.linkedView(ax) - if link is not None: - link.linkedViewChanged(self, ax) + # Inform linked views that the range has changed + for ax in [0, 1]: + if not changed[ax]: + continue + link = self.linkedView(ax) + if link is not None: + link.linkedViewChanged(self, ax) - self.update() - self._matrixNeedsUpdate = True - def updateMatrix(self, changed=None): ## Make the childGroup's transform match the requested viewRange. bounds = self.rect() diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index c9f421b4..65252cfe 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -12,7 +12,7 @@ Widget used for displaying 2D or 3D data. Features: - ROI plotting - Image normalization through a variety of methods """ -import sys +import os, sys import numpy as np from ..Qt import QtCore, QtGui, USE_PYSIDE @@ -136,6 +136,8 @@ class ImageView(QtGui.QWidget): self.ui.histogram.setImageItem(self.imageItem) + self.menu = None + self.ui.normGroup.hide() self.roi = PlotROI(10) @@ -176,7 +178,8 @@ class ImageView(QtGui.QWidget): self.timeLine.sigPositionChanged.connect(self.timeLineChanged) self.ui.roiBtn.clicked.connect(self.roiClicked) self.roi.sigRegionChanged.connect(self.roiChanged) - self.ui.normBtn.toggled.connect(self.normToggled) + #self.ui.normBtn.toggled.connect(self.normToggled) + self.ui.menuBtn.clicked.connect(self.menuClicked) self.ui.normDivideRadio.clicked.connect(self.normRadioChanged) self.ui.normSubtractRadio.clicked.connect(self.normRadioChanged) self.ui.normOffRadio.clicked.connect(self.normRadioChanged) @@ -321,6 +324,10 @@ class ImageView(QtGui.QWidget): profiler() + def clear(self): + self.image = None + self.imageItem.clear() + def play(self, rate): """Begin automatically stepping frames forward at the given rate (in fps). This can also be accessed by pressing the spacebar.""" @@ -671,3 +678,43 @@ class ImageView(QtGui.QWidget): def getHistogramWidget(self): """Return the HistogramLUTWidget for this ImageView""" return self.ui.histogram + + def export(self, fileName): + """ + Export data from the ImageView to a file, or to a stack of files if + the data is 3D. Saving an image stack will result in index numbers + being added to the file name. Images are saved as they would appear + onscreen, with levels and lookup table applied. + """ + img = self.getProcessedImage() + if self.hasTimeAxis(): + base, ext = os.path.splitext(fileName) + fmt = "%%s%%0%dd%%s" % int(np.log10(img.shape[0])+1) + for i in range(img.shape[0]): + self.imageItem.setImage(img[i], autoLevels=False) + self.imageItem.save(fmt % (base, i, ext)) + self.updateImage() + else: + self.imageItem.save(fileName) + + def exportClicked(self): + fileName = QtGui.QFileDialog.getSaveFileName() + if fileName == '': + return + self.export(fileName) + + def buildMenu(self): + self.menu = QtGui.QMenu() + self.normAction = QtGui.QAction("Normalization", self.menu) + self.normAction.setCheckable(True) + self.normAction.toggled.connect(self.normToggled) + self.menu.addAction(self.normAction) + self.exportAction = QtGui.QAction("Export", self.menu) + self.exportAction.triggered.connect(self.exportClicked) + self.menu.addAction(self.exportAction) + + def menuClicked(self): + if self.menu is None: + self.buildMenu() + self.menu.popup(QtGui.QCursor.pos()) + diff --git a/pyqtgraph/imageview/ImageViewTemplate.ui b/pyqtgraph/imageview/ImageViewTemplate.ui index 9a3dab03..927bda30 100644 --- a/pyqtgraph/imageview/ImageViewTemplate.ui +++ b/pyqtgraph/imageview/ImageViewTemplate.ui @@ -53,7 +53,7 @@ - + 0 @@ -61,10 +61,7 @@ - Norm - - - true + Menu diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py index 78156317..e728b265 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/imageview/ImageViewTemplate.ui' +# Form implementation generated from reading ui file 'ImageViewTemplate.ui' # -# Created: Mon Dec 23 10:10:52 2013 -# by: PyQt4 UI code generator 4.10 +# Created: Thu May 1 15:20:40 2014 +# by: PyQt4 UI code generator 4.10.4 # # WARNING! All changes made in this file will be lost! @@ -55,15 +55,14 @@ class Ui_Form(object): self.roiBtn.setCheckable(True) self.roiBtn.setObjectName(_fromUtf8("roiBtn")) self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) - self.normBtn = QtGui.QPushButton(self.layoutWidget) + self.menuBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth()) - self.normBtn.setSizePolicy(sizePolicy) - self.normBtn.setCheckable(True) - self.normBtn.setObjectName(_fromUtf8("normBtn")) - self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1) + sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth()) + self.menuBtn.setSizePolicy(sizePolicy) + self.menuBtn.setObjectName(_fromUtf8("menuBtn")) + self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1) self.roiPlot = PlotWidget(self.splitter) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) @@ -149,7 +148,7 @@ class Ui_Form(object): def retranslateUi(self, Form): Form.setWindowTitle(_translate("Form", "Form", None)) self.roiBtn.setText(_translate("Form", "ROI", None)) - self.normBtn.setText(_translate("Form", "Norm", None)) + self.menuBtn.setText(_translate("Form", "Menu", None)) self.normGroup.setTitle(_translate("Form", "Normalization", None)) self.normSubtractRadio.setText(_translate("Form", "Subtract", None)) self.normDivideRadio.setText(_translate("Form", "Divide", None)) diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyside.py b/pyqtgraph/imageview/ImageViewTemplate_pyside.py index 2f8b570b..6d6c9632 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyside.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/imageview/ImageViewTemplate.ui' +# Form implementation generated from reading ui file 'ImageViewTemplate.ui' # -# Created: Mon Dec 23 10:10:52 2013 -# by: pyside-uic 0.2.14 running on PySide 1.1.2 +# Created: Thu May 1 15:20:42 2014 +# by: pyside-uic 0.2.15 running on PySide 1.2.1 # # WARNING! All changes made in this file will be lost! @@ -41,15 +41,14 @@ class Ui_Form(object): self.roiBtn.setCheckable(True) self.roiBtn.setObjectName("roiBtn") self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) - self.normBtn = QtGui.QPushButton(self.layoutWidget) + self.menuBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth()) - self.normBtn.setSizePolicy(sizePolicy) - self.normBtn.setCheckable(True) - self.normBtn.setObjectName("normBtn") - self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1) + sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth()) + self.menuBtn.setSizePolicy(sizePolicy) + self.menuBtn.setObjectName("menuBtn") + self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1) self.roiPlot = PlotWidget(self.splitter) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) @@ -135,7 +134,7 @@ class Ui_Form(object): def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) - self.normBtn.setText(QtGui.QApplication.translate("Form", "Norm", None, QtGui.QApplication.UnicodeUTF8)) + self.menuBtn.setText(QtGui.QApplication.translate("Form", "Menu", None, QtGui.QApplication.UnicodeUTF8)) self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8)) self.normSubtractRadio.setText(QtGui.QApplication.translate("Form", "Subtract", None, QtGui.QApplication.UnicodeUTF8)) self.normDivideRadio.setText(QtGui.QApplication.translate("Form", "Divide", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index d24a7d05..9c3f5b8a 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -103,6 +103,14 @@ class MetaArray(object): """ version = '2' + + # Default hdf5 compression to use when writing + # 'gzip' is widely available and somewhat slow + # 'lzf' is faster, but generally not available outside h5py + # 'szip' is also faster, but lacks write support on windows + # (so by default, we use no compression) + # May also be a tuple (filter, opts), such as ('gzip', 3) + defaultCompression = None ## Types allowed as axis or column names nameTypes = [basestring, tuple] @@ -122,7 +130,7 @@ class MetaArray(object): if file is not None: self._data = None self.readFile(file, **kwargs) - if self._data is None: + if kwargs.get("readAllData", True) and self._data is None: raise Exception("File read failed: %s" % file) else: self._info = info @@ -720,25 +728,28 @@ class MetaArray(object): """ ## decide which read function to use - fd = open(filename, 'rb') - magic = fd.read(8) - if magic == '\x89HDF\r\n\x1a\n': - fd.close() - self._readHDF5(filename, **kwargs) - self._isHDF = True - else: - fd.seek(0) - meta = MetaArray._readMeta(fd) - if 'version' in meta: - ver = meta['version'] + with open(filename, 'rb') as fd: + magic = fd.read(8) + if magic == '\x89HDF\r\n\x1a\n': + fd.close() + self._readHDF5(filename, **kwargs) + self._isHDF = True else: - ver = 1 - rFuncName = '_readData%s' % str(ver) - if not hasattr(MetaArray, rFuncName): - raise Exception("This MetaArray library does not support array version '%s'" % ver) - rFunc = getattr(self, rFuncName) - rFunc(fd, meta, **kwargs) - self._isHDF = False + fd.seek(0) + meta = MetaArray._readMeta(fd) + + if not kwargs.get("readAllData", True): + self._data = np.empty(meta['shape'], dtype=meta['type']) + if 'version' in meta: + ver = meta['version'] + else: + ver = 1 + rFuncName = '_readData%s' % str(ver) + if not hasattr(MetaArray, rFuncName): + raise Exception("This MetaArray library does not support array version '%s'" % ver) + rFunc = getattr(self, rFuncName) + rFunc(fd, meta, **kwargs) + self._isHDF = False @staticmethod def _readMeta(fd): @@ -756,7 +767,7 @@ class MetaArray(object): #print ret return ret - def _readData1(self, fd, meta, mmap=False): + def _readData1(self, fd, meta, mmap=False, **kwds): ## Read array data from the file descriptor for MetaArray v1 files ## read in axis values for any axis that specifies a length frameSize = 1 @@ -766,16 +777,18 @@ class MetaArray(object): frameSize *= ax['values_len'] del ax['values_len'] del ax['values_type'] + self._info = meta['info'] + if not kwds.get("readAllData", True): + return ## the remaining data is the actual array if mmap: subarr = np.memmap(fd, dtype=meta['type'], mode='r', shape=meta['shape']) else: subarr = np.fromstring(fd.read(), dtype=meta['type']) subarr.shape = meta['shape'] - self._info = meta['info'] self._data = subarr - def _readData2(self, fd, meta, mmap=False, subset=None): + def _readData2(self, fd, meta, mmap=False, subset=None, **kwds): ## read in axis values dynAxis = None frameSize = 1 @@ -792,7 +805,10 @@ class MetaArray(object): frameSize *= ax['values_len'] del ax['values_len'] del ax['values_type'] - + self._info = meta['info'] + if not kwds.get("readAllData", True): + return + ## No axes are dynamic, just read the entire array in at once if dynAxis is None: #if rewriteDynamic is not None: @@ -1027,10 +1043,18 @@ class MetaArray(object): def writeHDF5(self, fileName, **opts): ## default options for writing datasets + comp = self.defaultCompression + if isinstance(comp, tuple): + comp, copts = comp + else: + copts = None + dsOpts = { - 'compression': 'lzf', + 'compression': comp, 'chunks': True, } + if copts is not None: + dsOpts['compression_opts'] = copts ## if there is an appendable axis, then we can guess the desired chunk shape (optimized for appending) appAxis = opts.get('appendAxis', None) diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 4e7b7a1c..4f484b74 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -1,5 +1,6 @@ import os, time, sys, traceback, weakref import numpy as np +import threading try: import __builtin__ as builtins import cPickle as pickle @@ -53,8 +54,10 @@ class RemoteEventHandler(object): ## status is either 'result' or 'error' ## if 'error', then result will be (exception, formatted exceprion) ## where exception may be None if it could not be passed through the Connection. + self.resultLock = threading.RLock() self.proxies = {} ## maps {weakref(proxy): proxyId}; used to inform the remote process when a proxy has been deleted. + self.proxyLock = threading.RLock() ## attributes that affect the behavior of the proxy. ## See ObjectProxy._setProxyOptions for description @@ -66,10 +69,15 @@ class RemoteEventHandler(object): 'deferGetattr': False, ## True, False 'noProxyTypes': [ type(None), str, int, float, tuple, list, dict, LocalObjectProxy, ObjectProxy ], } + self.optsLock = threading.RLock() self.nextRequestId = 0 self.exited = False + # Mutexes to help prevent issues when multiple threads access the same RemoteEventHandler + self.processLock = threading.RLock() + self.sendLock = threading.RLock() + RemoteEventHandler.handlers[pid] = self ## register this handler as the one communicating with pid @classmethod @@ -86,46 +94,59 @@ class RemoteEventHandler(object): cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)), -1) def getProxyOption(self, opt): - return self.proxyOptions[opt] + with self.optsLock: + return self.proxyOptions[opt] def setProxyOptions(self, **kwds): """ Set the default behavior options for object proxies. See ObjectProxy._setProxyOptions for more info. """ - self.proxyOptions.update(kwds) + with self.optsLock: + self.proxyOptions.update(kwds) def processRequests(self): """Process all pending requests from the pipe, return after no more events are immediately available. (non-blocking) Returns the number of events processed. """ - if self.exited: - self.debugMsg(' processRequests: exited already; raise ClosedError.') - raise ClosedError() - - numProcessed = 0 - while self.conn.poll(): - try: - self.handleRequest() - numProcessed += 1 - except ClosedError: - self.debugMsg('processRequests: got ClosedError from handleRequest; setting exited=True.') - self.exited = True - raise - #except IOError as err: ## let handleRequest take care of this. - #self.debugMsg(' got IOError from handleRequest; try again.') - #if err.errno == 4: ## interrupted system call; try again - #continue - #else: - #raise - except: - print("Error in process %s" % self.name) - sys.excepthook(*sys.exc_info()) - - if numProcessed > 0: - self.debugMsg('processRequests: finished %d requests' % numProcessed) - return numProcessed + with self.processLock: + + if self.exited: + self.debugMsg(' processRequests: exited already; raise ClosedError.') + raise ClosedError() + + numProcessed = 0 + + while self.conn.poll(): + #try: + #poll = self.conn.poll() + #if not poll: + #break + #except IOError: # this can happen if the remote process dies. + ## might it also happen in other circumstances? + #raise ClosedError() + + try: + self.handleRequest() + numProcessed += 1 + except ClosedError: + self.debugMsg('processRequests: got ClosedError from handleRequest; setting exited=True.') + self.exited = True + raise + #except IOError as err: ## let handleRequest take care of this. + #self.debugMsg(' got IOError from handleRequest; try again.') + #if err.errno == 4: ## interrupted system call; try again + #continue + #else: + #raise + except: + print("Error in process %s" % self.name) + sys.excepthook(*sys.exc_info()) + + if numProcessed > 0: + self.debugMsg('processRequests: finished %d requests' % numProcessed) + return numProcessed def handleRequest(self): """Handle a single request from the remote process. @@ -183,9 +204,11 @@ class RemoteEventHandler(object): returnType = opts.get('returnType', 'auto') if cmd == 'result': - self.results[resultId] = ('result', opts['result']) + with self.resultLock: + self.results[resultId] = ('result', opts['result']) elif cmd == 'error': - self.results[resultId] = ('error', (opts['exception'], opts['excString'])) + with self.resultLock: + self.results[resultId] = ('error', (opts['exception'], opts['excString'])) elif cmd == 'getObjAttr': result = getattr(opts['obj'], opts['attr']) elif cmd == 'callObj': @@ -259,7 +282,9 @@ class RemoteEventHandler(object): self.debugMsg(" handleRequest: sending return value for %d: %s" % (reqId, str(result))) #print "returnValue:", returnValue, result if returnType == 'auto': - result = self.autoProxy(result, self.proxyOptions['noProxyTypes']) + with self.optsLock: + noProxyTypes = self.proxyOptions['noProxyTypes'] + result = self.autoProxy(result, noProxyTypes) elif returnType == 'proxy': result = LocalObjectProxy(result) @@ -378,54 +403,59 @@ class RemoteEventHandler(object): traceback ============= ===================================================================== """ - #if len(kwds) > 0: - #print "Warning: send() ignored args:", kwds + if self.exited: + self.debugMsg(' send: exited already; raise ClosedError.') + raise ClosedError() + + with self.sendLock: + #if len(kwds) > 0: + #print "Warning: send() ignored args:", kwds + + if opts is None: + opts = {} - if opts is None: - opts = {} - - assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"' - if reqId is None: - if callSync != 'off': ## requested return value; use the next available request ID - reqId = self.nextRequestId - self.nextRequestId += 1 - else: - ## If requestId is provided, this _must_ be a response to a previously received request. - assert request in ['result', 'error'] - - if returnType is not None: - opts['returnType'] = returnType + assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"' + if reqId is None: + if callSync != 'off': ## requested return value; use the next available request ID + reqId = self.nextRequestId + self.nextRequestId += 1 + else: + ## If requestId is provided, this _must_ be a response to a previously received request. + assert request in ['result', 'error'] - #print os.getpid(), "send request:", request, reqId, opts - - ## double-pickle args to ensure that at least status and request ID get through - try: - optStr = pickle.dumps(opts) - except: - print("==== Error pickling this object: ====") - print(opts) - print("=======================================") - raise - - nByteMsgs = 0 - if byteData is not None: - nByteMsgs = len(byteData) + if returnType is not None: + opts['returnType'] = returnType + + #print os.getpid(), "send request:", request, reqId, opts + + ## double-pickle args to ensure that at least status and request ID get through + try: + optStr = pickle.dumps(opts) + except: + print("==== Error pickling this object: ====") + print(opts) + print("=======================================") + raise + + nByteMsgs = 0 + if byteData is not None: + nByteMsgs = len(byteData) + + ## Send primary request + request = (request, reqId, nByteMsgs, optStr) + self.debugMsg('send request: cmd=%s nByteMsgs=%d id=%s opts=%s' % (str(request[0]), nByteMsgs, str(reqId), str(opts))) + self.conn.send(request) + + ## follow up by sending byte messages + if byteData is not None: + for obj in byteData: ## Remote process _must_ be prepared to read the same number of byte messages! + self.conn.send_bytes(obj) + self.debugMsg(' sent %d byte messages' % len(byteData)) + + self.debugMsg(' call sync: %s' % callSync) + if callSync == 'off': + return - ## Send primary request - request = (request, reqId, nByteMsgs, optStr) - self.debugMsg('send request: cmd=%s nByteMsgs=%d id=%s opts=%s' % (str(request[0]), nByteMsgs, str(reqId), str(opts))) - self.conn.send(request) - - ## follow up by sending byte messages - if byteData is not None: - for obj in byteData: ## Remote process _must_ be prepared to read the same number of byte messages! - self.conn.send_bytes(obj) - self.debugMsg(' sent %d byte messages' % len(byteData)) - - self.debugMsg(' call sync: %s' % callSync) - if callSync == 'off': - return - req = Request(self, reqId, description=str(request), timeout=timeout) if callSync == 'async': return req @@ -437,20 +467,30 @@ class RemoteEventHandler(object): return req def close(self, callSync='off', noCleanup=False, **kwds): - self.send(request='close', opts=dict(noCleanup=noCleanup), callSync=callSync, **kwds) + try: + self.send(request='close', opts=dict(noCleanup=noCleanup), callSync=callSync, **kwds) + self.exited = True + except ClosedError: + pass def getResult(self, reqId): ## raises NoResultError if the result is not available yet #print self.results.keys(), os.getpid() - if reqId not in self.results: + with self.resultLock: + haveResult = reqId in self.results + + if not haveResult: try: self.processRequests() except ClosedError: ## even if remote connection has closed, we may have ## received new data during this call to processRequests() pass - if reqId not in self.results: - raise NoResultError() - status, result = self.results.pop(reqId) + + with self.resultLock: + if reqId not in self.results: + raise NoResultError() + status, result = self.results.pop(reqId) + if status == 'result': return result elif status == 'error': @@ -494,11 +534,13 @@ class RemoteEventHandler(object): args = list(args) ## Decide whether to send arguments by value or by proxy - noProxyTypes = opts.pop('noProxyTypes', None) - if noProxyTypes is None: - noProxyTypes = self.proxyOptions['noProxyTypes'] - - autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy']) + with self.optsLock: + noProxyTypes = opts.pop('noProxyTypes', None) + if noProxyTypes is None: + noProxyTypes = self.proxyOptions['noProxyTypes'] + + autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy']) + if autoProxy is True: args = [self.autoProxy(v, noProxyTypes) for v in args] for k, v in kwds.iteritems(): @@ -520,11 +562,14 @@ class RemoteEventHandler(object): return self.send(request='callObj', opts=dict(obj=obj, args=args, kwds=kwds), byteData=byteMsgs, **opts) def registerProxy(self, proxy): - ref = weakref.ref(proxy, self.deleteProxy) - self.proxies[ref] = proxy._proxyId + with self.proxyLock: + ref = weakref.ref(proxy, self.deleteProxy) + self.proxies[ref] = proxy._proxyId def deleteProxy(self, ref): - proxyId = self.proxies.pop(ref) + with self.proxyLock: + proxyId = self.proxies.pop(ref) + try: self.send(request='del', opts=dict(proxyId=proxyId), callSync='off') except IOError: ## if remote process has closed down, there is no need to send delete requests anymore diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 1c75c333..5f37ccdc 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -1,6 +1,7 @@ from ..Qt import QtGui, QtCore import os, weakref, re from ..pgcollections import OrderedDict +from ..python2_3 import asUnicode from .ParameterItem import ParameterItem PARAM_TYPES = {} @@ -13,7 +14,9 @@ def registerParameterType(name, cls, override=False): PARAM_TYPES[name] = cls PARAM_NAMES[cls] = name - +def __reload__(old): + PARAM_TYPES.update(old.get('PARAM_TYPES', {})) + PARAM_NAMES.update(old.get('PARAM_NAMES', {})) class Parameter(QtCore.QObject): """ @@ -46,6 +49,7 @@ class Parameter(QtCore.QObject): including during editing. sigChildAdded(self, child, index) Emitted when a child is added sigChildRemoved(self, child) Emitted when a child is removed + sigRemoved(self) Emitted when this parameter is removed sigParentChanged(self, parent) Emitted when this parameter's parent has changed sigLimitsChanged(self, limits) Emitted when this parameter's limits have changed sigDefaultChanged(self, default) Emitted when this parameter's default value has changed @@ -61,6 +65,7 @@ class Parameter(QtCore.QObject): sigChildAdded = QtCore.Signal(object, object, object) ## self, child, index sigChildRemoved = QtCore.Signal(object, object) ## self, child + sigRemoved = QtCore.Signal(object) ## self sigParentChanged = QtCore.Signal(object, object) ## self, parent sigLimitsChanged = QtCore.Signal(object, object) ## self, limits sigDefaultChanged = QtCore.Signal(object, object) ## self, default @@ -133,6 +138,12 @@ class Parameter(QtCore.QObject): expanded If True, the Parameter will appear expanded when displayed in a ParameterTree (its children will be visible). (default=True) + title (str or None) If specified, then the parameter will be + displayed to the user using this string as its name. + However, the parameter will still be referred to + internally using the *name* specified above. Note that + this option is not compatible with renamable=True. + (default=None; added in version 0.9.9) ======================= ========================================================= """ @@ -148,6 +159,7 @@ class Parameter(QtCore.QObject): 'removable': False, 'strictNaming': False, # forces name to be usable as a python variable 'expanded': True, + 'title': None, #'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits. } self.opts.update(opts) @@ -266,16 +278,27 @@ class Parameter(QtCore.QObject): vals[ch.name()] = (ch.value(), ch.getValues()) return vals - def saveState(self): + def saveState(self, filter=None): """ Return a structure representing the entire state of the parameter tree. - The tree state may be restored from this structure using restoreState() + The tree state may be restored from this structure using restoreState(). + + If *filter* is set to 'user', then only user-settable data will be included in the + returned state. """ - state = self.opts.copy() - state['children'] = OrderedDict([(ch.name(), ch.saveState()) for ch in self]) - if state['type'] is None: - global PARAM_NAMES - state['type'] = PARAM_NAMES.get(type(self), None) + if filter is None: + state = self.opts.copy() + if state['type'] is None: + global PARAM_NAMES + state['type'] = PARAM_NAMES.get(type(self), None) + elif filter == 'user': + state = {'value': self.value()} + else: + raise ValueError("Unrecognized filter argument: '%s'" % filter) + + ch = OrderedDict([(ch.name(), ch.saveState(filter=filter)) for ch in self]) + if len(ch) > 0: + state['children'] = ch return state def restoreState(self, state, recursive=True, addChildren=True, removeChildren=True, blockSignals=True): @@ -293,8 +316,11 @@ class Parameter(QtCore.QObject): ## list of children may be stored either as list or dict. if isinstance(childState, dict): - childState = childState.values() - + cs = [] + for k,v in childState.items(): + cs.append(v.copy()) + cs[-1].setdefault('name', k) + childState = cs if blockSignals: self.blockTreeChangeSignal() @@ -311,14 +337,14 @@ class Parameter(QtCore.QObject): for ch in childState: name = ch['name'] - typ = ch['type'] + #typ = ch.get('type', None) #print('child: %s, %s' % (self.name()+'.'+name, typ)) - ## First, see if there is already a child with this name and type + ## First, see if there is already a child with this name gotChild = False for i, ch2 in enumerate(self.childs[ptr:]): #print " ", ch2.name(), ch2.type() - if ch2.name() != name or not ch2.isType(typ): + if ch2.name() != name: # or not ch2.isType(typ): continue gotChild = True #print " found it" @@ -393,15 +419,22 @@ class Parameter(QtCore.QObject): Note that the value of the parameter can *always* be changed by calling setValue(). """ - return not self.opts.get('readonly', False) + return not self.readonly() def setWritable(self, writable=True): """Set whether this Parameter should be editable by the user. (This is exactly the opposite of setReadonly).""" self.setOpts(readonly=not writable) + def readonly(self): + """ + Return True if this parameter is read-only. (this is the opposite of writable()) + """ + return self.opts.get('readonly', False) + def setReadonly(self, readonly=True): - """Set whether this Parameter's value may be edited by the user.""" + """Set whether this Parameter's value may be edited by the user + (this is the opposite of setWritable()).""" self.setOpts(readonly=readonly) def setOpts(self, **opts): @@ -453,11 +486,20 @@ class Parameter(QtCore.QObject): return ParameterItem(self, depth=depth) - def addChild(self, child): - """Add another parameter to the end of this parameter's child list.""" - return self.insertChild(len(self.childs), child) + def addChild(self, child, autoIncrementName=None): + """ + Add another parameter to the end of this parameter's child list. + + See insertChild() for a description of the *autoIncrementName* + argument. + """ + return self.insertChild(len(self.childs), child, autoIncrementName=autoIncrementName) def addChildren(self, children): + """ + Add a list or dict of children to this parameter. This method calls + addChild once for each value in *children*. + """ ## If children was specified as dict, then assume keys are the names. if isinstance(children, dict): ch2 = [] @@ -473,19 +515,24 @@ class Parameter(QtCore.QObject): self.addChild(chOpts) - def insertChild(self, pos, child): + def insertChild(self, pos, child, autoIncrementName=None): """ Insert a new child at pos. If pos is a Parameter, then insert at the position of that Parameter. If child is a dict, then a parameter is constructed using :func:`Parameter.create `. + + By default, the child's 'autoIncrementName' option determines whether + the name will be adjusted to avoid prior name collisions. This + behavior may be overridden by specifying the *autoIncrementName* + argument. This argument was added in version 0.9.9. """ if isinstance(child, dict): child = Parameter.create(**child) name = child.name() if name in self.names and child is not self.names[name]: - if child.opts.get('autoIncrementName', False): + if autoIncrementName is True or (autoIncrementName is None and child.opts.get('autoIncrementName', False)): name = self.incrementName(name) child.setName(name) else: @@ -550,6 +597,7 @@ class Parameter(QtCore.QObject): if parent is None: raise Exception("Cannot remove; no parent.") parent.removeChild(self) + self.sigRemoved.emit(self) def incrementName(self, name): ## return an unused name by adding a number to the name given @@ -590,9 +638,12 @@ class Parameter(QtCore.QObject): names = (names,) return self.param(*names).setValue(value) - def param(self, *names): + def child(self, *names): """Return a child parameter. - Accepts the name of the child or a tuple (path, to, child)""" + Accepts the name of the child or a tuple (path, to, child) + + Added in version 0.9.9. Ealier versions used the 'param' method, which is still + implemented for backward compatibility.""" try: param = self.names[names[0]] except KeyError: @@ -603,8 +654,12 @@ class Parameter(QtCore.QObject): else: return param + def param(self, *names): + # for backward compatibility. + return self.child(*names) + def __repr__(self): - return "<%s '%s' at 0x%x>" % (self.__class__.__name__, self.name(), id(self)) + return asUnicode("<%s '%s' at 0x%x>") % (self.__class__.__name__, self.name(), id(self)) def __getattr__(self, attr): ## Leaving this undocumented because I might like to remove it in the future.. @@ -692,7 +747,8 @@ class Parameter(QtCore.QObject): if self.blockTreeChangeEmit == 0: changes = self.treeStateChanges self.treeStateChanges = [] - self.sigTreeStateChanged.emit(self, changes) + if len(changes) > 0: + self.sigTreeStateChanged.emit(self, changes) class SignalBlocker(object): diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index 5a90becf..c149c411 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -1,4 +1,5 @@ from ..Qt import QtGui, QtCore +from ..python2_3 import asUnicode import os, weakref, re class ParameterItem(QtGui.QTreeWidgetItem): @@ -15,8 +16,11 @@ class ParameterItem(QtGui.QTreeWidgetItem): """ def __init__(self, param, depth=0): - QtGui.QTreeWidgetItem.__init__(self, [param.name(), '']) - + title = param.opts.get('title', None) + if title is None: + title = param.name() + QtGui.QTreeWidgetItem.__init__(self, [title, '']) + self.param = param self.param.registerItem(self) ## let parameter know this item is connected to it (for debugging) self.depth = depth @@ -30,7 +34,6 @@ class ParameterItem(QtGui.QTreeWidgetItem): param.sigOptionsChanged.connect(self.optsChanged) param.sigParentChanged.connect(self.parentChanged) - opts = param.opts ## Generate context menu for renaming/removing parameter @@ -38,6 +41,8 @@ class ParameterItem(QtGui.QTreeWidgetItem): self.contextMenu.addSeparator() flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled if opts.get('renamable', False): + if param.opts.get('title', None) is not None: + raise Exception("Cannot make parameter with both title != None and renamable == True.") flags |= QtCore.Qt.ItemIsEditable self.contextMenu.addAction('Rename').triggered.connect(self.editName) if opts.get('removable', False): @@ -107,15 +112,15 @@ class ParameterItem(QtGui.QTreeWidgetItem): self.contextMenu.popup(ev.globalPos()) def columnChangedEvent(self, col): - """Called when the text in a column has been edited. + """Called when the text in a column has been edited (or otherwise changed). By default, we only use changes to column 0 to rename the parameter. """ - if col == 0: + if col == 0 and (self.param.opts.get('title', None) is None): if self.ignoreNameColumnChange: return try: - newName = self.param.setName(str(self.text(col))) - except: + newName = self.param.setName(asUnicode(self.text(col))) + except Exception: self.setText(0, self.param.name()) raise @@ -127,8 +132,9 @@ class ParameterItem(QtGui.QTreeWidgetItem): def nameChanged(self, param, name): ## called when the parameter's name has changed. - self.setText(0, name) - + if self.param.opts.get('title', None) is None: + self.setText(0, name) + def limitsChanged(self, param, limits): """Called when the parameter's limits have changed""" pass diff --git a/pyqtgraph/parametertree/ParameterSystem.py b/pyqtgraph/parametertree/ParameterSystem.py new file mode 100644 index 00000000..33bb2de8 --- /dev/null +++ b/pyqtgraph/parametertree/ParameterSystem.py @@ -0,0 +1,127 @@ +from .parameterTypes import GroupParameter +from .. import functions as fn +from .SystemSolver import SystemSolver + + +class ParameterSystem(GroupParameter): + """ + ParameterSystem is a subclass of GroupParameter that manages a tree of + sub-parameters with a set of interdependencies--changing any one parameter + may affect other parameters in the system. + + See parametertree/SystemSolver for more information. + + NOTE: This API is experimental and may change substantially across minor + version numbers. + """ + def __init__(self, *args, **kwds): + GroupParameter.__init__(self, *args, **kwds) + self._system = None + self._fixParams = [] # all auto-generated 'fixed' params + sys = kwds.pop('system', None) + if sys is not None: + self.setSystem(sys) + self._ignoreChange = [] # params whose changes should be ignored temporarily + self.sigTreeStateChanged.connect(self.updateSystem) + + def setSystem(self, sys): + self._system = sys + + # auto-generate defaults to match child parameters + defaults = {} + vals = {} + for param in self: + name = param.name() + constraints = '' + if hasattr(sys, '_' + name): + constraints += 'n' + + if not param.readonly(): + constraints += 'f' + if 'n' in constraints: + ch = param.addChild(dict(name='fixed', type='bool', value=False)) + self._fixParams.append(ch) + param.setReadonly(True) + param.setOpts(expanded=False) + else: + vals[name] = param.value() + ch = param.addChild(dict(name='fixed', type='bool', value=True, readonly=True)) + #self._fixParams.append(ch) + + defaults[name] = [None, param.type(), None, constraints] + + sys.defaultState.update(defaults) + sys.reset() + for name, value in vals.items(): + setattr(sys, name, value) + + self.updateAllParams() + + def updateSystem(self, param, changes): + changes = [ch for ch in changes if ch[0] not in self._ignoreChange] + + #resets = [ch[0] for ch in changes if ch[1] == 'setToDefault'] + sets = [ch[0] for ch in changes if ch[1] == 'value'] + #for param in resets: + #setattr(self._system, param.name(), None) + + for param in sets: + #if param in resets: + #continue + + #if param in self._fixParams: + #param.parent().setWritable(param.value()) + #else: + if param in self._fixParams: + parent = param.parent() + if param.value(): + setattr(self._system, parent.name(), parent.value()) + else: + setattr(self._system, parent.name(), None) + else: + setattr(self._system, param.name(), param.value()) + + self.updateAllParams() + + def updateAllParams(self): + try: + self.sigTreeStateChanged.disconnect(self.updateSystem) + for name, state in self._system._vars.items(): + param = self.child(name) + try: + v = getattr(self._system, name) + if self._system._vars[name][2] is None: + self.updateParamState(self.child(name), 'autoSet') + param.setValue(v) + else: + self.updateParamState(self.child(name), 'fixed') + except RuntimeError: + self.updateParamState(param, 'autoUnset') + finally: + self.sigTreeStateChanged.connect(self.updateSystem) + + def updateParamState(self, param, state): + if state == 'autoSet': + bg = fn.mkBrush((200, 255, 200, 255)) + bold = False + readonly = True + elif state == 'autoUnset': + bg = fn.mkBrush(None) + bold = False + readonly = False + elif state == 'fixed': + bg = fn.mkBrush('y') + bold = True + readonly = False + + param.setReadonly(readonly) + + #for item in param.items: + #item.setBackground(0, bg) + #f = item.font(0) + #f.setWeight(f.Bold if bold else f.Normal) + #item.setFont(0, f) + + + + diff --git a/pyqtgraph/parametertree/SystemSolver.py b/pyqtgraph/parametertree/SystemSolver.py new file mode 100644 index 00000000..367210f2 --- /dev/null +++ b/pyqtgraph/parametertree/SystemSolver.py @@ -0,0 +1,381 @@ +from collections import OrderedDict +import numpy as np + +class SystemSolver(object): + """ + This abstract class is used to formalize and manage user interaction with a + complex system of equations (related to "constraint satisfaction problems"). + It is often the case that devices must be controlled + through a large number of free variables, and interactions between these + variables make the system difficult to manage and conceptualize as a user + interface. This class does _not_ attempt to numerically solve the system + of equations. Rather, it provides a framework for subdividing the system + into manageable pieces and specifying closed-form solutions to these small + pieces. + + For an example, see the simple Camera class below. + + Theory of operation: Conceptualize the system as 1) a set of variables + whose values may be either user-specified or automatically generated, and + 2) a set of functions that define *how* each variable should be generated. + When a variable is accessed (as an instance attribute), the solver first + checks to see if it already has a value (either user-supplied, or cached + from a previous calculation). If it does not, then the solver calls a + method on itself (the method must be named `_variableName`) that will + either return the calculated value (which usually involves acccessing + other variables in the system), or raise RuntimeError if it is unable to + calculate the value (usually because the user has not provided sufficient + input to fully constrain the system). + + Each method that calculates a variable value may include multiple + try/except blocks, so that if one method generates a RuntimeError, it may + fall back on others. + In this way, the system may be solved by recursively searching the tree of + possible relationships between variables. This allows the user flexibility + in deciding which variables are the most important to specify, while + avoiding the apparent combinatorial explosion of calculation pathways + that must be considered by the developer. + + Solved values are cached for efficiency, and automatically cleared when + a state change invalidates the cache. The rules for this are simple: any + time a value is set, it invalidates the cache *unless* the previous value + was None (which indicates that no other variable has yet requested that + value). More complex cache management may be defined in subclasses. + + + Subclasses must define: + + 1) The *defaultState* class attribute: This is a dict containing a + description of the variables in the system--their default values, + data types, and the ways they can be constrained. The format is:: + + { name: [value, type, constraint, allowed_constraints], ...} + + * *value* is the default value. May be None if it has not been specified + yet. + * *type* may be float, int, bool, np.ndarray, ... + * *constraint* may be None, single value, or (min, max) + * None indicates that the value is not constrained--it may be + automatically generated if the value is requested. + * *allowed_constraints* is a string composed of (n)one, (f)ixed, and (r)ange. + + Note: do not put mutable objects inside defaultState! + + 2) For each variable that may be automatically determined, a method must + be defined with the name `_variableName`. This method may either return + the + """ + + defaultState = OrderedDict() + + def __init__(self): + self.__dict__['_vars'] = OrderedDict() + self.__dict__['_currentGets'] = set() + self.reset() + + def reset(self): + """ + Reset all variables in the solver to their default state. + """ + self._currentGets.clear() + for k in self.defaultState: + self._vars[k] = self.defaultState[k][:] + + def __getattr__(self, name): + if name in self._vars: + return self.get(name) + raise AttributeError(name) + + def __setattr__(self, name, value): + """ + Set the value of a state variable. + If None is given for the value, then the constraint will also be set to None. + If a tuple is given for a scalar variable, then the tuple is used as a range constraint instead of a value. + Otherwise, the constraint is set to 'fixed'. + + """ + # First check this is a valid attribute + if name in self._vars: + if value is None: + self.set(name, value, None) + elif isinstance(value, tuple) and self._vars[name][1] is not np.ndarray: + self.set(name, None, value) + else: + self.set(name, value, 'fixed') + else: + # also allow setting any other pre-existing attribute + if hasattr(self, name): + object.__setattr__(self, name, value) + else: + raise AttributeError(name) + + def get(self, name): + """ + Return the value for parameter *name*. + + If the value has not been specified, then attempt to compute it from + other interacting parameters. + + If no value can be determined, then raise RuntimeError. + """ + if name in self._currentGets: + raise RuntimeError("Cyclic dependency while calculating '%s'." % name) + self._currentGets.add(name) + try: + v = self._vars[name][0] + if v is None: + cfunc = getattr(self, '_' + name, None) + if cfunc is None: + v = None + else: + v = cfunc() + if v is None: + raise RuntimeError("Parameter '%s' is not specified." % name) + v = self.set(name, v) + finally: + self._currentGets.remove(name) + + return v + + def set(self, name, value=None, constraint=True): + """ + Set a variable *name* to *value*. The actual set value is returned (in + some cases, the value may be cast into another type). + + If *value* is None, then the value is left to be determined in the + future. At any time, the value may be re-assigned arbitrarily unless + a constraint is given. + + If *constraint* is True (the default), then supplying a value that + violates a previously specified constraint will raise an exception. + + If *constraint* is 'fixed', then the value is set (if provided) and + the variable will not be updated automatically in the future. + + If *constraint* is a tuple, then the value is constrained to be within the + given (min, max). Either constraint may be None to disable + it. In some cases, a constraint cannot be satisfied automatically, + and the user will be forced to resolve the constraint manually. + + If *constraint* is None, then any constraints are removed for the variable. + """ + var = self._vars[name] + if constraint is None: + if 'n' not in var[3]: + raise TypeError("Empty constraints not allowed for '%s'" % name) + var[2] = constraint + elif constraint == 'fixed': + if 'f' not in var[3]: + raise TypeError("Fixed constraints not allowed for '%s'" % name) + var[2] = constraint + elif isinstance(constraint, tuple): + if 'r' not in var[3]: + raise TypeError("Range constraints not allowed for '%s'" % name) + assert len(constraint) == 2 + var[2] = constraint + elif constraint is not True: + raise TypeError("constraint must be None, True, 'fixed', or tuple. (got %s)" % constraint) + + # type checking / massaging + if var[1] is np.ndarray: + value = np.array(value, dtype=float) + elif var[1] in (int, float, tuple) and value is not None: + value = var[1](value) + + # constraint checks + if constraint is True and not self.check_constraint(name, value): + raise ValueError("Setting %s = %s violates constraint %s" % (name, value, var[2])) + + # invalidate other dependent values + if var[0] is not None: + # todo: we can make this more clever..(and might need to) + # we just know that a value of None cannot have dependencies + # (because if anyone else had asked for this value, it wouldn't be + # None anymore) + self.resetUnfixed() + + var[0] = value + return value + + def check_constraint(self, name, value): + c = self._vars[name][2] + if c is None or value is None: + return True + if isinstance(c, tuple): + return ((c[0] is None or c[0] <= value) and + (c[1] is None or c[1] >= value)) + else: + return value == c + + def saveState(self): + """ + Return a serializable description of the solver's current state. + """ + state = OrderedDict() + for name, var in self._vars.items(): + state[name] = (var[0], var[2]) + return state + + def restoreState(self, state): + """ + Restore the state of all values and constraints in the solver. + """ + self.reset() + for name, var in state.items(): + self.set(name, var[0], var[1]) + + def resetUnfixed(self): + """ + For any variable that does not have a fixed value, reset + its value to None. + """ + for var in self._vars.values(): + if var[2] != 'fixed': + var[0] = None + + def solve(self): + for k in self._vars: + getattr(self, k) + + def __repr__(self): + state = OrderedDict() + for name, var in self._vars.items(): + if var[2] == 'fixed': + state[name] = var[0] + state = ', '.join(["%s=%s" % (n, v) for n,v in state.items()]) + return "<%s %s>" % (self.__class__.__name__, state) + + + + + +if __name__ == '__main__': + + class Camera(SystemSolver): + """ + Consider a simple SLR camera. The variables we will consider that + affect the camera's behavior while acquiring a photo are aperture, shutter speed, + ISO, and flash (of course there are many more, but let's keep the example simple). + + In rare cases, the user wants to manually specify each of these variables and + no more work needs to be done to take the photo. More often, the user wants to + specify more interesting constraints like depth of field, overall exposure, + or maximum allowed ISO value. + + If we add a simple light meter measurement into this system and an 'exposure' + variable that indicates the desired exposure (0 is "perfect", -1 is one stop + darker, etc), then the system of equations governing the camera behavior would + have the following variables: + + aperture, shutter, iso, flash, exposure, light meter + + The first four variables are the "outputs" of the system (they directly drive + the camera), the last is a constant (the camera itself cannot affect the + reading on the light meter), and 'exposure' specifies a desired relationship + between other variables in the system. + + So the question is: how can I formalize a system like this as a user interface? + Typical cameras have a fairly limited approach: provide the user with a list + of modes, each of which defines a particular set of constraints. For example: + + manual: user provides aperture, shutter, iso, and flash + aperture priority: user provides aperture and exposure, camera selects + iso, shutter, and flash automatically + shutter priority: user provides shutter and exposure, camera selects + iso, aperture, and flash + program: user specifies exposure, camera selects all other variables + automatically + action: camera selects all variables while attempting to maximize + shutter speed + portrait: camera selects all variables while attempting to minimize + aperture + + A more general approach might allow the user to provide more explicit + constraints on each variable (for example: I want a shutter speed of 1/30 or + slower, an ISO no greater than 400, an exposure between -1 and 1, and the + smallest aperture possible given all other constraints) and have the camera + solve the system of equations, with a warning if no solution is found. This + is exactly what we will implement in this example class. + """ + + defaultState = OrderedDict([ + # Field stop aperture + ('aperture', [None, float, None, 'nf']), + # Duration that shutter is held open. + ('shutter', [None, float, None, 'nf']), + # ISO (sensitivity) value. 100, 200, 400, 800, 1600.. + ('iso', [None, int, None, 'nf']), + + # Flash is a value indicating the brightness of the flash. A table + # is used to decide on "balanced" settings for each flash level: + # 0: no flash + # 1: s=1/60, a=2.0, iso=100 + # 2: s=1/60, a=4.0, iso=100 ..and so on.. + ('flash', [None, float, None, 'nf']), + + # exposure is a value indicating how many stops brighter (+1) or + # darker (-1) the photographer would like the photo to appear from + # the 'balanced' settings indicated by the light meter (see below). + ('exposure', [None, float, None, 'f']), + + # Let's define this as an external light meter (not affected by + # aperture) with logarithmic output. We arbitrarily choose the + # following settings as "well balanced" for each light meter value: + # -1: s=1/60, a=2.0, iso=100 + # 0: s=1/60, a=4.0, iso=100 + # 1: s=1/120, a=4.0, iso=100 ..and so on.. + # Note that the only allowed constraint mode is (f)ixed, since the + # camera never _computes_ the light meter value, it only reads it. + ('lightMeter', [None, float, None, 'f']), + + # Indicates the camera's final decision on how it thinks the photo will + # look, given the chosen settings. This value is _only_ determined + # automatically. + ('balance', [None, float, None, 'n']), + ]) + + def _aperture(self): + """ + Determine aperture automatically under a variety of conditions. + """ + iso = self.iso + exp = self.exposure + light = self.lightMeter + + try: + # shutter-priority mode + sh = self.shutter # this raises RuntimeError if shutter has not + # been specified + ap = 4.0 * (sh / (1./60.)) * (iso / 100.) * (2 ** exp) * (2 ** light) + ap = np.clip(ap, 2.0, 16.0) + except RuntimeError: + # program mode; we can select a suitable shutter + # value at the same time. + sh = (1./60.) + raise + + + + return ap + + def _balance(self): + iso = self.iso + light = self.lightMeter + sh = self.shutter + ap = self.aperture + fl = self.flash + + bal = (4.0 / ap) * (sh / (1./60.)) * (iso / 100.) * (2 ** light) + return np.log2(bal) + + camera = Camera() + + camera.iso = 100 + camera.exposure = 0 + camera.lightMeter = 2 + camera.shutter = 1./60. + camera.flash = 0 + + camera.solve() + print camera.saveState() + \ No newline at end of file diff --git a/pyqtgraph/parametertree/__init__.py b/pyqtgraph/parametertree/__init__.py index acdb7a37..722410d5 100644 --- a/pyqtgraph/parametertree/__init__.py +++ b/pyqtgraph/parametertree/__init__.py @@ -1,5 +1,5 @@ from .Parameter import Parameter, registerParameterType from .ParameterTree import ParameterTree from .ParameterItem import ParameterItem - +from .ParameterSystem import ParameterSystem, SystemSolver from . import parameterTypes as types \ No newline at end of file diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 8aba4bca..7b1c5ee6 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -78,6 +78,7 @@ class WidgetParameterItem(ParameterItem): ## no starting value was given; use whatever the widget has self.widgetValueChanged() + self.updateDefaultBtn() def makeWidget(self): """ @@ -191,6 +192,9 @@ class WidgetParameterItem(ParameterItem): def updateDefaultBtn(self): ## enable/disable default btn self.defaultBtn.setEnabled(not self.param.valueIsDefault() and self.param.writable()) + + # hide / show + self.defaultBtn.setVisible(not self.param.readonly()) def updateDisplayLabel(self, value=None): """Update the display label to reflect the value of the parameter.""" @@ -234,6 +238,8 @@ class WidgetParameterItem(ParameterItem): self.widget.show() self.displayLabel.hide() self.widget.setFocus(QtCore.Qt.OtherFocusReason) + if isinstance(self.widget, SpinBox): + self.widget.selectNumber() # select the numerical portion of the text for quick editing def hideEditor(self): self.widget.hide() @@ -277,7 +283,7 @@ class WidgetParameterItem(ParameterItem): if 'readonly' in opts: self.updateDefaultBtn() if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)): - w.setEnabled(not opts['readonly']) + self.widget.setEnabled(not opts['readonly']) ## If widget is a SpinBox, pass options straight through if isinstance(self.widget, SpinBox): @@ -315,8 +321,8 @@ class SimpleParameter(Parameter): def colorValue(self): return fn.mkColor(Parameter.value(self)) - def saveColorState(self): - state = Parameter.saveState(self) + def saveColorState(self, *args, **kwds): + state = Parameter.saveState(self, *args, **kwds) state['value'] = fn.colorTuple(self.value()) return state @@ -539,7 +545,6 @@ class ListParameter(Parameter): self.forward, self.reverse = self.mapping(limits) Parameter.setLimits(self, limits) - #print self.name(), self.value(), limits, self.reverse if len(self.reverse[0]) > 0 and self.value() not in self.reverse[0]: self.setValue(self.reverse[0][0]) diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index 47fa266d..f622dd87 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -61,6 +61,19 @@ def test_interpolateArray(): assert_array_almost_equal(r1, r2) +def test_subArray(): + a = np.array([0, 0, 111, 112, 113, 0, 121, 122, 123, 0, 0, 0, 211, 212, 213, 0, 221, 222, 223, 0, 0, 0, 0]) + b = pg.subArray(a, offset=2, shape=(2,2,3), stride=(10,4,1)) + c = np.array([[[111,112,113], [121,122,123]], [[211,212,213], [221,222,223]]]) + assert np.all(b == c) + + # operate over first axis; broadcast over the rest + aa = np.vstack([a, a/100.]).T + cc = np.empty(c.shape + (2,)) + cc[..., 0] = c + cc[..., 1] = c / 100. + bb = pg.subArray(aa, offset=2, shape=(2,2,3), stride=(10,4,1)) + assert np.all(bb == cc) diff --git a/pyqtgraph/util/garbage_collector.py b/pyqtgraph/util/garbage_collector.py new file mode 100644 index 00000000..979e66c5 --- /dev/null +++ b/pyqtgraph/util/garbage_collector.py @@ -0,0 +1,50 @@ +import gc + +from ..Qt import QtCore + +class GarbageCollector(object): + ''' + Disable automatic garbage collection and instead collect manually + on a timer. + + This is done to ensure that garbage collection only happens in the GUI + thread, as otherwise Qt can crash. + + Credit: Erik Janssens + Source: http://pydev.blogspot.com/2014/03/should-python-garbage-collector-be.html + ''' + + def __init__(self, interval=1.0, debug=False): + self.debug = debug + if debug: + gc.set_debug(gc.DEBUG_LEAK) + + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.check) + + self.threshold = gc.get_threshold() + gc.disable() + self.timer.start(interval * 1000) + + def check(self): + #return self.debug_cycles() # uncomment to just debug cycles + l0, l1, l2 = gc.get_count() + if self.debug: + print('gc_check called:', l0, l1, l2) + if l0 > self.threshold[0]: + num = gc.collect(0) + if self.debug: + print('collecting gen 0, found: %d unreachable' % num) + if l1 > self.threshold[1]: + num = gc.collect(1) + if self.debug: + print('collecting gen 1, found: %d unreachable' % num) + if l2 > self.threshold[2]: + num = gc.collect(2) + if self.debug: + print('collecting gen 2, found: %d unreachable' % num) + + def debug_cycles(self): + gc.collect() + for obj in gc.garbage: + print (obj, repr(obj), type(obj)) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index 8cd72e15..f6e28960 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -19,8 +19,8 @@ class ColorMapWidget(ptree.ParameterTree): """ sigColorMapChanged = QtCore.Signal(object) - def __init__(self): - ptree.ParameterTree.__init__(self, showHeader=False) + def __init__(self, parent=None): + ptree.ParameterTree.__init__(self, parent=parent, showHeader=False) self.params = ColorMapParameter() self.setParameters(self.params) @@ -32,6 +32,15 @@ class ColorMapWidget(ptree.ParameterTree): def mapChanged(self): self.sigColorMapChanged.emit(self) + + def widgetGroupInterface(self): + return (self.sigColorMapChanged, self.saveState, self.restoreState) + + def saveState(self): + return self.params.saveState() + + def restoreState(self, state): + self.params.restoreState(state) class ColorMapParameter(ptree.types.GroupParameter): @@ -48,9 +57,11 @@ class ColorMapParameter(ptree.types.GroupParameter): def addNew(self, name): mode = self.fields[name].get('mode', 'range') if mode == 'range': - self.addChild(RangeColorMapItem(name, self.fields[name])) + item = RangeColorMapItem(name, self.fields[name]) elif mode == 'enum': - self.addChild(EnumColorMapItem(name, self.fields[name])) + item = EnumColorMapItem(name, self.fields[name]) + self.addChild(item) + return item def fieldNames(self): return self.fields.keys() @@ -95,6 +106,9 @@ class ColorMapParameter(ptree.types.GroupParameter): returned as 0.0-1.0 float values. ============== ================================================================= """ + if isinstance(data, dict): + data = np.array([tuple(data.values())], dtype=[(k, float) for k in data.keys()]) + colors = np.zeros((len(data),4)) for item in self.children(): if not item['Enabled']: @@ -126,8 +140,26 @@ class ColorMapParameter(ptree.types.GroupParameter): return colors + def saveState(self): + items = OrderedDict() + for item in self: + itemState = item.saveState(filter='user') + itemState['field'] = item.fieldName + items[item.name()] = itemState + state = {'fields': self.fields, 'items': items} + return state + + def restoreState(self, state): + if 'fields' in state: + self.setFields(state['fields']) + for itemState in state['items']: + item = self.addNew(itemState['field']) + item.restoreState(itemState) + class RangeColorMapItem(ptree.types.SimpleParameter): + mapType = 'range' + def __init__(self, name, opts): self.fieldName = name units = opts.get('units', '') @@ -151,8 +183,6 @@ class RangeColorMapItem(ptree.types.SimpleParameter): def map(self, data): data = data[self.fieldName] - - scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1) cmap = self.value() colors = cmap.map(scaled, mode='float') @@ -162,10 +192,11 @@ class RangeColorMapItem(ptree.types.SimpleParameter): nanColor = (nanColor.red()/255., nanColor.green()/255., nanColor.blue()/255., nanColor.alpha()/255.) colors[mask] = nanColor - return colors - + return colors class EnumColorMapItem(ptree.types.GroupParameter): + mapType = 'enum' + def __init__(self, name, opts): self.fieldName = name vals = opts.get('values', []) diff --git a/pyqtgraph/widgets/ComboBox.py b/pyqtgraph/widgets/ComboBox.py index f9983c97..5cf6f918 100644 --- a/pyqtgraph/widgets/ComboBox.py +++ b/pyqtgraph/widgets/ComboBox.py @@ -1,5 +1,6 @@ from ..Qt import QtGui, QtCore from ..SignalProxy import SignalProxy +import sys from ..pgcollections import OrderedDict from ..python2_3 import asUnicode @@ -20,6 +21,10 @@ class ComboBox(QtGui.QComboBox): self.currentIndexChanged.connect(self.indexChanged) self._ignoreIndexChange = False + #self.value = default + if 'darwin' in sys.platform: ## because MacOSX can show names that are wider than the comboBox + self.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength) + #self.setMinimumContentsLength(10) self._chosenText = None self._items = OrderedDict() diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index b99121bf..29e60319 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -57,7 +57,7 @@ class DataTreeWidget(QtGui.QTreeWidget): } if isinstance(data, dict): - for k in data: + for k in data.keys(): self.buildTree(data[k], node, str(k)) elif isinstance(data, list) or isinstance(data, tuple): for i in range(len(data)): diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 422522de..23516827 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -47,29 +47,29 @@ class SpinBox(QtGui.QAbstractSpinBox): """ ============== ======================================================================== **Arguments:** - parent Sets the parent widget for this SpinBox (optional) - value (float/int) initial value + parent Sets the parent widget for this SpinBox (optional). Default is None. + value (float/int) initial value. Default is 0.0. bounds (min,max) Minimum and maximum values allowed in the SpinBox. - Either may be None to leave the value unbounded. - suffix (str) suffix (units) to display after the numerical value + Either may be None to leave the value unbounded. By default, values are unbounded. + suffix (str) suffix (units) to display after the numerical value. By default, suffix is an empty str. siPrefix (bool) If True, then an SI prefix is automatically prepended to the units and the value is scaled accordingly. For example, if value=0.003 and suffix='V', then the SpinBox will display - "300 mV" (but a call to SpinBox.value will still return 0.003). + "300 mV" (but a call to SpinBox.value will still return 0.003). Default is False. step (float) The size of a single step. This is used when clicking the up/ down arrows, when rolling the mouse wheel, or when pressing keyboard arrows while the widget has keyboard focus. Note that the interpretation of this value is different when specifying - the 'dec' argument. + the 'dec' argument. Default is 0.01. dec (bool) If True, then the step value will be adjusted to match the current size of the variable (for example, a value of 15 might step in increments of 1 whereas a value of 1500 would step in increments of 100). In this case, the 'step' argument is interpreted *relative* to the current value. The most common - 'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0. + 'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0. Default is False. minStep (float) When dec=True, this specifies the minimum allowable step size. - int (bool) if True, the value is forced to integer type - decimals (int) Number of decimal values to display + int (bool) if True, the value is forced to integer type. Default is False + decimals (int) Number of decimal values to display. Default is 2. ============== ======================================================================== """ QtGui.QAbstractSpinBox.__init__(self, parent) @@ -233,6 +233,18 @@ class SpinBox(QtGui.QAbstractSpinBox): def setDecimals(self, decimals): self.setOpts(decimals=decimals) + + def selectNumber(self): + """ + Select the numerical portion of the text to allow quick editing by the user. + """ + le = self.lineEdit() + text = le.text() + try: + index = text.index(' ') + except ValueError: + return + le.setSelection(0, index) def value(self): """ diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 14060546..69085a20 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -365,7 +365,7 @@ class TableWidget(QtGui.QTableWidget): ev.ignore() def handleItemChanged(self, item): - item.textChanged() + item.itemChanged() class TableWidgetItem(QtGui.QTableWidgetItem): @@ -425,7 +425,8 @@ class TableWidgetItem(QtGui.QTableWidgetItem): def _updateText(self): self._blockValueChange = True try: - self.setText(self.format()) + self._text = self.format() + self.setText(self._text) finally: self._blockValueChange = False @@ -433,14 +434,22 @@ class TableWidgetItem(QtGui.QTableWidgetItem): self.value = value self._updateText() + def itemChanged(self): + """Called when the data of this item has changed.""" + if self.text() != self._text: + self.textChanged() + def textChanged(self): """Called when this item's text has changed for any reason.""" + self._text = self.text() + if self._blockValueChange: # text change was result of value or format change; do not # propagate. return try: + self.value = type(self.value)(self.text()) except ValueError: self.value = str(self.text()) From 1df5103d94a1f04c534a829a62df2deffedcbfa8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 7 Aug 2014 09:11:34 -0400 Subject: [PATCH 241/268] Fixes following acq4 merge --- pyqtgraph/exporters/tests/test_csv.py | 2 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 6 ++++-- pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 2 +- pyqtgraph/parametertree/SystemSolver.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/exporters/tests/test_csv.py b/pyqtgraph/exporters/tests/test_csv.py index 70c69c72..a98372ec 100644 --- a/pyqtgraph/exporters/tests/test_csv.py +++ b/pyqtgraph/exporters/tests/test_csv.py @@ -29,7 +29,7 @@ def test_CSVExporter(): r = csv.reader(open('test.csv', 'r')) lines = [line for line in r] header = lines.pop(0) - assert header == ['myPlot_x', 'myPlot_y', 'x', 'y', 'x', 'y'] + assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002'] i = 0 for vals in lines: diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 542bbc1a..ceca62c8 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -427,11 +427,11 @@ class ViewBox(GraphicsWidget): self.linkedYChanged() self.updateAutoRange() self.updateViewRange() + self._matrixNeedsUpdate = True self.sigStateChanged.emit(self) self.background.setRect(self.rect()) self.sigResized.emit(self) - def viewRange(self): """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]""" return [x[:] for x in self.state['viewRange']] ## return copy @@ -909,7 +909,7 @@ class ViewBox(GraphicsWidget): if k in args: if not np.all(np.isfinite(args[k])): r = args.pop(k) - print "Warning: %s is invalid: %s" % (k, str(r)) + #print("Warning: %s is invalid: %s" % (k, str(r)) self.setRange(**args) finally: @@ -1135,6 +1135,8 @@ class ViewBox(GraphicsWidget): Return the transform that maps from child(item in the childGroup) coordinates to local coordinates. (This maps from inside the viewbox to outside) """ + if self._matrixNeedsUpdate: + self.updateMatrix() m = self.childGroup.transform() #m1 = QtGui.QTransform() #m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y()) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index 7cb366c2..f1063e7f 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -20,11 +20,11 @@ def test_ViewBox(): win.show() vb = win.addViewBox() + # set range before viewbox is shown vb.setRange(xRange=[0, 10], yRange=[0, 10], padding=0) # required to make mapFromView work properly. qtest.qWaitForWindowShown(win) - vb.update() g = pg.GridItem() vb.addItem(g) diff --git a/pyqtgraph/parametertree/SystemSolver.py b/pyqtgraph/parametertree/SystemSolver.py index 367210f2..0a889dfa 100644 --- a/pyqtgraph/parametertree/SystemSolver.py +++ b/pyqtgraph/parametertree/SystemSolver.py @@ -377,5 +377,5 @@ if __name__ == '__main__': camera.flash = 0 camera.solve() - print camera.saveState() + print(camera.saveState()) \ No newline at end of file From 42eae475b9e95cefb3c05a18bd03435c33443570 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 7 Aug 2014 09:12:31 -0400 Subject: [PATCH 242/268] Disable image downsampling when n=1 --- pyqtgraph/functions.py | 2 ++ pyqtgraph/graphicsItems/ImageItem.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 6ae2f65b..897a123d 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1222,6 +1222,8 @@ def downsample(data, n, axis=0, xvals='subsample'): data = downsample(data, n[i], axis[i]) return data + if n <= 1: + return data nPts = int(data.shape[axis] / n) s = list(data.shape) s[axis] = nPts diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 5c39627c..5b041433 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -9,6 +9,8 @@ from .GraphicsObject import GraphicsObject from ..Point import Point __all__ = ['ImageItem'] + + class ImageItem(GraphicsObject): """ **Bases:** :class:`GraphicsObject ` From 706fe92fdbed99fa25760b0b381bdd4bcf8de180 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 7 Aug 2014 09:13:32 -0400 Subject: [PATCH 243/268] Remove old PIL-fix files, replace with equivalent runtime-patching code. --- pyqtgraph/PIL_Fix/Image.py-1.6 | 2099 ------------------------------- pyqtgraph/PIL_Fix/Image.py-1.7 | 2129 -------------------------------- pyqtgraph/PIL_Fix/README | 11 - pyqtgraph/util/pil_fix.py | 64 + 4 files changed, 64 insertions(+), 4239 deletions(-) delete mode 100644 pyqtgraph/PIL_Fix/Image.py-1.6 delete mode 100644 pyqtgraph/PIL_Fix/Image.py-1.7 delete mode 100644 pyqtgraph/PIL_Fix/README create mode 100644 pyqtgraph/util/pil_fix.py diff --git a/pyqtgraph/PIL_Fix/Image.py-1.6 b/pyqtgraph/PIL_Fix/Image.py-1.6 deleted file mode 100644 index 2b373059..00000000 --- a/pyqtgraph/PIL_Fix/Image.py-1.6 +++ /dev/null @@ -1,2099 +0,0 @@ -# -# The Python Imaging Library. -# $Id: Image.py 2933 2006-12-03 12:08:22Z fredrik $ -# -# the Image class wrapper -# -# partial release history: -# 1995-09-09 fl Created -# 1996-03-11 fl PIL release 0.0 (proof of concept) -# 1996-04-30 fl PIL release 0.1b1 -# 1999-07-28 fl PIL release 1.0 final -# 2000-06-07 fl PIL release 1.1 -# 2000-10-20 fl PIL release 1.1.1 -# 2001-05-07 fl PIL release 1.1.2 -# 2002-03-15 fl PIL release 1.1.3 -# 2003-05-10 fl PIL release 1.1.4 -# 2005-03-28 fl PIL release 1.1.5 -# 2006-12-02 fl PIL release 1.1.6 -# -# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-2006 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -VERSION = "1.1.6" - -try: - import warnings -except ImportError: - warnings = None - -class _imaging_not_installed: - # module placeholder - def __getattr__(self, id): - raise ImportError("The _imaging C module is not installed") - -try: - # give Tk a chance to set up the environment, in case we're - # using an _imaging module linked against libtcl/libtk (use - # __import__ to hide this from naive packagers; we don't really - # depend on Tk unless ImageTk is used, and that module already - # imports Tkinter) - __import__("FixTk") -except ImportError: - pass - -try: - # If the _imaging C module is not present, you can still use - # the "open" function to identify files, but you cannot load - # them. Note that other modules should not refer to _imaging - # directly; import Image and use the Image.core variable instead. - import _imaging - core = _imaging - del _imaging -except ImportError, v: - core = _imaging_not_installed() - if str(v)[:20] == "Module use of python" and warnings: - # The _imaging C module is present, but not compiled for - # the right version (windows only). Print a warning, if - # possible. - warnings.warn( - "The _imaging extension was built for another version " - "of Python; most PIL functions will be disabled", - RuntimeWarning - ) - -import ImageMode -import ImagePalette - -import os, string, sys - -# type stuff -from types import IntType, StringType, TupleType - -try: - UnicodeStringType = type(unicode("")) - ## - # (Internal) Checks if an object is a string. If the current - # Python version supports Unicode, this checks for both 8-bit - # and Unicode strings. - def isStringType(t): - return isinstance(t, StringType) or isinstance(t, UnicodeStringType) -except NameError: - def isStringType(t): - return isinstance(t, StringType) - -## -# (Internal) Checks if an object is a tuple. - -def isTupleType(t): - return isinstance(t, TupleType) - -## -# (Internal) Checks if an object is an image object. - -def isImageType(t): - return hasattr(t, "im") - -## -# (Internal) Checks if an object is a string, and that it points to a -# directory. - -def isDirectory(f): - return isStringType(f) and os.path.isdir(f) - -from operator import isNumberType, isSequenceType - -# -# Debug level - -DEBUG = 0 - -# -# Constants (also defined in _imagingmodule.c!) - -NONE = 0 - -# transpose -FLIP_LEFT_RIGHT = 0 -FLIP_TOP_BOTTOM = 1 -ROTATE_90 = 2 -ROTATE_180 = 3 -ROTATE_270 = 4 - -# transforms -AFFINE = 0 -EXTENT = 1 -PERSPECTIVE = 2 -QUAD = 3 -MESH = 4 - -# resampling filters -NONE = 0 -NEAREST = 0 -ANTIALIAS = 1 # 3-lobed lanczos -LINEAR = BILINEAR = 2 -CUBIC = BICUBIC = 3 - -# dithers -NONE = 0 -NEAREST = 0 -ORDERED = 1 # Not yet implemented -RASTERIZE = 2 # Not yet implemented -FLOYDSTEINBERG = 3 # default - -# palettes/quantizers -WEB = 0 -ADAPTIVE = 1 - -# categories -NORMAL = 0 -SEQUENCE = 1 -CONTAINER = 2 - -# -------------------------------------------------------------------- -# Registries - -ID = [] -OPEN = {} -MIME = {} -SAVE = {} -EXTENSION = {} - -# -------------------------------------------------------------------- -# Modes supported by this version - -_MODEINFO = { - # NOTE: this table will be removed in future versions. use - # getmode* functions or ImageMode descriptors instead. - - # official modes - "1": ("L", "L", ("1",)), - "L": ("L", "L", ("L",)), - "I": ("L", "I", ("I",)), - "F": ("L", "F", ("F",)), - "P": ("RGB", "L", ("P",)), - "RGB": ("RGB", "L", ("R", "G", "B")), - "RGBX": ("RGB", "L", ("R", "G", "B", "X")), - "RGBA": ("RGB", "L", ("R", "G", "B", "A")), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), - - # Experimental modes include I;16, I;16B, RGBa, BGR;15, - # and BGR;24. Use these modes only if you know exactly - # what you're doing... - -} - -if sys.byteorder == 'little': - _ENDIAN = '<' -else: - _ENDIAN = '>' - -_MODE_CONV = { - # official modes - "1": ('|b1', None), - "L": ('|u1', None), - "I": ('%si4' % _ENDIAN, None), # FIXME: is this correct? - "I;16": ('%su2' % _ENDIAN, None), # FIXME: is this correct? - "F": ('%sf4' % _ENDIAN, None), # FIXME: is this correct? - "P": ('|u1', None), - "RGB": ('|u1', 3), - "RGBX": ('|u1', 4), - "RGBA": ('|u1', 4), - "CMYK": ('|u1', 4), - "YCbCr": ('|u1', 4), -} - -def _conv_type_shape(im): - shape = im.size[::-1] - typ, extra = _MODE_CONV[im.mode] - if extra is None: - return shape, typ - else: - return shape+(extra,), typ - - -MODES = _MODEINFO.keys() -MODES.sort() - -# raw modes that may be memory mapped. NOTE: if you change this, you -# may have to modify the stride calculation in map.c too! -_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16B") - -## -# Gets the "base" mode for given mode. This function returns "L" for -# images that contain grayscale data, and "RGB" for images that -# contain color data. -# -# @param mode Input mode. -# @return "L" or "RGB". -# @exception KeyError If the input mode was not a standard mode. - -def getmodebase(mode): - return ImageMode.getmode(mode).basemode - -## -# Gets the storage type mode. Given a mode, this function returns a -# single-layer mode suitable for storing individual bands. -# -# @param mode Input mode. -# @return "L", "I", or "F". -# @exception KeyError If the input mode was not a standard mode. - -def getmodetype(mode): - return ImageMode.getmode(mode).basetype - -## -# Gets a list of individual band names. Given a mode, this function -# returns a tuple containing the names of individual bands (use -# {@link #getmodetype} to get the mode used to store each individual -# band. -# -# @param mode Input mode. -# @return A tuple containing band names. The length of the tuple -# gives the number of bands in an image of the given mode. -# @exception KeyError If the input mode was not a standard mode. - -def getmodebandnames(mode): - return ImageMode.getmode(mode).bands - -## -# Gets the number of individual bands for this mode. -# -# @param mode Input mode. -# @return The number of bands in this mode. -# @exception KeyError If the input mode was not a standard mode. - -def getmodebands(mode): - return len(ImageMode.getmode(mode).bands) - -# -------------------------------------------------------------------- -# Helpers - -_initialized = 0 - -## -# Explicitly loads standard file format drivers. - -def preinit(): - "Load standard file format drivers." - - global _initialized - if _initialized >= 1: - return - - try: - import BmpImagePlugin - except ImportError: - pass - try: - import GifImagePlugin - except ImportError: - pass - try: - import JpegImagePlugin - except ImportError: - pass - try: - import PpmImagePlugin - except ImportError: - pass - try: - import PngImagePlugin - except ImportError: - pass -# try: -# import TiffImagePlugin -# except ImportError: -# pass - - _initialized = 1 - -## -# Explicitly initializes the Python Imaging Library. This function -# loads all available file format drivers. - -def init(): - "Load all file format drivers." - - global _initialized - if _initialized >= 2: - return - - visited = {} - - directories = sys.path - - try: - directories = directories + [os.path.dirname(__file__)] - except NameError: - pass - - # only check directories (including current, if present in the path) - for directory in filter(isDirectory, directories): - fullpath = os.path.abspath(directory) - if visited.has_key(fullpath): - continue - for file in os.listdir(directory): - if file[-14:] == "ImagePlugin.py": - f, e = os.path.splitext(file) - try: - sys.path.insert(0, directory) - try: - __import__(f, globals(), locals(), []) - finally: - del sys.path[0] - except ImportError: - if DEBUG: - print "Image: failed to import", - print f, ":", sys.exc_value - visited[fullpath] = None - - if OPEN or SAVE: - _initialized = 2 - - -# -------------------------------------------------------------------- -# Codec factories (used by tostring/fromstring and ImageFile.load) - -def _getdecoder(mode, decoder_name, args, extra=()): - - # tweak arguments - if args is None: - args = () - elif not isTupleType(args): - args = (args,) - - try: - # get decoder - decoder = getattr(core, decoder_name + "_decoder") - # print decoder, (mode,) + args + extra - return apply(decoder, (mode,) + args + extra) - except AttributeError: - raise IOError("decoder %s not available" % decoder_name) - -def _getencoder(mode, encoder_name, args, extra=()): - - # tweak arguments - if args is None: - args = () - elif not isTupleType(args): - args = (args,) - - try: - # get encoder - encoder = getattr(core, encoder_name + "_encoder") - # print encoder, (mode,) + args + extra - return apply(encoder, (mode,) + args + extra) - except AttributeError: - raise IOError("encoder %s not available" % encoder_name) - - -# -------------------------------------------------------------------- -# Simple expression analyzer - -class _E: - def __init__(self, data): self.data = data - def __coerce__(self, other): return self, _E(other) - def __add__(self, other): return _E((self.data, "__add__", other.data)) - def __mul__(self, other): return _E((self.data, "__mul__", other.data)) - -def _getscaleoffset(expr): - stub = ["stub"] - data = expr(_E(stub)).data - try: - (a, b, c) = data # simplified syntax - if (a is stub and b == "__mul__" and isNumberType(c)): - return c, 0.0 - if (a is stub and b == "__add__" and isNumberType(c)): - return 1.0, c - except TypeError: pass - try: - ((a, b, c), d, e) = data # full syntax - if (a is stub and b == "__mul__" and isNumberType(c) and - d == "__add__" and isNumberType(e)): - return c, e - except TypeError: pass - raise ValueError("illegal expression") - - -# -------------------------------------------------------------------- -# Implementation wrapper - -## -# This class represents an image object. To create Image objects, use -# the appropriate factory functions. There's hardly ever any reason -# to call the Image constructor directly. -# -# @see #open -# @see #new -# @see #fromstring - -class Image: - - format = None - format_description = None - - def __init__(self): - self.im = None - self.mode = "" - self.size = (0, 0) - self.palette = None - self.info = {} - self.category = NORMAL - self.readonly = 0 - - def _new(self, im): - new = Image() - new.im = im - new.mode = im.mode - new.size = im.size - new.palette = self.palette - if im.mode == "P": - new.palette = ImagePalette.ImagePalette() - try: - new.info = self.info.copy() - except AttributeError: - # fallback (pre-1.5.2) - new.info = {} - for k, v in self.info: - new.info[k] = v - return new - - _makeself = _new # compatibility - - def _copy(self): - self.load() - self.im = self.im.copy() - self.readonly = 0 - - def _dump(self, file=None, format=None): - import tempfile - if not file: - file = tempfile.mktemp() - self.load() - if not format or format == "PPM": - self.im.save_ppm(file) - else: - file = file + "." + format - self.save(file, format) - return file - - def __getattr__(self, name): - if name == "__array_interface__": - # numpy array interface support - new = {} - shape, typestr = _conv_type_shape(self) - new['shape'] = shape - new['typestr'] = typestr - new['data'] = self.tostring() - return new - raise AttributeError(name) - - ## - # Returns a string containing pixel data. - # - # @param encoder_name What encoder to use. The default is to - # use the standard "raw" encoder. - # @param *args Extra arguments to the encoder. - # @return An 8-bit string. - - def tostring(self, encoder_name="raw", *args): - "Return image as a binary string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if encoder_name == "raw" and args == (): - args = self.mode - - self.load() - - # unpack data - e = _getencoder(self.mode, encoder_name, args) - e.setimage(self.im) - - bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - - data = [] - while 1: - l, s, d = e.encode(bufsize) - data.append(d) - if s: - break - if s < 0: - raise RuntimeError("encoder error %d in tostring" % s) - - return string.join(data, "") - - ## - # Returns the image converted to an X11 bitmap. This method - # only works for mode "1" images. - # - # @param name The name prefix to use for the bitmap variables. - # @return A string containing an X11 bitmap. - # @exception ValueError If the mode is not "1" - - def tobitmap(self, name="image"): - "Return image as an XBM bitmap" - - self.load() - if self.mode != "1": - raise ValueError("not a bitmap") - data = self.tostring("xbm") - return string.join(["#define %s_width %d\n" % (name, self.size[0]), - "#define %s_height %d\n"% (name, self.size[1]), - "static char %s_bits[] = {\n" % name, data, "};"], "") - - ## - # Loads this image with pixel data from a string. - #

- # This method is similar to the {@link #fromstring} function, but - # loads data into this image instead of creating a new image - # object. - - def fromstring(self, data, decoder_name="raw", *args): - "Load data to image from binary string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - # default format - if decoder_name == "raw" and args == (): - args = self.mode - - # unpack data - d = _getdecoder(self.mode, decoder_name, args) - d.setimage(self.im) - s = d.decode(data) - - if s[0] >= 0: - raise ValueError("not enough image data") - if s[1] != 0: - raise ValueError("cannot decode image data") - - ## - # Allocates storage for the image and loads the pixel data. In - # normal cases, you don't need to call this method, since the - # Image class automatically loads an opened image when it is - # accessed for the first time. - # - # @return An image access object. - - def load(self): - "Explicitly load pixel data." - if self.im and self.palette and self.palette.dirty: - # realize palette - apply(self.im.putpalette, self.palette.getdata()) - self.palette.dirty = 0 - self.palette.mode = "RGB" - self.palette.rawmode = None - if self.info.has_key("transparency"): - self.im.putpalettealpha(self.info["transparency"], 0) - self.palette.mode = "RGBA" - if self.im: - return self.im.pixel_access(self.readonly) - - ## - # Verifies the contents of a file. For data read from a file, this - # method attempts to determine if the file is broken, without - # actually decoding the image data. If this method finds any - # problems, it raises suitable exceptions. If you need to load - # the image after using this method, you must reopen the image - # file. - - def verify(self): - "Verify file contents." - pass - - - ## - # Returns a converted copy of this image. For the "P" mode, this - # method translates pixels through the palette. If mode is - # omitted, a mode is chosen so that all information in the image - # and the palette can be represented without a palette. - #

- # The current version supports all possible conversions between - # "L", "RGB" and "CMYK." - #

- # When translating a colour image to black and white (mode "L"), - # the library uses the ITU-R 601-2 luma transform: - #

- # L = R * 299/1000 + G * 587/1000 + B * 114/1000 - #

- # When translating a greyscale image into a bilevel image (mode - # "1"), all non-zero values are set to 255 (white). To use other - # thresholds, use the {@link #Image.point} method. - # - # @def convert(mode, matrix=None) - # @param mode The requested mode. - # @param matrix An optional conversion matrix. If given, this - # should be 4- or 16-tuple containing floating point values. - # @return An Image object. - - def convert(self, mode=None, data=None, dither=None, - palette=WEB, colors=256): - "Convert to other pixel format" - - if not mode: - # determine default mode - if self.mode == "P": - self.load() - if self.palette: - mode = self.palette.mode - else: - mode = "RGB" - else: - return self.copy() - - self.load() - - if data: - # matrix conversion - if mode not in ("L", "RGB"): - raise ValueError("illegal conversion") - im = self.im.convert_matrix(mode, data) - return self._new(im) - - if mode == "P" and palette == ADAPTIVE: - im = self.im.quantize(colors) - return self._new(im) - - # colourspace conversion - if dither is None: - dither = FLOYDSTEINBERG - - try: - im = self.im.convert(mode, dither) - except ValueError: - try: - # normalize source image and try again - im = self.im.convert(getmodebase(self.mode)) - im = im.convert(mode, dither) - except KeyError: - raise ValueError("illegal conversion") - - return self._new(im) - - def quantize(self, colors=256, method=0, kmeans=0, palette=None): - - # methods: - # 0 = median cut - # 1 = maximum coverage - - # NOTE: this functionality will be moved to the extended - # quantizer interface in a later version of PIL. - - self.load() - - if palette: - # use palette from reference image - palette.load() - if palette.mode != "P": - raise ValueError("bad mode for palette image") - if self.mode != "RGB" and self.mode != "L": - raise ValueError( - "only RGB or L mode images can be quantized to a palette" - ) - im = self.im.convert("P", 1, palette.im) - return self._makeself(im) - - im = self.im.quantize(colors, method, kmeans) - return self._new(im) - - ## - # Copies this image. Use this method if you wish to paste things - # into an image, but still retain the original. - # - # @return An Image object. - - def copy(self): - "Copy raster data" - - self.load() - im = self.im.copy() - return self._new(im) - - ## - # Returns a rectangular region from this image. The box is a - # 4-tuple defining the left, upper, right, and lower pixel - # coordinate. - #

- # This is a lazy operation. Changes to the source image may or - # may not be reflected in the cropped image. To break the - # connection, call the {@link #Image.load} method on the cropped - # copy. - # - # @param The crop rectangle, as a (left, upper, right, lower)-tuple. - # @return An Image object. - - def crop(self, box=None): - "Crop region from image" - - self.load() - if box is None: - return self.copy() - - # lazy operation - return _ImageCrop(self, box) - - ## - # Configures the image file loader so it returns a version of the - # image that as closely as possible matches the given mode and - # size. For example, you can use this method to convert a colour - # JPEG to greyscale while loading it, or to extract a 128x192 - # version from a PCD file. - #

- # Note that this method modifies the Image object in place. If - # the image has already been loaded, this method has no effect. - # - # @param mode The requested mode. - # @param size The requested size. - - def draft(self, mode, size): - "Configure image decoder" - - pass - - def _expand(self, xmargin, ymargin=None): - if ymargin is None: - ymargin = xmargin - self.load() - return self._new(self.im.expand(xmargin, ymargin, 0)) - - ## - # Filters this image using the given filter. For a list of - # available filters, see the ImageFilter module. - # - # @param filter Filter kernel. - # @return An Image object. - # @see ImageFilter - - def filter(self, filter): - "Apply environment filter to image" - - self.load() - - from ImageFilter import Filter - if not isinstance(filter, Filter): - filter = filter() - - if self.im.bands == 1: - return self._new(filter.filter(self.im)) - # fix to handle multiband images since _imaging doesn't - ims = [] - for c in range(self.im.bands): - ims.append(self._new(filter.filter(self.im.getband(c)))) - return merge(self.mode, ims) - - ## - # Returns a tuple containing the name of each band in this image. - # For example, getbands on an RGB image returns ("R", "G", "B"). - # - # @return A tuple containing band names. - - def getbands(self): - "Get band names" - - return ImageMode.getmode(self.mode).bands - - ## - # Calculates the bounding box of the non-zero regions in the - # image. - # - # @return The bounding box is returned as a 4-tuple defining the - # left, upper, right, and lower pixel coordinate. If the image - # is completely empty, this method returns None. - - def getbbox(self): - "Get bounding box of actual data (non-zero pixels) in image" - - self.load() - return self.im.getbbox() - - ## - # Returns a list of colors used in this image. - # - # @param maxcolors Maximum number of colors. If this number is - # exceeded, this method returns None. The default limit is - # 256 colors. - # @return An unsorted list of (count, pixel) values. - - def getcolors(self, maxcolors=256): - "Get colors from image, up to given limit" - - self.load() - if self.mode in ("1", "L", "P"): - h = self.im.histogram() - out = [] - for i in range(256): - if h[i]: - out.append((h[i], i)) - if len(out) > maxcolors: - return None - return out - return self.im.getcolors(maxcolors) - - ## - # Returns the contents of this image as a sequence object - # containing pixel values. The sequence object is flattened, so - # that values for line one follow directly after the values of - # line zero, and so on. - #

- # Note that the sequence object returned by this method is an - # internal PIL data type, which only supports certain sequence - # operations. To convert it to an ordinary sequence (e.g. for - # printing), use list(im.getdata()). - # - # @param band What band to return. The default is to return - # all bands. To return a single band, pass in the index - # value (e.g. 0 to get the "R" band from an "RGB" image). - # @return A sequence-like object. - - def getdata(self, band = None): - "Get image data as sequence object." - - self.load() - if band is not None: - return self.im.getband(band) - return self.im # could be abused - - ## - # Gets the the minimum and maximum pixel values for each band in - # the image. - # - # @return For a single-band image, a 2-tuple containing the - # minimum and maximum pixel value. For a multi-band image, - # a tuple containing one 2-tuple for each band. - - def getextrema(self): - "Get min/max value" - - self.load() - if self.im.bands > 1: - extrema = [] - for i in range(self.im.bands): - extrema.append(self.im.getband(i).getextrema()) - return tuple(extrema) - return self.im.getextrema() - - ## - # Returns a PyCObject that points to the internal image memory. - # - # @return A PyCObject object. - - def getim(self): - "Get PyCObject pointer to internal image memory" - - self.load() - return self.im.ptr - - - ## - # Returns the image palette as a list. - # - # @return A list of color values [r, g, b, ...], or None if the - # image has no palette. - - def getpalette(self): - "Get palette contents." - - self.load() - try: - return map(ord, self.im.getpalette()) - except ValueError: - return None # no palette - - - ## - # Returns the pixel value at a given position. - # - # @param xy The coordinate, given as (x, y). - # @return The pixel value. If the image is a multi-layer image, - # this method returns a tuple. - - def getpixel(self, xy): - "Get pixel value" - - self.load() - return self.im.getpixel(xy) - - ## - # Returns the horizontal and vertical projection. - # - # @return Two sequences, indicating where there are non-zero - # pixels along the X-axis and the Y-axis, respectively. - - def getprojection(self): - "Get projection to x and y axes" - - self.load() - x, y = self.im.getprojection() - return map(ord, x), map(ord, y) - - ## - # Returns a histogram for the image. The histogram is returned as - # a list of pixel counts, one for each pixel value in the source - # image. If the image has more than one band, the histograms for - # all bands are concatenated (for example, the histogram for an - # "RGB" image contains 768 values). - #

- # A bilevel image (mode "1") is treated as a greyscale ("L") image - # by this method. - #

- # If a mask is provided, the method returns a histogram for those - # parts of the image where the mask image is non-zero. The mask - # image must have the same size as the image, and be either a - # bi-level image (mode "1") or a greyscale image ("L"). - # - # @def histogram(mask=None) - # @param mask An optional mask. - # @return A list containing pixel counts. - - def histogram(self, mask=None, extrema=None): - "Take histogram of image" - - self.load() - if mask: - mask.load() - return self.im.histogram((0, 0), mask.im) - if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.histogram(extrema) - return self.im.histogram() - - ## - # (Deprecated) Returns a copy of the image where the data has been - # offset by the given distances. Data wraps around the edges. If - # yoffset is omitted, it is assumed to be equal to xoffset. - #

- # This method is deprecated. New code should use the offset - # function in the ImageChops module. - # - # @param xoffset The horizontal distance. - # @param yoffset The vertical distance. If omitted, both - # distances are set to the same value. - # @return An Image object. - - def offset(self, xoffset, yoffset=None): - "(deprecated) Offset image in horizontal and/or vertical direction" - if warnings: - warnings.warn( - "'offset' is deprecated; use 'ImageChops.offset' instead", - DeprecationWarning, stacklevel=2 - ) - import ImageChops - return ImageChops.offset(self, xoffset, yoffset) - - ## - # Pastes another image into this image. The box argument is either - # a 2-tuple giving the upper left corner, a 4-tuple defining the - # left, upper, right, and lower pixel coordinate, or None (same as - # (0, 0)). If a 4-tuple is given, the size of the pasted image - # must match the size of the region. - #

- # If the modes don't match, the pasted image is converted to the - # mode of this image (see the {@link #Image.convert} method for - # details). - #

- # Instead of an image, the source can be a integer or tuple - # containing pixel values. The method then fills the region - # with the given colour. When creating RGB images, you can - # also use colour strings as supported by the ImageColor module. - #

- # If a mask is given, this method updates only the regions - # indicated by the mask. You can use either "1", "L" or "RGBA" - # images (in the latter case, the alpha band is used as mask). - # Where the mask is 255, the given image is copied as is. Where - # the mask is 0, the current value is preserved. Intermediate - # values can be used for transparency effects. - #

- # Note that if you paste an "RGBA" image, the alpha band is - # ignored. You can work around this by using the same image as - # both source image and mask. - # - # @param im Source image or pixel value (integer or tuple). - # @param box An optional 4-tuple giving the region to paste into. - # If a 2-tuple is used instead, it's treated as the upper left - # corner. If omitted or None, the source is pasted into the - # upper left corner. - #

- # If an image is given as the second argument and there is no - # third, the box defaults to (0, 0), and the second argument - # is interpreted as a mask image. - # @param mask An optional mask image. - # @return An Image object. - - def paste(self, im, box=None, mask=None): - "Paste other image into region" - - if isImageType(box) and mask is None: - # abbreviated paste(im, mask) syntax - mask = box; box = None - - if box is None: - # cover all of self - box = (0, 0) + self.size - - if len(box) == 2: - # lower left corner given; get size from image or mask - if isImageType(im): - size = im.size - elif isImageType(mask): - size = mask.size - else: - # FIXME: use self.size here? - raise ValueError( - "cannot determine region size; use 4-item box" - ) - box = box + (box[0]+size[0], box[1]+size[1]) - - if isStringType(im): - import ImageColor - im = ImageColor.getcolor(im, self.mode) - - elif isImageType(im): - im.load() - if self.mode != im.mode: - if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): - # should use an adapter for this! - im = im.convert(self.mode) - im = im.im - - self.load() - if self.readonly: - self._copy() - - if mask: - mask.load() - self.im.paste(im, box, mask.im) - else: - self.im.paste(im, box) - - ## - # Maps this image through a lookup table or function. - # - # @param lut A lookup table, containing 256 values per band in the - # image. A function can be used instead, it should take a single - # argument. The function is called once for each possible pixel - # value, and the resulting table is applied to all bands of the - # image. - # @param mode Output mode (default is same as input). In the - # current version, this can only be used if the source image - # has mode "L" or "P", and the output has mode "1". - # @return An Image object. - - def point(self, lut, mode=None): - "Map image through lookup table" - - self.load() - - if not isSequenceType(lut): - # if it isn't a list, it should be a function - if self.mode in ("I", "I;16", "F"): - # check if the function can be used with point_transform - scale, offset = _getscaleoffset(lut) - return self._new(self.im.point_transform(scale, offset)) - # for other modes, convert the function to a table - lut = map(lut, range(256)) * self.im.bands - - if self.mode == "F": - # FIXME: _imaging returns a confusing error message for this case - raise ValueError("point operation not supported for this mode") - - return self._new(self.im.point(lut, mode)) - - ## - # Adds or replaces the alpha layer in this image. If the image - # does not have an alpha layer, it's converted to "LA" or "RGBA". - # The new layer must be either "L" or "1". - # - # @param im The new alpha layer. This can either be an "L" or "1" - # image having the same size as this image, or an integer or - # other color value. - - def putalpha(self, alpha): - "Set alpha layer" - - self.load() - if self.readonly: - self._copy() - - if self.mode not in ("LA", "RGBA"): - # attempt to promote self to a matching alpha mode - try: - mode = getmodebase(self.mode) + "A" - try: - self.im.setmode(mode) - except (AttributeError, ValueError): - # do things the hard way - im = self.im.convert(mode) - if im.mode not in ("LA", "RGBA"): - raise ValueError # sanity check - self.im = im - self.mode = self.im.mode - except (KeyError, ValueError): - raise ValueError("illegal image mode") - - if self.mode == "LA": - band = 1 - else: - band = 3 - - if isImageType(alpha): - # alpha layer - if alpha.mode not in ("1", "L"): - raise ValueError("illegal image mode") - alpha.load() - if alpha.mode == "1": - alpha = alpha.convert("L") - else: - # constant alpha - try: - self.im.fillband(band, alpha) - except (AttributeError, ValueError): - # do things the hard way - alpha = new("L", self.size, alpha) - else: - return - - self.im.putband(alpha.im, band) - - ## - # Copies pixel data to this image. This method copies data from a - # sequence object into the image, starting at the upper left - # corner (0, 0), and continuing until either the image or the - # sequence ends. The scale and offset values are used to adjust - # the sequence values: pixel = value*scale + offset. - # - # @param data A sequence object. - # @param scale An optional scale value. The default is 1.0. - # @param offset An optional offset value. The default is 0.0. - - def putdata(self, data, scale=1.0, offset=0.0): - "Put data from a sequence object into an image." - - self.load() - if self.readonly: - self._copy() - - self.im.putdata(data, scale, offset) - - ## - # Attaches a palette to this image. The image must be a "P" or - # "L" image, and the palette sequence must contain 768 integer - # values, where each group of three values represent the red, - # green, and blue values for the corresponding pixel - # index. Instead of an integer sequence, you can use an 8-bit - # string. - # - # @def putpalette(data) - # @param data A palette sequence (either a list or a string). - - def putpalette(self, data, rawmode="RGB"): - "Put palette data into an image." - - self.load() - if self.mode not in ("L", "P"): - raise ValueError("illegal image mode") - if not isStringType(data): - data = string.join(map(chr, data), "") - self.mode = "P" - self.palette = ImagePalette.raw(rawmode, data) - self.palette.mode = "RGB" - self.load() # install new palette - - ## - # Modifies the pixel at the given position. The colour is given as - # a single numerical value for single-band images, and a tuple for - # multi-band images. - #

- # Note that this method is relatively slow. For more extensive - # changes, use {@link #Image.paste} or the ImageDraw module - # instead. - # - # @param xy The pixel coordinate, given as (x, y). - # @param value The pixel value. - # @see #Image.paste - # @see #Image.putdata - # @see ImageDraw - - def putpixel(self, xy, value): - "Set pixel value" - - self.load() - if self.readonly: - self._copy() - - return self.im.putpixel(xy, value) - - ## - # Returns a resized copy of this image. - # - # @def resize(size, filter=NEAREST) - # @param size The requested size in pixels, as a 2-tuple: - # (width, height). - # @param filter An optional resampling filter. This can be - # one of NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), BICUBIC - # (cubic spline interpolation in a 4x4 environment), or - # ANTIALIAS (a high-quality downsampling filter). - # If omitted, or if the image has mode "1" or "P", it is - # set NEAREST. - # @return An Image object. - - def resize(self, size, resample=NEAREST): - "Resize image" - - if resample not in (NEAREST, BILINEAR, BICUBIC, ANTIALIAS): - raise ValueError("unknown resampling filter") - - self.load() - - if self.mode in ("1", "P"): - resample = NEAREST - - if resample == ANTIALIAS: - # requires stretch support (imToolkit & PIL 1.1.3) - try: - im = self.im.stretch(size, resample) - except AttributeError: - raise ValueError("unsupported resampling filter") - else: - im = self.im.resize(size, resample) - - return self._new(im) - - ## - # Returns a rotated copy of this image. This method returns a - # copy of this image, rotated the given number of degrees counter - # clockwise around its centre. - # - # @def rotate(angle, filter=NEAREST) - # @param angle In degrees counter clockwise. - # @param filter An optional resampling filter. This can be - # one of NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), or BICUBIC - # (cubic spline interpolation in a 4x4 environment). - # If omitted, or if the image has mode "1" or "P", it is - # set NEAREST. - # @param expand Optional expansion flag. If true, expands the output - # image to make it large enough to hold the entire rotated image. - # If false or omitted, make the output image the same size as the - # input image. - # @return An Image object. - - def rotate(self, angle, resample=NEAREST, expand=0): - "Rotate image. Angle given as degrees counter-clockwise." - - if expand: - import math - angle = -angle * math.pi / 180 - matrix = [ - math.cos(angle), math.sin(angle), 0.0, - -math.sin(angle), math.cos(angle), 0.0 - ] - def transform(x, y, (a, b, c, d, e, f)=matrix): - return a*x + b*y + c, d*x + e*y + f - - # calculate output size - w, h = self.size - xx = [] - yy = [] - for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - x, y = transform(x, y) - xx.append(x) - yy.append(y) - w = int(math.ceil(max(xx)) - math.floor(min(xx))) - h = int(math.ceil(max(yy)) - math.floor(min(yy))) - - # adjust center - x, y = transform(w / 2.0, h / 2.0) - matrix[2] = self.size[0] / 2.0 - x - matrix[5] = self.size[1] / 2.0 - y - - return self.transform((w, h), AFFINE, matrix) - - if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") - - self.load() - - if self.mode in ("1", "P"): - resample = NEAREST - - return self._new(self.im.rotate(angle, resample)) - - ## - # Saves this image under the given filename. If no format is - # specified, the format to use is determined from the filename - # extension, if possible. - #

- # Keyword options can be used to provide additional instructions - # to the writer. If a writer doesn't recognise an option, it is - # silently ignored. The available options are described later in - # this handbook. - #

- # You can use a file object instead of a filename. In this case, - # you must always specify the format. The file object must - # implement the seek, tell, and write - # methods, and be opened in binary mode. - # - # @def save(file, format=None, **options) - # @param file File name or file object. - # @param format Optional format override. If omitted, the - # format to use is determined from the filename extension. - # If a file object was used instead of a filename, this - # parameter should always be used. - # @param **options Extra parameters to the image writer. - # @return None - # @exception KeyError If the output format could not be determined - # from the file name. Use the format option to solve this. - # @exception IOError If the file could not be written. The file - # may have been created, and may contain partial data. - - def save(self, fp, format=None, **params): - "Save image to file or stream" - - if isStringType(fp): - filename = fp - else: - if hasattr(fp, "name") and isStringType(fp.name): - filename = fp.name - else: - filename = "" - - # may mutate self! - self.load() - - self.encoderinfo = params - self.encoderconfig = () - - preinit() - - ext = string.lower(os.path.splitext(filename)[1]) - - if not format: - try: - format = EXTENSION[ext] - except KeyError: - init() - try: - format = EXTENSION[ext] - except KeyError: - raise KeyError(ext) # unknown extension - - try: - save_handler = SAVE[string.upper(format)] - except KeyError: - init() - save_handler = SAVE[string.upper(format)] # unknown format - - if isStringType(fp): - import __builtin__ - fp = __builtin__.open(fp, "wb") - close = 1 - else: - close = 0 - - try: - save_handler(self, fp, filename) - finally: - # do what we can to clean up - if close: - fp.close() - - ## - # Seeks to the given frame in this sequence file. If you seek - # beyond the end of the sequence, the method raises an - # EOFError exception. When a sequence file is opened, the - # library automatically seeks to frame 0. - #

- # Note that in the current version of the library, most sequence - # formats only allows you to seek to the next frame. - # - # @param frame Frame number, starting at 0. - # @exception EOFError If the call attempts to seek beyond the end - # of the sequence. - # @see #Image.tell - - def seek(self, frame): - "Seek to given frame in sequence file" - - # overridden by file handlers - if frame != 0: - raise EOFError - - ## - # Displays this image. This method is mainly intended for - # debugging purposes. - #

- # On Unix platforms, this method saves the image to a temporary - # PPM file, and calls the xv utility. - #

- # On Windows, it saves the image to a temporary BMP file, and uses - # the standard BMP display utility to show it (usually Paint). - # - # @def show(title=None) - # @param title Optional title to use for the image window, - # where possible. - - def show(self, title=None, command=None): - "Display image (for debug purposes only)" - - _showxv(self, title, command) - - ## - # Split this image into individual bands. This method returns a - # tuple of individual image bands from an image. For example, - # splitting an "RGB" image creates three new images each - # containing a copy of one of the original bands (red, green, - # blue). - # - # @return A tuple containing bands. - - def split(self): - "Split image into bands" - - ims = [] - self.load() - for i in range(self.im.bands): - ims.append(self._new(self.im.getband(i))) - return tuple(ims) - - ## - # Returns the current frame number. - # - # @return Frame number, starting with 0. - # @see #Image.seek - - def tell(self): - "Return current frame number" - - return 0 - - ## - # Make this image into a thumbnail. This method modifies the - # image to contain a thumbnail version of itself, no larger than - # the given size. This method calculates an appropriate thumbnail - # size to preserve the aspect of the image, calls the {@link - # #Image.draft} method to configure the file reader (where - # applicable), and finally resizes the image. - #

- # Note that the bilinear and bicubic filters in the current - # version of PIL are not well-suited for thumbnail generation. - # You should use ANTIALIAS unless speed is much more - # important than quality. - #

- # Also note that this function modifies the Image object in place. - # If you need to use the full resolution image as well, apply this - # method to a {@link #Image.copy} of the original image. - # - # @param size Requested size. - # @param resample Optional resampling filter. This can be one - # of NEAREST, BILINEAR, BICUBIC, or - # ANTIALIAS (best quality). If omitted, it defaults - # to NEAREST (this will be changed to ANTIALIAS in a - # future version). - # @return None - - def thumbnail(self, size, resample=NEAREST): - "Create thumbnail representation (modifies image in place)" - - # FIXME: the default resampling filter will be changed - # to ANTIALIAS in future versions - - # preserve aspect ratio - x, y = self.size - if x > size[0]: y = max(y * size[0] / x, 1); x = size[0] - if y > size[1]: x = max(x * size[1] / y, 1); y = size[1] - size = x, y - - if size == self.size: - return - - self.draft(None, size) - - self.load() - - try: - im = self.resize(size, resample) - except ValueError: - if resample != ANTIALIAS: - raise - im = self.resize(size, NEAREST) # fallback - - self.im = im.im - self.mode = im.mode - self.size = size - - self.readonly = 0 - - # FIXME: the different tranform methods need further explanation - # instead of bloating the method docs, add a separate chapter. - - ## - # Transforms this image. This method creates a new image with the - # given size, and the same mode as the original, and copies data - # to the new image using the given transform. - #

- # @def transform(size, method, data, resample=NEAREST) - # @param size The output size. - # @param method The transformation method. This is one of - # EXTENT (cut out a rectangular subregion), AFFINE - # (affine transform), PERSPECTIVE (perspective - # transform), QUAD (map a quadrilateral to a - # rectangle), or MESH (map a number of source quadrilaterals - # in one operation). - # @param data Extra data to the transformation method. - # @param resample Optional resampling filter. It can be one of - # NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), or - # BICUBIC (cubic spline interpolation in a 4x4 - # environment). If omitted, or if the image has mode - # "1" or "P", it is set to NEAREST. - # @return An Image object. - - def transform(self, size, method, data=None, resample=NEAREST, fill=1): - "Transform image" - - import ImageTransform - if isinstance(method, ImageTransform.Transform): - method, data = method.getdata() - if data is None: - raise ValueError("missing method data") - im = new(self.mode, size, None) - if method == MESH: - # list of quads - for box, quad in data: - im.__transformer(box, self, QUAD, quad, resample, fill) - else: - im.__transformer((0, 0)+size, self, method, data, resample, fill) - - return im - - def __transformer(self, box, image, method, data, - resample=NEAREST, fill=1): - - # FIXME: this should be turned into a lazy operation (?) - - w = box[2]-box[0] - h = box[3]-box[1] - - if method == AFFINE: - # change argument order to match implementation - data = (data[2], data[0], data[1], - data[5], data[3], data[4]) - elif method == EXTENT: - # convert extent to an affine transform - x0, y0, x1, y1 = data - xs = float(x1 - x0) / w - ys = float(y1 - y0) / h - method = AFFINE - data = (x0 + xs/2, xs, 0, y0 + ys/2, 0, ys) - elif method == PERSPECTIVE: - # change argument order to match implementation - data = (data[2], data[0], data[1], - data[5], data[3], data[4], - data[6], data[7]) - elif method == QUAD: - # quadrilateral warp. data specifies the four corners - # given as NW, SW, SE, and NE. - nw = data[0:2]; sw = data[2:4]; se = data[4:6]; ne = data[6:8] - x0, y0 = nw; As = 1.0 / w; At = 1.0 / h - data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, - (se[0]-sw[0]-ne[0]+x0)*As*At, - y0, (ne[1]-y0)*As, (sw[1]-y0)*At, - (se[1]-sw[1]-ne[1]+y0)*As*At) - else: - raise ValueError("unknown transformation method") - - if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") - - image.load() - - self.load() - - if image.mode in ("1", "P"): - resample = NEAREST - - self.im.transform2(box, image.im, method, data, resample, fill) - - ## - # Returns a flipped or rotated copy of this image. - # - # @param method One of FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, - # ROTATE_90, ROTATE_180, or ROTATE_270. - - def transpose(self, method): - "Transpose image (flip or rotate in 90 degree steps)" - - self.load() - im = self.im.transpose(method) - return self._new(im) - -# -------------------------------------------------------------------- -# Lazy operations - -class _ImageCrop(Image): - - def __init__(self, im, box): - - Image.__init__(self) - - x0, y0, x1, y1 = box - if x1 < x0: - x1 = x0 - if y1 < y0: - y1 = y0 - - self.mode = im.mode - self.size = x1-x0, y1-y0 - - self.__crop = x0, y0, x1, y1 - - self.im = im.im - - def load(self): - - # lazy evaluation! - if self.__crop: - self.im = self.im.crop(self.__crop) - self.__crop = None - - # FIXME: future versions should optimize crop/paste - # sequences! - -# -------------------------------------------------------------------- -# Factories - -# -# Debugging - -def _wedge(): - "Create greyscale wedge (for debugging only)" - - return Image()._new(core.wedge("L")) - -## -# Creates a new image with the given mode and size. -# -# @param mode The mode to use for the new image. -# @param size A 2-tuple, containing (width, height) in pixels. -# @param color What colour to use for the image. Default is black. -# If given, this should be a single integer or floating point value -# for single-band modes, and a tuple for multi-band modes (one value -# per band). When creating RGB images, you can also use colour -# strings as supported by the ImageColor module. If the colour is -# None, the image is not initialised. -# @return An Image object. - -def new(mode, size, color=0): - "Create a new image" - - if color is None: - # don't initialize - return Image()._new(core.new(mode, size)) - - if isStringType(color): - # css3-style specifier - - import ImageColor - color = ImageColor.getcolor(color, mode) - - return Image()._new(core.fill(mode, size, color)) - -## -# Creates an image memory from pixel data in a string. -#

-# In its simplest form, this function takes three arguments -# (mode, size, and unpacked pixel data). -#

-# Note that this function decodes pixel data only, not entire images. -# If you have an entire image in a string, wrap it in a -# StringIO object, and use {@link #open} to load it. -# -# @param mode The image mode. -# @param size The image size. -# @param data An 8-bit string containing raw data for the given mode. -# @param decoder_name What decoder to use. -# @param *args Additional parameters for the given decoder. -# @return An Image object. - -def fromstring(mode, size, data, decoder_name="raw", *args): - "Load image from string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if decoder_name == "raw" and args == (): - args = mode - - im = new(mode, size) - im.fromstring(data, decoder_name, args) - return im - -## -# (New in 1.1.4) Creates an image memory from pixel data in a string -# or byte buffer. -#

-# This function is similar to {@link #fromstring}, but uses data in -# the byte buffer, where possible. This means that changes to the -# original buffer object are reflected in this image). Not all modes -# can share memory; supported modes include "L", "RGBX", "RGBA", and -# "CMYK". -#

-# Note that this function decodes pixel data only, not entire images. -# If you have an entire image file in a string, wrap it in a -# StringIO object, and use {@link #open} to load it. -#

-# In the current version, the default parameters used for the "raw" -# decoder differs from that used for {@link fromstring}. This is a -# bug, and will probably be fixed in a future release. The current -# release issues a warning if you do this; to disable the warning, -# you should provide the full set of parameters. See below for -# details. -# -# @param mode The image mode. -# @param size The image size. -# @param data An 8-bit string or other buffer object containing raw -# data for the given mode. -# @param decoder_name What decoder to use. -# @param *args Additional parameters for the given decoder. For the -# default encoder ("raw"), it's recommended that you provide the -# full set of parameters: -# frombuffer(mode, size, data, "raw", mode, 0, 1). -# @return An Image object. -# @since 1.1.4 - -def frombuffer(mode, size, data, decoder_name="raw", *args): - "Load image from string or buffer" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if decoder_name == "raw": - if args == (): - if warnings: - warnings.warn( - "the frombuffer defaults may change in a future release; " - "for portability, change the call to read:\n" - " frombuffer(mode, size, data, 'raw', mode, 0, 1)", - RuntimeWarning, stacklevel=2 - ) - args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 - if args[0] in _MAPMODES: - im = new(mode, (1,1)) - im = im._new( - core.map_buffer(data, size, decoder_name, None, 0, args) - ) - im.readonly = 1 - return im - - return apply(fromstring, (mode, size, data, decoder_name, args)) - - -## -# (New in 1.1.6) Create an image memory from an object exporting -# the array interface (using the buffer protocol). -# -# If obj is not contiguous, then the tostring method is called -# and {@link frombuffer} is used. -# -# @param obj Object with array interface -# @param mode Mode to use (will be determined from type if None) -# @return An image memory. - -def fromarray(obj, mode=None): - arr = obj.__array_interface__ - shape = arr['shape'] - ndim = len(shape) - try: - strides = arr['strides'] - except KeyError: - strides = None - if mode is None: - typestr = arr['typestr'] - if not (typestr[0] == '|' or typestr[0] == _ENDIAN or - typestr[1:] not in ['u1', 'b1', 'i4', 'f4']): - raise TypeError("cannot handle data-type") - if typestr[0] == _ENDIAN: - typestr = typestr[1:3] - else: - typestr = typestr[:2] - if typestr == 'i4': - mode = 'I' - if typestr == 'u2': - mode = 'I;16' - elif typestr == 'f4': - mode = 'F' - elif typestr == 'b1': - mode = '1' - elif ndim == 2: - mode = 'L' - elif ndim == 3: - mode = 'RGB' - elif ndim == 4: - mode = 'RGBA' - else: - raise TypeError("Do not understand data.") - ndmax = 4 - bad_dims=0 - if mode in ['1','L','I','P','F']: - ndmax = 2 - elif mode == 'RGB': - ndmax = 3 - if ndim > ndmax: - raise ValueError("Too many dimensions.") - - size = shape[:2][::-1] - if strides is not None: - obj = obj.tostring() - - return frombuffer(mode, size, obj, "raw", mode, 0, 1) - -## -# Opens and identifies the given image file. -#

-# This is a lazy operation; this function identifies the file, but the -# actual image data is not read from the file until you try to process -# the data (or call the {@link #Image.load} method). -# -# @def open(file, mode="r") -# @param file A filename (string) or a file object. The file object -# must implement read, seek, and tell methods, -# and be opened in binary mode. -# @param mode The mode. If given, this argument must be "r". -# @return An Image object. -# @exception IOError If the file cannot be found, or the image cannot be -# opened and identified. -# @see #new - -def open(fp, mode="r"): - "Open an image file, without loading the raster data" - - if mode != "r": - raise ValueError("bad mode") - - if isStringType(fp): - import __builtin__ - filename = fp - fp = __builtin__.open(fp, "rb") - else: - filename = "" - - prefix = fp.read(16) - - preinit() - - for i in ID: - try: - factory, accept = OPEN[i] - if not accept or accept(prefix): - fp.seek(0) - return factory(fp, filename) - except (SyntaxError, IndexError, TypeError): - pass - - init() - - for i in ID: - try: - factory, accept = OPEN[i] - if not accept or accept(prefix): - fp.seek(0) - return factory(fp, filename) - except (SyntaxError, IndexError, TypeError): - pass - - raise IOError("cannot identify image file") - -# -# Image processing. - -## -# Creates a new image by interpolating between two input images, using -# a constant alpha. -# -#

-#    out = image1 * (1.0 - alpha) + image2 * alpha
-# 
-# -# @param im1 The first image. -# @param im2 The second image. Must have the same mode and size as -# the first image. -# @param alpha The interpolation alpha factor. If alpha is 0.0, a -# copy of the first image is returned. If alpha is 1.0, a copy of -# the second image is returned. There are no restrictions on the -# alpha value. If necessary, the result is clipped to fit into -# the allowed output range. -# @return An Image object. - -def blend(im1, im2, alpha): - "Interpolate between images." - - im1.load() - im2.load() - return im1._new(core.blend(im1.im, im2.im, alpha)) - -## -# Creates a new image by interpolating between two input images, -# using the mask as alpha. -# -# @param image1 The first image. -# @param image2 The second image. Must have the same mode and -# size as the first image. -# @param mask A mask image. This image can can have mode -# "1", "L", or "RGBA", and must have the same size as the -# other two images. - -def composite(image1, image2, mask): - "Create composite image by blending images using a transparency mask" - - image = image2.copy() - image.paste(image1, None, mask) - return image - -## -# Applies the function (which should take one argument) to each pixel -# in the given image. If the image has more than one band, the same -# function is applied to each band. Note that the function is -# evaluated once for each possible pixel value, so you cannot use -# random components or other generators. -# -# @def eval(image, function) -# @param image The input image. -# @param function A function object, taking one integer argument. -# @return An Image object. - -def eval(image, *args): - "Evaluate image expression" - - return image.point(args[0]) - -## -# Creates a new image from a number of single-band images. -# -# @param mode The mode to use for the output image. -# @param bands A sequence containing one single-band image for -# each band in the output image. All bands must have the -# same size. -# @return An Image object. - -def merge(mode, bands): - "Merge a set of single band images into a new multiband image." - - if getmodebands(mode) != len(bands) or "*" in mode: - raise ValueError("wrong number of bands") - for im in bands[1:]: - if im.mode != getmodetype(mode): - raise ValueError("mode mismatch") - if im.size != bands[0].size: - raise ValueError("size mismatch") - im = core.new(mode, bands[0].size) - for i in range(getmodebands(mode)): - bands[i].load() - im.putband(bands[i].im, i) - return bands[0]._new(im) - -# -------------------------------------------------------------------- -# Plugin registry - -## -# Register an image file plugin. This function should not be used -# in application code. -# -# @param id An image format identifier. -# @param factory An image file factory method. -# @param accept An optional function that can be used to quickly -# reject images having another format. - -def register_open(id, factory, accept=None): - id = string.upper(id) - ID.append(id) - OPEN[id] = factory, accept - -## -# Registers an image MIME type. This function should not be used -# in application code. -# -# @param id An image format identifier. -# @param mimetype The image MIME type for this format. - -def register_mime(id, mimetype): - MIME[string.upper(id)] = mimetype - -## -# Registers an image save function. This function should not be -# used in application code. -# -# @param id An image format identifier. -# @param driver A function to save images in this format. - -def register_save(id, driver): - SAVE[string.upper(id)] = driver - -## -# Registers an image extension. This function should not be -# used in application code. -# -# @param id An image format identifier. -# @param extension An extension used for this format. - -def register_extension(id, extension): - EXTENSION[string.lower(extension)] = string.upper(id) - - -# -------------------------------------------------------------------- -# Simple display support - -def _showxv(image, title=None, command=None): - - if os.name == "nt": - format = "BMP" - elif sys.platform == "darwin": - format = "JPEG" - if not command: - command = "open -a /Applications/Preview.app" - else: - format = None - if not command: - command = "xv" - if title: - command = command + " -name \"%s\"" % title - - if image.mode == "I;16": - # @PIL88 @PIL101 - # "I;16" isn't an 'official' mode, but we still want to - # provide a simple way to show 16-bit images. - base = "L" - else: - base = getmodebase(image.mode) - if base != image.mode and image.mode != "1": - file = image.convert(base)._dump(format=format) - else: - file = image._dump(format=format) - - if os.name == "nt": - command = "start /wait %s && del /f %s" % (file, file) - elif sys.platform == "darwin": - # on darwin open returns immediately resulting in the temp - # file removal while app is opening - command = "(%s %s; sleep 20; rm -f %s)&" % (command, file, file) - else: - command = "(%s %s; rm -f %s)&" % (command, file, file) - - os.system(command) diff --git a/pyqtgraph/PIL_Fix/Image.py-1.7 b/pyqtgraph/PIL_Fix/Image.py-1.7 deleted file mode 100644 index cacbcc64..00000000 --- a/pyqtgraph/PIL_Fix/Image.py-1.7 +++ /dev/null @@ -1,2129 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# the Image class wrapper -# -# partial release history: -# 1995-09-09 fl Created -# 1996-03-11 fl PIL release 0.0 (proof of concept) -# 1996-04-30 fl PIL release 0.1b1 -# 1999-07-28 fl PIL release 1.0 final -# 2000-06-07 fl PIL release 1.1 -# 2000-10-20 fl PIL release 1.1.1 -# 2001-05-07 fl PIL release 1.1.2 -# 2002-03-15 fl PIL release 1.1.3 -# 2003-05-10 fl PIL release 1.1.4 -# 2005-03-28 fl PIL release 1.1.5 -# 2006-12-02 fl PIL release 1.1.6 -# 2009-11-15 fl PIL release 1.1.7 -# -# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-2009 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -VERSION = "1.1.7" - -try: - import warnings -except ImportError: - warnings = None - -class _imaging_not_installed: - # module placeholder - def __getattr__(self, id): - raise ImportError("The _imaging C module is not installed") - -try: - # give Tk a chance to set up the environment, in case we're - # using an _imaging module linked against libtcl/libtk (use - # __import__ to hide this from naive packagers; we don't really - # depend on Tk unless ImageTk is used, and that module already - # imports Tkinter) - __import__("FixTk") -except ImportError: - pass - -try: - # If the _imaging C module is not present, you can still use - # the "open" function to identify files, but you cannot load - # them. Note that other modules should not refer to _imaging - # directly; import Image and use the Image.core variable instead. - import _imaging - core = _imaging - del _imaging -except ImportError, v: - core = _imaging_not_installed() - if str(v)[:20] == "Module use of python" and warnings: - # The _imaging C module is present, but not compiled for - # the right version (windows only). Print a warning, if - # possible. - warnings.warn( - "The _imaging extension was built for another version " - "of Python; most PIL functions will be disabled", - RuntimeWarning - ) - -import ImageMode -import ImagePalette - -import os, string, sys - -# type stuff -from types import IntType, StringType, TupleType - -try: - UnicodeStringType = type(unicode("")) - ## - # (Internal) Checks if an object is a string. If the current - # Python version supports Unicode, this checks for both 8-bit - # and Unicode strings. - def isStringType(t): - return isinstance(t, StringType) or isinstance(t, UnicodeStringType) -except NameError: - def isStringType(t): - return isinstance(t, StringType) - -## -# (Internal) Checks if an object is a tuple. - -def isTupleType(t): - return isinstance(t, TupleType) - -## -# (Internal) Checks if an object is an image object. - -def isImageType(t): - return hasattr(t, "im") - -## -# (Internal) Checks if an object is a string, and that it points to a -# directory. - -def isDirectory(f): - return isStringType(f) and os.path.isdir(f) - -from operator import isNumberType, isSequenceType - -# -# Debug level - -DEBUG = 0 - -# -# Constants (also defined in _imagingmodule.c!) - -NONE = 0 - -# transpose -FLIP_LEFT_RIGHT = 0 -FLIP_TOP_BOTTOM = 1 -ROTATE_90 = 2 -ROTATE_180 = 3 -ROTATE_270 = 4 - -# transforms -AFFINE = 0 -EXTENT = 1 -PERSPECTIVE = 2 -QUAD = 3 -MESH = 4 - -# resampling filters -NONE = 0 -NEAREST = 0 -ANTIALIAS = 1 # 3-lobed lanczos -LINEAR = BILINEAR = 2 -CUBIC = BICUBIC = 3 - -# dithers -NONE = 0 -NEAREST = 0 -ORDERED = 1 # Not yet implemented -RASTERIZE = 2 # Not yet implemented -FLOYDSTEINBERG = 3 # default - -# palettes/quantizers -WEB = 0 -ADAPTIVE = 1 - -# categories -NORMAL = 0 -SEQUENCE = 1 -CONTAINER = 2 - -# -------------------------------------------------------------------- -# Registries - -ID = [] -OPEN = {} -MIME = {} -SAVE = {} -EXTENSION = {} - -# -------------------------------------------------------------------- -# Modes supported by this version - -_MODEINFO = { - # NOTE: this table will be removed in future versions. use - # getmode* functions or ImageMode descriptors instead. - - # official modes - "1": ("L", "L", ("1",)), - "L": ("L", "L", ("L",)), - "I": ("L", "I", ("I",)), - "F": ("L", "F", ("F",)), - "P": ("RGB", "L", ("P",)), - "RGB": ("RGB", "L", ("R", "G", "B")), - "RGBX": ("RGB", "L", ("R", "G", "B", "X")), - "RGBA": ("RGB", "L", ("R", "G", "B", "A")), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), - - # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and - # BGR;24. Use these modes only if you know exactly what you're - # doing... - -} - -try: - byteorder = sys.byteorder -except AttributeError: - import struct - if struct.unpack("h", "\0\1")[0] == 1: - byteorder = "big" - else: - byteorder = "little" - -if byteorder == 'little': - _ENDIAN = '<' -else: - _ENDIAN = '>' - -_MODE_CONV = { - # official modes - "1": ('|b1', None), # broken - "L": ('|u1', None), - "I": (_ENDIAN + 'i4', None), - "I;16": ('%su2' % _ENDIAN, None), - "F": (_ENDIAN + 'f4', None), - "P": ('|u1', None), - "RGB": ('|u1', 3), - "RGBX": ('|u1', 4), - "RGBA": ('|u1', 4), - "CMYK": ('|u1', 4), - "YCbCr": ('|u1', 4), -} - -def _conv_type_shape(im): - shape = im.size[1], im.size[0] - typ, extra = _MODE_CONV[im.mode] - if extra is None: - return shape, typ - else: - return shape+(extra,), typ - - -MODES = _MODEINFO.keys() -MODES.sort() - -# raw modes that may be memory mapped. NOTE: if you change this, you -# may have to modify the stride calculation in map.c too! -_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") - -## -# Gets the "base" mode for given mode. This function returns "L" for -# images that contain grayscale data, and "RGB" for images that -# contain color data. -# -# @param mode Input mode. -# @return "L" or "RGB". -# @exception KeyError If the input mode was not a standard mode. - -def getmodebase(mode): - return ImageMode.getmode(mode).basemode - -## -# Gets the storage type mode. Given a mode, this function returns a -# single-layer mode suitable for storing individual bands. -# -# @param mode Input mode. -# @return "L", "I", or "F". -# @exception KeyError If the input mode was not a standard mode. - -def getmodetype(mode): - return ImageMode.getmode(mode).basetype - -## -# Gets a list of individual band names. Given a mode, this function -# returns a tuple containing the names of individual bands (use -# {@link #getmodetype} to get the mode used to store each individual -# band. -# -# @param mode Input mode. -# @return A tuple containing band names. The length of the tuple -# gives the number of bands in an image of the given mode. -# @exception KeyError If the input mode was not a standard mode. - -def getmodebandnames(mode): - return ImageMode.getmode(mode).bands - -## -# Gets the number of individual bands for this mode. -# -# @param mode Input mode. -# @return The number of bands in this mode. -# @exception KeyError If the input mode was not a standard mode. - -def getmodebands(mode): - return len(ImageMode.getmode(mode).bands) - -# -------------------------------------------------------------------- -# Helpers - -_initialized = 0 - -## -# Explicitly loads standard file format drivers. - -def preinit(): - "Load standard file format drivers." - - global _initialized - if _initialized >= 1: - return - - try: - import BmpImagePlugin - except ImportError: - pass - try: - import GifImagePlugin - except ImportError: - pass - try: - import JpegImagePlugin - except ImportError: - pass - try: - import PpmImagePlugin - except ImportError: - pass - try: - import PngImagePlugin - except ImportError: - pass -# try: -# import TiffImagePlugin -# except ImportError: -# pass - - _initialized = 1 - -## -# Explicitly initializes the Python Imaging Library. This function -# loads all available file format drivers. - -def init(): - "Load all file format drivers." - - global _initialized - if _initialized >= 2: - return 0 - - visited = {} - - directories = sys.path - - try: - directories = directories + [os.path.dirname(__file__)] - except NameError: - pass - - # only check directories (including current, if present in the path) - for directory in filter(isDirectory, directories): - fullpath = os.path.abspath(directory) - if visited.has_key(fullpath): - continue - for file in os.listdir(directory): - if file[-14:] == "ImagePlugin.py": - f, e = os.path.splitext(file) - try: - sys.path.insert(0, directory) - try: - __import__(f, globals(), locals(), []) - finally: - del sys.path[0] - except ImportError: - if DEBUG: - print "Image: failed to import", - print f, ":", sys.exc_value - visited[fullpath] = None - - if OPEN or SAVE: - _initialized = 2 - return 1 - -# -------------------------------------------------------------------- -# Codec factories (used by tostring/fromstring and ImageFile.load) - -def _getdecoder(mode, decoder_name, args, extra=()): - - # tweak arguments - if args is None: - args = () - elif not isTupleType(args): - args = (args,) - - try: - # get decoder - decoder = getattr(core, decoder_name + "_decoder") - # print decoder, (mode,) + args + extra - return apply(decoder, (mode,) + args + extra) - except AttributeError: - raise IOError("decoder %s not available" % decoder_name) - -def _getencoder(mode, encoder_name, args, extra=()): - - # tweak arguments - if args is None: - args = () - elif not isTupleType(args): - args = (args,) - - try: - # get encoder - encoder = getattr(core, encoder_name + "_encoder") - # print encoder, (mode,) + args + extra - return apply(encoder, (mode,) + args + extra) - except AttributeError: - raise IOError("encoder %s not available" % encoder_name) - - -# -------------------------------------------------------------------- -# Simple expression analyzer - -class _E: - def __init__(self, data): self.data = data - def __coerce__(self, other): return self, _E(other) - def __add__(self, other): return _E((self.data, "__add__", other.data)) - def __mul__(self, other): return _E((self.data, "__mul__", other.data)) - -def _getscaleoffset(expr): - stub = ["stub"] - data = expr(_E(stub)).data - try: - (a, b, c) = data # simplified syntax - if (a is stub and b == "__mul__" and isNumberType(c)): - return c, 0.0 - if (a is stub and b == "__add__" and isNumberType(c)): - return 1.0, c - except TypeError: pass - try: - ((a, b, c), d, e) = data # full syntax - if (a is stub and b == "__mul__" and isNumberType(c) and - d == "__add__" and isNumberType(e)): - return c, e - except TypeError: pass - raise ValueError("illegal expression") - - -# -------------------------------------------------------------------- -# Implementation wrapper - -## -# This class represents an image object. To create Image objects, use -# the appropriate factory functions. There's hardly ever any reason -# to call the Image constructor directly. -# -# @see #open -# @see #new -# @see #fromstring - -class Image: - - format = None - format_description = None - - def __init__(self): - # FIXME: take "new" parameters / other image? - # FIXME: turn mode and size into delegating properties? - self.im = None - self.mode = "" - self.size = (0, 0) - self.palette = None - self.info = {} - self.category = NORMAL - self.readonly = 0 - - def _new(self, im): - new = Image() - new.im = im - new.mode = im.mode - new.size = im.size - new.palette = self.palette - if im.mode == "P": - new.palette = ImagePalette.ImagePalette() - try: - new.info = self.info.copy() - except AttributeError: - # fallback (pre-1.5.2) - new.info = {} - for k, v in self.info: - new.info[k] = v - return new - - _makeself = _new # compatibility - - def _copy(self): - self.load() - self.im = self.im.copy() - self.readonly = 0 - - def _dump(self, file=None, format=None): - import tempfile - if not file: - file = tempfile.mktemp() - self.load() - if not format or format == "PPM": - self.im.save_ppm(file) - else: - file = file + "." + format - self.save(file, format) - return file - - def __repr__(self): - return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( - self.__class__.__module__, self.__class__.__name__, - self.mode, self.size[0], self.size[1], - id(self) - ) - - def __getattr__(self, name): - if name == "__array_interface__": - # numpy array interface support - new = {} - shape, typestr = _conv_type_shape(self) - new['shape'] = shape - new['typestr'] = typestr - new['data'] = self.tostring() - return new - raise AttributeError(name) - - ## - # Returns a string containing pixel data. - # - # @param encoder_name What encoder to use. The default is to - # use the standard "raw" encoder. - # @param *args Extra arguments to the encoder. - # @return An 8-bit string. - - def tostring(self, encoder_name="raw", *args): - "Return image as a binary string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if encoder_name == "raw" and args == (): - args = self.mode - - self.load() - - # unpack data - e = _getencoder(self.mode, encoder_name, args) - e.setimage(self.im) - - bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - - data = [] - while 1: - l, s, d = e.encode(bufsize) - data.append(d) - if s: - break - if s < 0: - raise RuntimeError("encoder error %d in tostring" % s) - - return string.join(data, "") - - ## - # Returns the image converted to an X11 bitmap. This method - # only works for mode "1" images. - # - # @param name The name prefix to use for the bitmap variables. - # @return A string containing an X11 bitmap. - # @exception ValueError If the mode is not "1" - - def tobitmap(self, name="image"): - "Return image as an XBM bitmap" - - self.load() - if self.mode != "1": - raise ValueError("not a bitmap") - data = self.tostring("xbm") - return string.join(["#define %s_width %d\n" % (name, self.size[0]), - "#define %s_height %d\n"% (name, self.size[1]), - "static char %s_bits[] = {\n" % name, data, "};"], "") - - ## - # Loads this image with pixel data from a string. - #

- # This method is similar to the {@link #fromstring} function, but - # loads data into this image instead of creating a new image - # object. - - def fromstring(self, data, decoder_name="raw", *args): - "Load data to image from binary string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - # default format - if decoder_name == "raw" and args == (): - args = self.mode - - # unpack data - d = _getdecoder(self.mode, decoder_name, args) - d.setimage(self.im) - s = d.decode(data) - - if s[0] >= 0: - raise ValueError("not enough image data") - if s[1] != 0: - raise ValueError("cannot decode image data") - - ## - # Allocates storage for the image and loads the pixel data. In - # normal cases, you don't need to call this method, since the - # Image class automatically loads an opened image when it is - # accessed for the first time. - # - # @return An image access object. - - def load(self): - "Explicitly load pixel data." - if self.im and self.palette and self.palette.dirty: - # realize palette - apply(self.im.putpalette, self.palette.getdata()) - self.palette.dirty = 0 - self.palette.mode = "RGB" - self.palette.rawmode = None - if self.info.has_key("transparency"): - self.im.putpalettealpha(self.info["transparency"], 0) - self.palette.mode = "RGBA" - if self.im: - return self.im.pixel_access(self.readonly) - - ## - # Verifies the contents of a file. For data read from a file, this - # method attempts to determine if the file is broken, without - # actually decoding the image data. If this method finds any - # problems, it raises suitable exceptions. If you need to load - # the image after using this method, you must reopen the image - # file. - - def verify(self): - "Verify file contents." - pass - - ## - # Returns a converted copy of this image. For the "P" mode, this - # method translates pixels through the palette. If mode is - # omitted, a mode is chosen so that all information in the image - # and the palette can be represented without a palette. - #

- # The current version supports all possible conversions between - # "L", "RGB" and "CMYK." - #

- # When translating a colour image to black and white (mode "L"), - # the library uses the ITU-R 601-2 luma transform: - #

- # L = R * 299/1000 + G * 587/1000 + B * 114/1000 - #

- # When translating a greyscale image into a bilevel image (mode - # "1"), all non-zero values are set to 255 (white). To use other - # thresholds, use the {@link #Image.point} method. - # - # @def convert(mode, matrix=None, **options) - # @param mode The requested mode. - # @param matrix An optional conversion matrix. If given, this - # should be 4- or 16-tuple containing floating point values. - # @param options Additional options, given as keyword arguments. - # @keyparam dither Dithering method, used when converting from - # mode "RGB" to "P". - # Available methods are NONE or FLOYDSTEINBERG (default). - # @keyparam palette Palette to use when converting from mode "RGB" - # to "P". Available palettes are WEB or ADAPTIVE. - # @keyparam colors Number of colors to use for the ADAPTIVE palette. - # Defaults to 256. - # @return An Image object. - - def convert(self, mode=None, data=None, dither=None, - palette=WEB, colors=256): - "Convert to other pixel format" - - if not mode: - # determine default mode - if self.mode == "P": - self.load() - if self.palette: - mode = self.palette.mode - else: - mode = "RGB" - else: - return self.copy() - - self.load() - - if data: - # matrix conversion - if mode not in ("L", "RGB"): - raise ValueError("illegal conversion") - im = self.im.convert_matrix(mode, data) - return self._new(im) - - if mode == "P" and palette == ADAPTIVE: - im = self.im.quantize(colors) - return self._new(im) - - # colourspace conversion - if dither is None: - dither = FLOYDSTEINBERG - - try: - im = self.im.convert(mode, dither) - except ValueError: - try: - # normalize source image and try again - im = self.im.convert(getmodebase(self.mode)) - im = im.convert(mode, dither) - except KeyError: - raise ValueError("illegal conversion") - - return self._new(im) - - def quantize(self, colors=256, method=0, kmeans=0, palette=None): - - # methods: - # 0 = median cut - # 1 = maximum coverage - - # NOTE: this functionality will be moved to the extended - # quantizer interface in a later version of PIL. - - self.load() - - if palette: - # use palette from reference image - palette.load() - if palette.mode != "P": - raise ValueError("bad mode for palette image") - if self.mode != "RGB" and self.mode != "L": - raise ValueError( - "only RGB or L mode images can be quantized to a palette" - ) - im = self.im.convert("P", 1, palette.im) - return self._makeself(im) - - im = self.im.quantize(colors, method, kmeans) - return self._new(im) - - ## - # Copies this image. Use this method if you wish to paste things - # into an image, but still retain the original. - # - # @return An Image object. - - def copy(self): - "Copy raster data" - - self.load() - im = self.im.copy() - return self._new(im) - - ## - # Returns a rectangular region from this image. The box is a - # 4-tuple defining the left, upper, right, and lower pixel - # coordinate. - #

- # This is a lazy operation. Changes to the source image may or - # may not be reflected in the cropped image. To break the - # connection, call the {@link #Image.load} method on the cropped - # copy. - # - # @param The crop rectangle, as a (left, upper, right, lower)-tuple. - # @return An Image object. - - def crop(self, box=None): - "Crop region from image" - - self.load() - if box is None: - return self.copy() - - # lazy operation - return _ImageCrop(self, box) - - ## - # Configures the image file loader so it returns a version of the - # image that as closely as possible matches the given mode and - # size. For example, you can use this method to convert a colour - # JPEG to greyscale while loading it, or to extract a 128x192 - # version from a PCD file. - #

- # Note that this method modifies the Image object in place. If - # the image has already been loaded, this method has no effect. - # - # @param mode The requested mode. - # @param size The requested size. - - def draft(self, mode, size): - "Configure image decoder" - - pass - - def _expand(self, xmargin, ymargin=None): - if ymargin is None: - ymargin = xmargin - self.load() - return self._new(self.im.expand(xmargin, ymargin, 0)) - - ## - # Filters this image using the given filter. For a list of - # available filters, see the ImageFilter module. - # - # @param filter Filter kernel. - # @return An Image object. - # @see ImageFilter - - def filter(self, filter): - "Apply environment filter to image" - - self.load() - - if callable(filter): - filter = filter() - if not hasattr(filter, "filter"): - raise TypeError("filter argument should be ImageFilter.Filter instance or class") - - if self.im.bands == 1: - return self._new(filter.filter(self.im)) - # fix to handle multiband images since _imaging doesn't - ims = [] - for c in range(self.im.bands): - ims.append(self._new(filter.filter(self.im.getband(c)))) - return merge(self.mode, ims) - - ## - # Returns a tuple containing the name of each band in this image. - # For example, getbands on an RGB image returns ("R", "G", "B"). - # - # @return A tuple containing band names. - - def getbands(self): - "Get band names" - - return ImageMode.getmode(self.mode).bands - - ## - # Calculates the bounding box of the non-zero regions in the - # image. - # - # @return The bounding box is returned as a 4-tuple defining the - # left, upper, right, and lower pixel coordinate. If the image - # is completely empty, this method returns None. - - def getbbox(self): - "Get bounding box of actual data (non-zero pixels) in image" - - self.load() - return self.im.getbbox() - - ## - # Returns a list of colors used in this image. - # - # @param maxcolors Maximum number of colors. If this number is - # exceeded, this method returns None. The default limit is - # 256 colors. - # @return An unsorted list of (count, pixel) values. - - def getcolors(self, maxcolors=256): - "Get colors from image, up to given limit" - - self.load() - if self.mode in ("1", "L", "P"): - h = self.im.histogram() - out = [] - for i in range(256): - if h[i]: - out.append((h[i], i)) - if len(out) > maxcolors: - return None - return out - return self.im.getcolors(maxcolors) - - ## - # Returns the contents of this image as a sequence object - # containing pixel values. The sequence object is flattened, so - # that values for line one follow directly after the values of - # line zero, and so on. - #

- # Note that the sequence object returned by this method is an - # internal PIL data type, which only supports certain sequence - # operations. To convert it to an ordinary sequence (e.g. for - # printing), use list(im.getdata()). - # - # @param band What band to return. The default is to return - # all bands. To return a single band, pass in the index - # value (e.g. 0 to get the "R" band from an "RGB" image). - # @return A sequence-like object. - - def getdata(self, band = None): - "Get image data as sequence object." - - self.load() - if band is not None: - return self.im.getband(band) - return self.im # could be abused - - ## - # Gets the the minimum and maximum pixel values for each band in - # the image. - # - # @return For a single-band image, a 2-tuple containing the - # minimum and maximum pixel value. For a multi-band image, - # a tuple containing one 2-tuple for each band. - - def getextrema(self): - "Get min/max value" - - self.load() - if self.im.bands > 1: - extrema = [] - for i in range(self.im.bands): - extrema.append(self.im.getband(i).getextrema()) - return tuple(extrema) - return self.im.getextrema() - - ## - # Returns a PyCObject that points to the internal image memory. - # - # @return A PyCObject object. - - def getim(self): - "Get PyCObject pointer to internal image memory" - - self.load() - return self.im.ptr - - - ## - # Returns the image palette as a list. - # - # @return A list of color values [r, g, b, ...], or None if the - # image has no palette. - - def getpalette(self): - "Get palette contents." - - self.load() - try: - return map(ord, self.im.getpalette()) - except ValueError: - return None # no palette - - - ## - # Returns the pixel value at a given position. - # - # @param xy The coordinate, given as (x, y). - # @return The pixel value. If the image is a multi-layer image, - # this method returns a tuple. - - def getpixel(self, xy): - "Get pixel value" - - self.load() - return self.im.getpixel(xy) - - ## - # Returns the horizontal and vertical projection. - # - # @return Two sequences, indicating where there are non-zero - # pixels along the X-axis and the Y-axis, respectively. - - def getprojection(self): - "Get projection to x and y axes" - - self.load() - x, y = self.im.getprojection() - return map(ord, x), map(ord, y) - - ## - # Returns a histogram for the image. The histogram is returned as - # a list of pixel counts, one for each pixel value in the source - # image. If the image has more than one band, the histograms for - # all bands are concatenated (for example, the histogram for an - # "RGB" image contains 768 values). - #

- # A bilevel image (mode "1") is treated as a greyscale ("L") image - # by this method. - #

- # If a mask is provided, the method returns a histogram for those - # parts of the image where the mask image is non-zero. The mask - # image must have the same size as the image, and be either a - # bi-level image (mode "1") or a greyscale image ("L"). - # - # @def histogram(mask=None) - # @param mask An optional mask. - # @return A list containing pixel counts. - - def histogram(self, mask=None, extrema=None): - "Take histogram of image" - - self.load() - if mask: - mask.load() - return self.im.histogram((0, 0), mask.im) - if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.histogram(extrema) - return self.im.histogram() - - ## - # (Deprecated) Returns a copy of the image where the data has been - # offset by the given distances. Data wraps around the edges. If - # yoffset is omitted, it is assumed to be equal to xoffset. - #

- # This method is deprecated. New code should use the offset - # function in the ImageChops module. - # - # @param xoffset The horizontal distance. - # @param yoffset The vertical distance. If omitted, both - # distances are set to the same value. - # @return An Image object. - - def offset(self, xoffset, yoffset=None): - "(deprecated) Offset image in horizontal and/or vertical direction" - if warnings: - warnings.warn( - "'offset' is deprecated; use 'ImageChops.offset' instead", - DeprecationWarning, stacklevel=2 - ) - import ImageChops - return ImageChops.offset(self, xoffset, yoffset) - - ## - # Pastes another image into this image. The box argument is either - # a 2-tuple giving the upper left corner, a 4-tuple defining the - # left, upper, right, and lower pixel coordinate, or None (same as - # (0, 0)). If a 4-tuple is given, the size of the pasted image - # must match the size of the region. - #

- # If the modes don't match, the pasted image is converted to the - # mode of this image (see the {@link #Image.convert} method for - # details). - #

- # Instead of an image, the source can be a integer or tuple - # containing pixel values. The method then fills the region - # with the given colour. When creating RGB images, you can - # also use colour strings as supported by the ImageColor module. - #

- # If a mask is given, this method updates only the regions - # indicated by the mask. You can use either "1", "L" or "RGBA" - # images (in the latter case, the alpha band is used as mask). - # Where the mask is 255, the given image is copied as is. Where - # the mask is 0, the current value is preserved. Intermediate - # values can be used for transparency effects. - #

- # Note that if you paste an "RGBA" image, the alpha band is - # ignored. You can work around this by using the same image as - # both source image and mask. - # - # @param im Source image or pixel value (integer or tuple). - # @param box An optional 4-tuple giving the region to paste into. - # If a 2-tuple is used instead, it's treated as the upper left - # corner. If omitted or None, the source is pasted into the - # upper left corner. - #

- # If an image is given as the second argument and there is no - # third, the box defaults to (0, 0), and the second argument - # is interpreted as a mask image. - # @param mask An optional mask image. - # @return An Image object. - - def paste(self, im, box=None, mask=None): - "Paste other image into region" - - if isImageType(box) and mask is None: - # abbreviated paste(im, mask) syntax - mask = box; box = None - - if box is None: - # cover all of self - box = (0, 0) + self.size - - if len(box) == 2: - # lower left corner given; get size from image or mask - if isImageType(im): - size = im.size - elif isImageType(mask): - size = mask.size - else: - # FIXME: use self.size here? - raise ValueError( - "cannot determine region size; use 4-item box" - ) - box = box + (box[0]+size[0], box[1]+size[1]) - - if isStringType(im): - import ImageColor - im = ImageColor.getcolor(im, self.mode) - - elif isImageType(im): - im.load() - if self.mode != im.mode: - if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): - # should use an adapter for this! - im = im.convert(self.mode) - im = im.im - - self.load() - if self.readonly: - self._copy() - - if mask: - mask.load() - self.im.paste(im, box, mask.im) - else: - self.im.paste(im, box) - - ## - # Maps this image through a lookup table or function. - # - # @param lut A lookup table, containing 256 values per band in the - # image. A function can be used instead, it should take a single - # argument. The function is called once for each possible pixel - # value, and the resulting table is applied to all bands of the - # image. - # @param mode Output mode (default is same as input). In the - # current version, this can only be used if the source image - # has mode "L" or "P", and the output has mode "1". - # @return An Image object. - - def point(self, lut, mode=None): - "Map image through lookup table" - - self.load() - - if isinstance(lut, ImagePointHandler): - return lut.point(self) - - if not isSequenceType(lut): - # if it isn't a list, it should be a function - if self.mode in ("I", "I;16", "F"): - # check if the function can be used with point_transform - scale, offset = _getscaleoffset(lut) - return self._new(self.im.point_transform(scale, offset)) - # for other modes, convert the function to a table - lut = map(lut, range(256)) * self.im.bands - - if self.mode == "F": - # FIXME: _imaging returns a confusing error message for this case - raise ValueError("point operation not supported for this mode") - - return self._new(self.im.point(lut, mode)) - - ## - # Adds or replaces the alpha layer in this image. If the image - # does not have an alpha layer, it's converted to "LA" or "RGBA". - # The new layer must be either "L" or "1". - # - # @param im The new alpha layer. This can either be an "L" or "1" - # image having the same size as this image, or an integer or - # other color value. - - def putalpha(self, alpha): - "Set alpha layer" - - self.load() - if self.readonly: - self._copy() - - if self.mode not in ("LA", "RGBA"): - # attempt to promote self to a matching alpha mode - try: - mode = getmodebase(self.mode) + "A" - try: - self.im.setmode(mode) - except (AttributeError, ValueError): - # do things the hard way - im = self.im.convert(mode) - if im.mode not in ("LA", "RGBA"): - raise ValueError # sanity check - self.im = im - self.mode = self.im.mode - except (KeyError, ValueError): - raise ValueError("illegal image mode") - - if self.mode == "LA": - band = 1 - else: - band = 3 - - if isImageType(alpha): - # alpha layer - if alpha.mode not in ("1", "L"): - raise ValueError("illegal image mode") - alpha.load() - if alpha.mode == "1": - alpha = alpha.convert("L") - else: - # constant alpha - try: - self.im.fillband(band, alpha) - except (AttributeError, ValueError): - # do things the hard way - alpha = new("L", self.size, alpha) - else: - return - - self.im.putband(alpha.im, band) - - ## - # Copies pixel data to this image. This method copies data from a - # sequence object into the image, starting at the upper left - # corner (0, 0), and continuing until either the image or the - # sequence ends. The scale and offset values are used to adjust - # the sequence values: pixel = value*scale + offset. - # - # @param data A sequence object. - # @param scale An optional scale value. The default is 1.0. - # @param offset An optional offset value. The default is 0.0. - - def putdata(self, data, scale=1.0, offset=0.0): - "Put data from a sequence object into an image." - - self.load() - if self.readonly: - self._copy() - - self.im.putdata(data, scale, offset) - - ## - # Attaches a palette to this image. The image must be a "P" or - # "L" image, and the palette sequence must contain 768 integer - # values, where each group of three values represent the red, - # green, and blue values for the corresponding pixel - # index. Instead of an integer sequence, you can use an 8-bit - # string. - # - # @def putpalette(data) - # @param data A palette sequence (either a list or a string). - - def putpalette(self, data, rawmode="RGB"): - "Put palette data into an image." - - if self.mode not in ("L", "P"): - raise ValueError("illegal image mode") - self.load() - if isinstance(data, ImagePalette.ImagePalette): - palette = ImagePalette.raw(data.rawmode, data.palette) - else: - if not isStringType(data): - data = string.join(map(chr, data), "") - palette = ImagePalette.raw(rawmode, data) - self.mode = "P" - self.palette = palette - self.palette.mode = "RGB" - self.load() # install new palette - - ## - # Modifies the pixel at the given position. The colour is given as - # a single numerical value for single-band images, and a tuple for - # multi-band images. - #

- # Note that this method is relatively slow. For more extensive - # changes, use {@link #Image.paste} or the ImageDraw module - # instead. - # - # @param xy The pixel coordinate, given as (x, y). - # @param value The pixel value. - # @see #Image.paste - # @see #Image.putdata - # @see ImageDraw - - def putpixel(self, xy, value): - "Set pixel value" - - self.load() - if self.readonly: - self._copy() - - return self.im.putpixel(xy, value) - - ## - # Returns a resized copy of this image. - # - # @def resize(size, filter=NEAREST) - # @param size The requested size in pixels, as a 2-tuple: - # (width, height). - # @param filter An optional resampling filter. This can be - # one of NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), BICUBIC - # (cubic spline interpolation in a 4x4 environment), or - # ANTIALIAS (a high-quality downsampling filter). - # If omitted, or if the image has mode "1" or "P", it is - # set NEAREST. - # @return An Image object. - - def resize(self, size, resample=NEAREST): - "Resize image" - - if resample not in (NEAREST, BILINEAR, BICUBIC, ANTIALIAS): - raise ValueError("unknown resampling filter") - - self.load() - - if self.mode in ("1", "P"): - resample = NEAREST - - if resample == ANTIALIAS: - # requires stretch support (imToolkit & PIL 1.1.3) - try: - im = self.im.stretch(size, resample) - except AttributeError: - raise ValueError("unsupported resampling filter") - else: - im = self.im.resize(size, resample) - - return self._new(im) - - ## - # Returns a rotated copy of this image. This method returns a - # copy of this image, rotated the given number of degrees counter - # clockwise around its centre. - # - # @def rotate(angle, filter=NEAREST) - # @param angle In degrees counter clockwise. - # @param filter An optional resampling filter. This can be - # one of NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), or BICUBIC - # (cubic spline interpolation in a 4x4 environment). - # If omitted, or if the image has mode "1" or "P", it is - # set NEAREST. - # @param expand Optional expansion flag. If true, expands the output - # image to make it large enough to hold the entire rotated image. - # If false or omitted, make the output image the same size as the - # input image. - # @return An Image object. - - def rotate(self, angle, resample=NEAREST, expand=0): - "Rotate image. Angle given as degrees counter-clockwise." - - if expand: - import math - angle = -angle * math.pi / 180 - matrix = [ - math.cos(angle), math.sin(angle), 0.0, - -math.sin(angle), math.cos(angle), 0.0 - ] - def transform(x, y, (a, b, c, d, e, f)=matrix): - return a*x + b*y + c, d*x + e*y + f - - # calculate output size - w, h = self.size - xx = [] - yy = [] - for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - x, y = transform(x, y) - xx.append(x) - yy.append(y) - w = int(math.ceil(max(xx)) - math.floor(min(xx))) - h = int(math.ceil(max(yy)) - math.floor(min(yy))) - - # adjust center - x, y = transform(w / 2.0, h / 2.0) - matrix[2] = self.size[0] / 2.0 - x - matrix[5] = self.size[1] / 2.0 - y - - return self.transform((w, h), AFFINE, matrix, resample) - - if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") - - self.load() - - if self.mode in ("1", "P"): - resample = NEAREST - - return self._new(self.im.rotate(angle, resample)) - - ## - # Saves this image under the given filename. If no format is - # specified, the format to use is determined from the filename - # extension, if possible. - #

- # Keyword options can be used to provide additional instructions - # to the writer. If a writer doesn't recognise an option, it is - # silently ignored. The available options are described later in - # this handbook. - #

- # You can use a file object instead of a filename. In this case, - # you must always specify the format. The file object must - # implement the seek, tell, and write - # methods, and be opened in binary mode. - # - # @def save(file, format=None, **options) - # @param file File name or file object. - # @param format Optional format override. If omitted, the - # format to use is determined from the filename extension. - # If a file object was used instead of a filename, this - # parameter should always be used. - # @param **options Extra parameters to the image writer. - # @return None - # @exception KeyError If the output format could not be determined - # from the file name. Use the format option to solve this. - # @exception IOError If the file could not be written. The file - # may have been created, and may contain partial data. - - def save(self, fp, format=None, **params): - "Save image to file or stream" - - if isStringType(fp): - filename = fp - else: - if hasattr(fp, "name") and isStringType(fp.name): - filename = fp.name - else: - filename = "" - - # may mutate self! - self.load() - - self.encoderinfo = params - self.encoderconfig = () - - preinit() - - ext = string.lower(os.path.splitext(filename)[1]) - - if not format: - try: - format = EXTENSION[ext] - except KeyError: - init() - try: - format = EXTENSION[ext] - except KeyError: - raise KeyError(ext) # unknown extension - - try: - save_handler = SAVE[string.upper(format)] - except KeyError: - init() - save_handler = SAVE[string.upper(format)] # unknown format - - if isStringType(fp): - import __builtin__ - fp = __builtin__.open(fp, "wb") - close = 1 - else: - close = 0 - - try: - save_handler(self, fp, filename) - finally: - # do what we can to clean up - if close: - fp.close() - - ## - # Seeks to the given frame in this sequence file. If you seek - # beyond the end of the sequence, the method raises an - # EOFError exception. When a sequence file is opened, the - # library automatically seeks to frame 0. - #

- # Note that in the current version of the library, most sequence - # formats only allows you to seek to the next frame. - # - # @param frame Frame number, starting at 0. - # @exception EOFError If the call attempts to seek beyond the end - # of the sequence. - # @see #Image.tell - - def seek(self, frame): - "Seek to given frame in sequence file" - - # overridden by file handlers - if frame != 0: - raise EOFError - - ## - # Displays this image. This method is mainly intended for - # debugging purposes. - #

- # On Unix platforms, this method saves the image to a temporary - # PPM file, and calls the xv utility. - #

- # On Windows, it saves the image to a temporary BMP file, and uses - # the standard BMP display utility to show it (usually Paint). - # - # @def show(title=None) - # @param title Optional title to use for the image window, - # where possible. - - def show(self, title=None, command=None): - "Display image (for debug purposes only)" - - _show(self, title=title, command=command) - - ## - # Split this image into individual bands. This method returns a - # tuple of individual image bands from an image. For example, - # splitting an "RGB" image creates three new images each - # containing a copy of one of the original bands (red, green, - # blue). - # - # @return A tuple containing bands. - - def split(self): - "Split image into bands" - - if self.im.bands == 1: - ims = [self.copy()] - else: - ims = [] - self.load() - for i in range(self.im.bands): - ims.append(self._new(self.im.getband(i))) - return tuple(ims) - - ## - # Returns the current frame number. - # - # @return Frame number, starting with 0. - # @see #Image.seek - - def tell(self): - "Return current frame number" - - return 0 - - ## - # Make this image into a thumbnail. This method modifies the - # image to contain a thumbnail version of itself, no larger than - # the given size. This method calculates an appropriate thumbnail - # size to preserve the aspect of the image, calls the {@link - # #Image.draft} method to configure the file reader (where - # applicable), and finally resizes the image. - #

- # Note that the bilinear and bicubic filters in the current - # version of PIL are not well-suited for thumbnail generation. - # You should use ANTIALIAS unless speed is much more - # important than quality. - #

- # Also note that this function modifies the Image object in place. - # If you need to use the full resolution image as well, apply this - # method to a {@link #Image.copy} of the original image. - # - # @param size Requested size. - # @param resample Optional resampling filter. This can be one - # of NEAREST, BILINEAR, BICUBIC, or - # ANTIALIAS (best quality). If omitted, it defaults - # to NEAREST (this will be changed to ANTIALIAS in a - # future version). - # @return None - - def thumbnail(self, size, resample=NEAREST): - "Create thumbnail representation (modifies image in place)" - - # FIXME: the default resampling filter will be changed - # to ANTIALIAS in future versions - - # preserve aspect ratio - x, y = self.size - if x > size[0]: y = max(y * size[0] / x, 1); x = size[0] - if y > size[1]: x = max(x * size[1] / y, 1); y = size[1] - size = x, y - - if size == self.size: - return - - self.draft(None, size) - - self.load() - - try: - im = self.resize(size, resample) - except ValueError: - if resample != ANTIALIAS: - raise - im = self.resize(size, NEAREST) # fallback - - self.im = im.im - self.mode = im.mode - self.size = size - - self.readonly = 0 - - # FIXME: the different tranform methods need further explanation - # instead of bloating the method docs, add a separate chapter. - - ## - # Transforms this image. This method creates a new image with the - # given size, and the same mode as the original, and copies data - # to the new image using the given transform. - #

- # @def transform(size, method, data, resample=NEAREST) - # @param size The output size. - # @param method The transformation method. This is one of - # EXTENT (cut out a rectangular subregion), AFFINE - # (affine transform), PERSPECTIVE (perspective - # transform), QUAD (map a quadrilateral to a - # rectangle), or MESH (map a number of source quadrilaterals - # in one operation). - # @param data Extra data to the transformation method. - # @param resample Optional resampling filter. It can be one of - # NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), or - # BICUBIC (cubic spline interpolation in a 4x4 - # environment). If omitted, or if the image has mode - # "1" or "P", it is set to NEAREST. - # @return An Image object. - - def transform(self, size, method, data=None, resample=NEAREST, fill=1): - "Transform image" - - if isinstance(method, ImageTransformHandler): - return method.transform(size, self, resample=resample, fill=fill) - if hasattr(method, "getdata"): - # compatibility w. old-style transform objects - method, data = method.getdata() - if data is None: - raise ValueError("missing method data") - im = new(self.mode, size, None) - if method == MESH: - # list of quads - for box, quad in data: - im.__transformer(box, self, QUAD, quad, resample, fill) - else: - im.__transformer((0, 0)+size, self, method, data, resample, fill) - - return im - - def __transformer(self, box, image, method, data, - resample=NEAREST, fill=1): - - # FIXME: this should be turned into a lazy operation (?) - - w = box[2]-box[0] - h = box[3]-box[1] - - if method == AFFINE: - # change argument order to match implementation - data = (data[2], data[0], data[1], - data[5], data[3], data[4]) - elif method == EXTENT: - # convert extent to an affine transform - x0, y0, x1, y1 = data - xs = float(x1 - x0) / w - ys = float(y1 - y0) / h - method = AFFINE - data = (x0 + xs/2, xs, 0, y0 + ys/2, 0, ys) - elif method == PERSPECTIVE: - # change argument order to match implementation - data = (data[2], data[0], data[1], - data[5], data[3], data[4], - data[6], data[7]) - elif method == QUAD: - # quadrilateral warp. data specifies the four corners - # given as NW, SW, SE, and NE. - nw = data[0:2]; sw = data[2:4]; se = data[4:6]; ne = data[6:8] - x0, y0 = nw; As = 1.0 / w; At = 1.0 / h - data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, - (se[0]-sw[0]-ne[0]+x0)*As*At, - y0, (ne[1]-y0)*As, (sw[1]-y0)*At, - (se[1]-sw[1]-ne[1]+y0)*As*At) - else: - raise ValueError("unknown transformation method") - - if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") - - image.load() - - self.load() - - if image.mode in ("1", "P"): - resample = NEAREST - - self.im.transform2(box, image.im, method, data, resample, fill) - - ## - # Returns a flipped or rotated copy of this image. - # - # @param method One of FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, - # ROTATE_90, ROTATE_180, or ROTATE_270. - - def transpose(self, method): - "Transpose image (flip or rotate in 90 degree steps)" - - self.load() - im = self.im.transpose(method) - return self._new(im) - -# -------------------------------------------------------------------- -# Lazy operations - -class _ImageCrop(Image): - - def __init__(self, im, box): - - Image.__init__(self) - - x0, y0, x1, y1 = box - if x1 < x0: - x1 = x0 - if y1 < y0: - y1 = y0 - - self.mode = im.mode - self.size = x1-x0, y1-y0 - - self.__crop = x0, y0, x1, y1 - - self.im = im.im - - def load(self): - - # lazy evaluation! - if self.__crop: - self.im = self.im.crop(self.__crop) - self.__crop = None - - if self.im: - return self.im.pixel_access(self.readonly) - - # FIXME: future versions should optimize crop/paste - # sequences! - -# -------------------------------------------------------------------- -# Abstract handlers. - -class ImagePointHandler: - # used as a mixin by point transforms (for use with im.point) - pass - -class ImageTransformHandler: - # used as a mixin by geometry transforms (for use with im.transform) - pass - -# -------------------------------------------------------------------- -# Factories - -# -# Debugging - -def _wedge(): - "Create greyscale wedge (for debugging only)" - - return Image()._new(core.wedge("L")) - -## -# Creates a new image with the given mode and size. -# -# @param mode The mode to use for the new image. -# @param size A 2-tuple, containing (width, height) in pixels. -# @param color What colour to use for the image. Default is black. -# If given, this should be a single integer or floating point value -# for single-band modes, and a tuple for multi-band modes (one value -# per band). When creating RGB images, you can also use colour -# strings as supported by the ImageColor module. If the colour is -# None, the image is not initialised. -# @return An Image object. - -def new(mode, size, color=0): - "Create a new image" - - if color is None: - # don't initialize - return Image()._new(core.new(mode, size)) - - if isStringType(color): - # css3-style specifier - - import ImageColor - color = ImageColor.getcolor(color, mode) - - return Image()._new(core.fill(mode, size, color)) - -## -# Creates an image memory from pixel data in a string. -#

-# In its simplest form, this function takes three arguments -# (mode, size, and unpacked pixel data). -#

-# You can also use any pixel decoder supported by PIL. For more -# information on available decoders, see the section Writing Your Own File Decoder. -#

-# Note that this function decodes pixel data only, not entire images. -# If you have an entire image in a string, wrap it in a -# StringIO object, and use {@link #open} to load it. -# -# @param mode The image mode. -# @param size The image size. -# @param data An 8-bit string containing raw data for the given mode. -# @param decoder_name What decoder to use. -# @param *args Additional parameters for the given decoder. -# @return An Image object. - -def fromstring(mode, size, data, decoder_name="raw", *args): - "Load image from string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if decoder_name == "raw" and args == (): - args = mode - - im = new(mode, size) - im.fromstring(data, decoder_name, args) - return im - -## -# (New in 1.1.4) Creates an image memory from pixel data in a string -# or byte buffer. -#

-# This function is similar to {@link #fromstring}, but uses data in -# the byte buffer, where possible. This means that changes to the -# original buffer object are reflected in this image). Not all modes -# can share memory; supported modes include "L", "RGBX", "RGBA", and -# "CMYK". -#

-# Note that this function decodes pixel data only, not entire images. -# If you have an entire image file in a string, wrap it in a -# StringIO object, and use {@link #open} to load it. -#

-# In the current version, the default parameters used for the "raw" -# decoder differs from that used for {@link fromstring}. This is a -# bug, and will probably be fixed in a future release. The current -# release issues a warning if you do this; to disable the warning, -# you should provide the full set of parameters. See below for -# details. -# -# @param mode The image mode. -# @param size The image size. -# @param data An 8-bit string or other buffer object containing raw -# data for the given mode. -# @param decoder_name What decoder to use. -# @param *args Additional parameters for the given decoder. For the -# default encoder ("raw"), it's recommended that you provide the -# full set of parameters: -# frombuffer(mode, size, data, "raw", mode, 0, 1). -# @return An Image object. -# @since 1.1.4 - -def frombuffer(mode, size, data, decoder_name="raw", *args): - "Load image from string or buffer" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if decoder_name == "raw": - if args == (): - if warnings: - warnings.warn( - "the frombuffer defaults may change in a future release; " - "for portability, change the call to read:\n" - " frombuffer(mode, size, data, 'raw', mode, 0, 1)", - RuntimeWarning, stacklevel=2 - ) - args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 - if args[0] in _MAPMODES: - im = new(mode, (1,1)) - im = im._new( - core.map_buffer(data, size, decoder_name, None, 0, args) - ) - im.readonly = 1 - return im - - return fromstring(mode, size, data, decoder_name, args) - - -## -# (New in 1.1.6) Creates an image memory from an object exporting -# the array interface (using the buffer protocol). -# -# If obj is not contiguous, then the tostring method is called -# and {@link frombuffer} is used. -# -# @param obj Object with array interface -# @param mode Mode to use (will be determined from type if None) -# @return An image memory. - -def fromarray(obj, mode=None): - arr = obj.__array_interface__ - shape = arr['shape'] - ndim = len(shape) - try: - strides = arr['strides'] - except KeyError: - strides = None - if mode is None: - try: - typekey = (1, 1) + shape[2:], arr['typestr'] - mode, rawmode = _fromarray_typemap[typekey] - except KeyError: - # print typekey - raise TypeError("Cannot handle this data type") - else: - rawmode = mode - if mode in ["1", "L", "I", "P", "F"]: - ndmax = 2 - elif mode == "RGB": - ndmax = 3 - else: - ndmax = 4 - if ndim > ndmax: - raise ValueError("Too many dimensions.") - - size = shape[1], shape[0] - if strides is not None: - obj = obj.tostring() - - return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) - -_fromarray_typemap = { - # (shape, typestr) => mode, rawmode - # first two members of shape are set to one - # ((1, 1), "|b1"): ("1", "1"), # broken - ((1, 1), "|u1"): ("L", "L"), - ((1, 1), "|i1"): ("I", "I;8"), - ((1, 1), "i2"): ("I", "I;16B"), - ((1, 1), "i4"): ("I", "I;32B"), - ((1, 1), "f4"): ("F", "F;32BF"), - ((1, 1), "f8"): ("F", "F;64BF"), - ((1, 1, 3), "|u1"): ("RGB", "RGB"), - ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), - } - -# shortcuts -_fromarray_typemap[((1, 1), _ENDIAN + "i4")] = ("I", "I") -_fromarray_typemap[((1, 1), _ENDIAN + "f4")] = ("F", "F") - -## -# Opens and identifies the given image file. -#

-# This is a lazy operation; this function identifies the file, but the -# actual image data is not read from the file until you try to process -# the data (or call the {@link #Image.load} method). -# -# @def open(file, mode="r") -# @param file A filename (string) or a file object. The file object -# must implement read, seek, and tell methods, -# and be opened in binary mode. -# @param mode The mode. If given, this argument must be "r". -# @return An Image object. -# @exception IOError If the file cannot be found, or the image cannot be -# opened and identified. -# @see #new - -def open(fp, mode="r"): - "Open an image file, without loading the raster data" - - if mode != "r": - raise ValueError("bad mode") - - if isStringType(fp): - import __builtin__ - filename = fp - fp = __builtin__.open(fp, "rb") - else: - filename = "" - - prefix = fp.read(16) - - preinit() - - for i in ID: - try: - factory, accept = OPEN[i] - if not accept or accept(prefix): - fp.seek(0) - return factory(fp, filename) - except (SyntaxError, IndexError, TypeError): - pass - - if init(): - - for i in ID: - try: - factory, accept = OPEN[i] - if not accept or accept(prefix): - fp.seek(0) - return factory(fp, filename) - except (SyntaxError, IndexError, TypeError): - pass - - raise IOError("cannot identify image file") - -# -# Image processing. - -## -# Creates a new image by interpolating between two input images, using -# a constant alpha. -# -#

-#    out = image1 * (1.0 - alpha) + image2 * alpha
-# 
-# -# @param im1 The first image. -# @param im2 The second image. Must have the same mode and size as -# the first image. -# @param alpha The interpolation alpha factor. If alpha is 0.0, a -# copy of the first image is returned. If alpha is 1.0, a copy of -# the second image is returned. There are no restrictions on the -# alpha value. If necessary, the result is clipped to fit into -# the allowed output range. -# @return An Image object. - -def blend(im1, im2, alpha): - "Interpolate between images." - - im1.load() - im2.load() - return im1._new(core.blend(im1.im, im2.im, alpha)) - -## -# Creates a new image by interpolating between two input images, -# using the mask as alpha. -# -# @param image1 The first image. -# @param image2 The second image. Must have the same mode and -# size as the first image. -# @param mask A mask image. This image can can have mode -# "1", "L", or "RGBA", and must have the same size as the -# other two images. - -def composite(image1, image2, mask): - "Create composite image by blending images using a transparency mask" - - image = image2.copy() - image.paste(image1, None, mask) - return image - -## -# Applies the function (which should take one argument) to each pixel -# in the given image. If the image has more than one band, the same -# function is applied to each band. Note that the function is -# evaluated once for each possible pixel value, so you cannot use -# random components or other generators. -# -# @def eval(image, function) -# @param image The input image. -# @param function A function object, taking one integer argument. -# @return An Image object. - -def eval(image, *args): - "Evaluate image expression" - - return image.point(args[0]) - -## -# Creates a new image from a number of single-band images. -# -# @param mode The mode to use for the output image. -# @param bands A sequence containing one single-band image for -# each band in the output image. All bands must have the -# same size. -# @return An Image object. - -def merge(mode, bands): - "Merge a set of single band images into a new multiband image." - - if getmodebands(mode) != len(bands) or "*" in mode: - raise ValueError("wrong number of bands") - for im in bands[1:]: - if im.mode != getmodetype(mode): - raise ValueError("mode mismatch") - if im.size != bands[0].size: - raise ValueError("size mismatch") - im = core.new(mode, bands[0].size) - for i in range(getmodebands(mode)): - bands[i].load() - im.putband(bands[i].im, i) - return bands[0]._new(im) - -# -------------------------------------------------------------------- -# Plugin registry - -## -# Register an image file plugin. This function should not be used -# in application code. -# -# @param id An image format identifier. -# @param factory An image file factory method. -# @param accept An optional function that can be used to quickly -# reject images having another format. - -def register_open(id, factory, accept=None): - id = string.upper(id) - ID.append(id) - OPEN[id] = factory, accept - -## -# Registers an image MIME type. This function should not be used -# in application code. -# -# @param id An image format identifier. -# @param mimetype The image MIME type for this format. - -def register_mime(id, mimetype): - MIME[string.upper(id)] = mimetype - -## -# Registers an image save function. This function should not be -# used in application code. -# -# @param id An image format identifier. -# @param driver A function to save images in this format. - -def register_save(id, driver): - SAVE[string.upper(id)] = driver - -## -# Registers an image extension. This function should not be -# used in application code. -# -# @param id An image format identifier. -# @param extension An extension used for this format. - -def register_extension(id, extension): - EXTENSION[string.lower(extension)] = string.upper(id) - - -# -------------------------------------------------------------------- -# Simple display support. User code may override this. - -def _show(image, **options): - # override me, as necessary - apply(_showxv, (image,), options) - -def _showxv(image, title=None, **options): - import ImageShow - apply(ImageShow.show, (image, title), options) diff --git a/pyqtgraph/PIL_Fix/README b/pyqtgraph/PIL_Fix/README deleted file mode 100644 index 3711e113..00000000 --- a/pyqtgraph/PIL_Fix/README +++ /dev/null @@ -1,11 +0,0 @@ -The file Image.py is a drop-in replacement for the same file in PIL 1.1.6. -It adds support for reading 16-bit TIFF files and converting then to numpy arrays. -(I submitted the changes to the PIL folks long ago, but to my knowledge the code -is not being used by them.) - -To use, copy this file into - /usr/lib/python2.6/dist-packages/PIL/ -or - C:\Python26\lib\site-packages\PIL\ - -..or wherever your system keeps its python modules. diff --git a/pyqtgraph/util/pil_fix.py b/pyqtgraph/util/pil_fix.py new file mode 100644 index 00000000..da1c52b3 --- /dev/null +++ b/pyqtgraph/util/pil_fix.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +Importing this module installs support for 16-bit images in PIL. +This works by patching objects in the PIL namespace; no files are +modified. +""" + +from PIL import Image + +if Image.VERSION == '1.1.7': + Image._MODE_CONV["I;16"] = ('%su2' % Image._ENDIAN, None) + Image._fromarray_typemap[((1, 1), " ndmax: + raise ValueError("Too many dimensions.") + + size = shape[:2][::-1] + if strides is not None: + obj = obj.tostring() + + return frombuffer(mode, size, obj, "raw", mode, 0, 1) + + Image.fromarray=fromarray \ No newline at end of file From 4896de5ee49715da47f7719bc68b5da9b8360bc9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Aug 2014 12:45:35 -0400 Subject: [PATCH 244/268] Fixed item context menus appearing after mouse has exited the item area. This occurred because the scene does not receive mouse move events while a context menu is displayed. If the user right-clicks on a new location while the menu is open, then the click event is delieverd as if the mouse had not moved. Corrected by sending a just-in-time hover event immediately before mouse press, if the cursor has moved. --- pyqtgraph/GraphicsScene/GraphicsScene.py | 37 ++++++++++-------------- pyqtgraph/GraphicsScene/mouseEvents.py | 3 ++ 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index a57cca34..c6afbe0f 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -135,8 +135,13 @@ class GraphicsScene(QtGui.QGraphicsScene): def mousePressEvent(self, ev): #print 'scenePress' QtGui.QGraphicsScene.mousePressEvent(self, ev) - #print "mouseGrabberItem: ", self.mouseGrabberItem() if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events + if self.lastHoverEvent is not None: + # If the mouse has moved since the last hover event, send a new one. + # This can happen if a context menu is open while the mouse is moving. + if ev.scenePos() != self.lastHoverEvent.scenePos(): + self.sendHoverEvents(ev) + self.clickEvents.append(MouseClickEvent(ev)) ## set focus on the topmost focusable item under this click @@ -145,10 +150,6 @@ class GraphicsScene(QtGui.QGraphicsScene): if i.isEnabled() and i.isVisible() and int(i.flags() & i.ItemIsFocusable) > 0: i.setFocus(QtCore.Qt.MouseFocusReason) break - #else: - #addr = sip.unwrapinstance(sip.cast(self.mouseGrabberItem(), QtGui.QGraphicsItem)) - #item = GraphicsScene._addressCache.get(addr, self.mouseGrabberItem()) - #print "click grabbed by:", item def mouseMoveEvent(self, ev): self.sigMouseMoved.emit(ev.scenePos()) @@ -189,7 +190,6 @@ class GraphicsScene(QtGui.QGraphicsScene): def mouseReleaseEvent(self, ev): #print 'sceneRelease' if self.mouseGrabberItem() is None: - #print "sending click/drag event" if ev.button() in self.dragButtons: if self.sendDragEvent(ev, final=True): #print "sent drag event" @@ -231,6 +231,8 @@ class GraphicsScene(QtGui.QGraphicsScene): prevItems = list(self.hoverItems.keys()) + #print "hover prev items:", prevItems + #print "hover test items:", items for item in items: if hasattr(item, 'hoverEvent'): event.currentItem = item @@ -248,6 +250,7 @@ class GraphicsScene(QtGui.QGraphicsScene): event.enter = False event.exit = True + #print "hover exit items:", prevItems for item in prevItems: event.currentItem = item try: @@ -257,9 +260,13 @@ class GraphicsScene(QtGui.QGraphicsScene): finally: del self.hoverItems[item] - if hasattr(ev, 'buttons') and int(ev.buttons()) == 0: + # Update last hover event unless: + # - mouse is dragging (move+buttons); in this case we want the dragged + # item to continue receiving events until the drag is over + # - event is not a mouse event (QEvent.Leave sometimes appears here) + if (ev.type() == ev.GraphicsSceneMousePress or + (ev.type() == ev.GraphicsSceneMouseMove and int(ev.buttons()) == 0)): self.lastHoverEvent = event ## save this so we can ask about accepted events later. - def sendDragEvent(self, ev, init=False, final=False): ## Send a MouseDragEvent to the current dragItem or to @@ -323,7 +330,6 @@ class GraphicsScene(QtGui.QGraphicsScene): acceptedItem = self.lastHoverEvent.clickItems().get(ev.button(), None) else: acceptedItem = None - if acceptedItem is not None: ev.currentItem = acceptedItem try: @@ -345,22 +351,9 @@ class GraphicsScene(QtGui.QGraphicsScene): if int(item.flags() & item.ItemIsFocusable) > 0: item.setFocus(QtCore.Qt.MouseFocusReason) break - #if not ev.isAccepted() and ev.button() is QtCore.Qt.RightButton: - #print "GraphicsScene emitting sigSceneContextMenu" - #self.sigMouseClicked.emit(ev) - #ev.accept() self.sigMouseClicked.emit(ev) return ev.isAccepted() - #def claimEvent(self, item, button, eventType): - #key = (button, eventType) - #if key in self.claimedEvents: - #return False - #self.claimedEvents[key] = item - #print "event", key, "claimed by", item - #return True - - def items(self, *args): #print 'args:', args items = QtGui.QGraphicsScene.items(self, *args) diff --git a/pyqtgraph/GraphicsScene/mouseEvents.py b/pyqtgraph/GraphicsScene/mouseEvents.py index 7809d464..2e472e04 100644 --- a/pyqtgraph/GraphicsScene/mouseEvents.py +++ b/pyqtgraph/GraphicsScene/mouseEvents.py @@ -355,6 +355,9 @@ class HoverEvent(object): return Point(self.currentItem.mapFromScene(self._lastScenePos)) def __repr__(self): + if self.exit: + return "" + if self.currentItem is None: lp = self._lastScenePos p = self._scenePos From 490148fe5c5091b1b2bc961356188fc5cc8c3187 Mon Sep 17 00:00:00 2001 From: Jacob Welsh Date: Sun, 24 Aug 2014 17:44:39 -0500 Subject: [PATCH 245/268] Fix getGitVersion showing a clean repo as modified --- tools/setupHelpers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index b23fea0a..b308b226 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -377,9 +377,9 @@ def getGitVersion(tagPrefix): # any uncommitted modifications? modified = False - status = check_output(['git', 'status', '-s'], universal_newlines=True).strip().split('\n') + status = check_output(['git', 'status', '--porcelain'], universal_newlines=True).strip().split('\n') for line in status: - if line[:2] != '??': + if line != '' and line[:2] != '??': modified = True break @@ -558,5 +558,3 @@ class MergeTestCommand(Command): def finalize_options(self): pass - - \ No newline at end of file From a7b0bbb3bb0f025e94c12e1d918862b732550ffc Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Aug 2014 22:50:14 -0400 Subject: [PATCH 246/268] Fixed AxisItem ignoring setWidth when label is displayed --- pyqtgraph/graphicsItems/AxisItem.py | 104 ++++++++++++++++++---------- 1 file changed, 67 insertions(+), 37 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index e5b9e3f5..b125cb7e 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -62,6 +62,11 @@ class AxisItem(GraphicsWidget): self.textWidth = 30 ## Keeps track of maximum width / height of tick text self.textHeight = 18 + # If the user specifies a width / height, remember that setting + # indefinitely. + self.fixedWidth = None + self.fixedHeight = None + self.labelText = '' self.labelUnits = '' self.labelUnitPrefix='' @@ -219,9 +224,9 @@ class AxisItem(GraphicsWidget): #self.drawLabel = show self.label.setVisible(show) if self.orientation in ['left', 'right']: - self.setWidth() + self._updateWidth() else: - self.setHeight() + self._updateHeight() if self.autoSIPrefix: self.updateAutoSIPrefix() @@ -291,54 +296,80 @@ class AxisItem(GraphicsWidget): if mx > self.textWidth or mx < self.textWidth-10: self.textWidth = mx if self.style['autoExpandTextSpace'] is True: - self.setWidth() + self._updateWidth() #return True ## size has changed else: mx = max(self.textHeight, x) if mx > self.textHeight or mx < self.textHeight-10: self.textHeight = mx if self.style['autoExpandTextSpace'] is True: - self.setHeight() + self._updateHeight() #return True ## size has changed def _adjustSize(self): if self.orientation in ['left', 'right']: - self.setWidth() + self._updateWidth() else: - self.setHeight() + self._updateHeight() def setHeight(self, h=None): """Set the height of this axis reserved for ticks and tick labels. - The height of the axis label is automatically added.""" - if h is None: - if not self.style['showValues']: - h = 0 - elif self.style['autoExpandTextSpace'] is True: - h = self.textHeight + The height of the axis label is automatically added. + + If *height* is None, then the value will be determined automatically + based on the size of the tick text.""" + self.fixedHeight = h + self._updateHeight() + + def _updateHeight(self): + if not self.isVisible(): + h = 0 + else: + if self.fixedHeight is None: + if not self.style['showValues']: + h = 0 + elif self.style['autoExpandTextSpace'] is True: + h = self.textHeight + else: + h = self.style['tickTextHeight'] + h += self.style['tickTextOffset'][1] if self.style['showValues'] else 0 + h += max(0, self.style['tickLength']) + if self.label.isVisible(): + h += self.label.boundingRect().height() * 0.8 else: - h = self.style['tickTextHeight'] - h += self.style['tickTextOffset'][1] if self.style['showValues'] else 0 - h += max(0, self.style['tickLength']) - if self.label.isVisible(): - h += self.label.boundingRect().height() * 0.8 + h = self.fixedHeight + self.setMaximumHeight(h) self.setMinimumHeight(h) self.picture = None def setWidth(self, w=None): """Set the width of this axis reserved for ticks and tick labels. - The width of the axis label is automatically added.""" - if w is None: - if not self.style['showValues']: - w = 0 - elif self.style['autoExpandTextSpace'] is True: - w = self.textWidth + The width of the axis label is automatically added. + + If *width* is None, then the value will be determined automatically + based on the size of the tick text.""" + self.fixedWidth = w + self._updateWidth() + + def _updateWidth(self): + if not self.isVisible(): + w = 0 + else: + if self.fixedWidth is None: + if not self.style['showValues']: + w = 0 + elif self.style['autoExpandTextSpace'] is True: + w = self.textWidth + else: + w = self.style['tickTextWidth'] + w += self.style['tickTextOffset'][0] if self.style['showValues'] else 0 + w += max(0, self.style['tickLength']) + if self.label.isVisible(): + w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate else: - w = self.style['tickTextWidth'] - w += self.style['tickTextOffset'][0] if self.style['showValues'] else 0 - w += max(0, self.style['tickLength']) - if self.label.isVisible(): - w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate + w = self.fixedWidth + self.setMaximumWidth(w) self.setMinimumWidth(w) self.picture = None @@ -1009,19 +1040,18 @@ class AxisItem(GraphicsWidget): profiler('draw text') def show(self): - - if self.orientation in ['left', 'right']: - self.setWidth() - else: - self.setHeight() GraphicsWidget.show(self) + if self.orientation in ['left', 'right']: + self._updateWidth() + else: + self._updateHeight() def hide(self): - if self.orientation in ['left', 'right']: - self.setWidth(0) - else: - self.setHeight(0) GraphicsWidget.hide(self) + if self.orientation in ['left', 'right']: + self._updateWidth() + else: + self._updateHeight() def wheelEvent(self, ev): if self.linkedView() is None: From 70d9f1eeed1f141cdf1b8e8fbc029cdd8a0e9fef Mon Sep 17 00:00:00 2001 From: John David Reaver Date: Sun, 28 Sep 2014 08:26:13 -0700 Subject: [PATCH 247/268] Fix OpenGL shader/texture sharing on PySide --- pyqtgraph/opengl/GLViewWidget.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index c71bb3c9..788ab725 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -7,6 +7,8 @@ from .. import functions as fn ##Vector = QtGui.QVector3D +ShareWidget = None + class GLViewWidget(QtOpenGL.QGLWidget): """ Basic widget for displaying 3D data @@ -16,14 +18,14 @@ class GLViewWidget(QtOpenGL.QGLWidget): """ - ShareWidget = None - def __init__(self, parent=None): - if GLViewWidget.ShareWidget is None: + global ShareWidget + + if ShareWidget is None: ## create a dummy widget to allow sharing objects (textures, shaders, etc) between views - GLViewWidget.ShareWidget = QtOpenGL.QGLWidget() + ShareWidget = QtOpenGL.QGLWidget() - QtOpenGL.QGLWidget.__init__(self, parent, GLViewWidget.ShareWidget) + QtOpenGL.QGLWidget.__init__(self, parent, ShareWidget) self.setFocusPolicy(QtCore.Qt.ClickFocus) From 35cacc78aaf4100a8da3fdd9020d7e563fdeab71 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 30 Sep 2014 16:23:00 -0400 Subject: [PATCH 248/268] Update docstrings for TextItem --- pyqtgraph/graphicsItems/TextItem.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index 22b1eee6..d3c98006 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -44,6 +44,11 @@ class TextItem(UIGraphicsItem): self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport def setText(self, text, color=(200,200,200)): + """ + Set the text and color of this item. + + This method sets the plain text of the item; see also setHtml(). + """ color = fn.mkColor(color) self.textItem.setDefaultTextColor(color) self.textItem.setPlainText(text) @@ -57,18 +62,41 @@ class TextItem(UIGraphicsItem): #self.translate(0, 20) def setPlainText(self, *args): + """ + Set the plain text to be rendered by this item. + + See QtGui.QGraphicsTextItem.setPlainText(). + """ self.textItem.setPlainText(*args) self.updateText() def setHtml(self, *args): + """ + Set the HTML code to be rendered by this item. + + See QtGui.QGraphicsTextItem.setHtml(). + """ self.textItem.setHtml(*args) self.updateText() def setTextWidth(self, *args): + """ + Set the width of the text. + + If the text requires more space than the width limit, then it will be + wrapped into multiple lines. + + See QtGui.QGraphicsTextItem.setTextWidth(). + """ self.textItem.setTextWidth(*args) self.updateText() def setFont(self, *args): + """ + Set the font for this text. + + See QtGui.QGraphicsTextItem.setFont(). + """ self.textItem.setFont(*args) self.updateText() From 88bf8880e16df7a7718bcb6e7eff331979d9d4f6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 3 Oct 2014 10:33:08 -0400 Subject: [PATCH 249/268] Correction to exporting docs --- doc/source/exporting.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/exporting.rst b/doc/source/exporting.rst index 137e6584..ccd017d7 100644 --- a/doc/source/exporting.rst +++ b/doc/source/exporting.rst @@ -39,13 +39,14 @@ Exporting from the API To export a file programatically, follow this example:: import pyqtgraph as pg + import pyqtgraph.exporters # generate something to export plt = pg.plot([1,5,2,4,3]) # create an exporter instance, as an argument give it # the item you wish to export - exporter = pg.exporters.ImageExporter.ImageExporter(plt.plotItem) + exporter = pg.exporters.ImageExporter(plt.plotItem) # set export parameters if needed exporter.parameters()['width'] = 100 # (note this also affects height parameter) From 8f273f53ab7138baa5fa8a7a9bc8bfa145a55000 Mon Sep 17 00:00:00 2001 From: John David Reaver Date: Wed, 15 Oct 2014 06:16:40 -0700 Subject: [PATCH 250/268] Fix memory leak in GLScatterPlotItem Fixes #103. If a ScatterPlotItem was removed from a plot and added again, glGenTetures was called again unneccesarily. Each time it is called, it eats up a little more space. --- pyqtgraph/opengl/items/GLScatterPlotItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index 6cfcc6aa..dc4b298a 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -66,7 +66,8 @@ class GLScatterPlotItem(GLGraphicsItem): #print pData.shape, pData.min(), pData.max() pData = pData.astype(np.ubyte) - self.pointTexture = glGenTextures(1) + if getattr(self, "pointTexture", None) is None: + self.pointTexture = glGenTextures(1) glActiveTexture(GL_TEXTURE0) glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.pointTexture) From 6cc0f5e33da62805cf7864aa7a34e3a27e51c157 Mon Sep 17 00:00:00 2001 From: Nicholas Tan Jerome Date: Thu, 16 Oct 2014 12:23:32 +0200 Subject: [PATCH 251/268] fixed the Pen None property. - https://groups.google.com/forum/#!topic/pyqtgraph/t6cl1CevlB0 Signed-off-by: Nicholas Tan Jerome --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index e39b535a..3eb93ada 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -241,8 +241,8 @@ class ScatterPlotItem(GraphicsObject): 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. 'antialias': getConfigOption('antialias'), 'name': None, - } - + } + self.setPen(fn.mkPen(getConfigOption('foreground')), update=False) self.setBrush(fn.mkBrush(100,100,150), update=False) self.setSymbol('o', update=False) @@ -351,16 +351,12 @@ class ScatterPlotItem(GraphicsObject): newData = self.data[len(oldData):] newData['size'] = -1 ## indicates to use default size - + if 'spots' in kargs: spots = kargs['spots'] for i in range(len(spots)): spot = spots[i] for k in spot: - #if k == 'pen': - #newData[k] = fn.mkPen(spot[k]) - #elif k == 'brush': - #newData[k] = fn.mkBrush(spot[k]) if k == 'pos': pos = spot[k] if isinstance(pos, QtCore.QPointF): @@ -369,10 +365,10 @@ class ScatterPlotItem(GraphicsObject): x,y = pos[0], pos[1] newData[i]['x'] = x newData[i]['y'] = y - elif k in ['x', 'y', 'size', 'symbol', 'pen', 'brush', 'data']: + elif k == 'pen': + newData[i][k] = fn.mkPen(spot[k]) + elif k in ['x', 'y', 'size', 'symbol', 'brush', 'data']: newData[i][k] = spot[k] - #elif k == 'data': - #self.pointData[i] = spot[k] else: raise Exception("Unknown spot parameter: %s" % k) elif 'y' in kargs: @@ -389,10 +385,10 @@ class ScatterPlotItem(GraphicsObject): if k in kargs: setMethod = getattr(self, 'set' + k[0].upper() + k[1:]) setMethod(kargs[k], update=False, dataSet=newData, mask=kargs.get('mask', None)) - + if 'data' in kargs: self.setPointData(kargs['data'], dataSet=newData) - + self.prepareGeometryChange() self.informViewBoundsChanged() self.bounds = [None, None] @@ -428,7 +424,7 @@ class ScatterPlotItem(GraphicsObject): all spots which do not have a pen explicitly set.""" update = kargs.pop('update', True) dataSet = kargs.pop('dataSet', self.data) - + if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): pens = args[0] if kargs['mask'] is not None: From 884df4934af6eadaa0065b700853838a32440576 Mon Sep 17 00:00:00 2001 From: Nicholas Tan Jerome Date: Fri, 17 Oct 2014 10:57:36 +0200 Subject: [PATCH 252/268] fixed a keyerror when passing a list into setBrush - https://groups.google.com/forum/#!topic/pyqtgraph/xVyCC2f7gVo Signed-off-by: Nicholas Tan Jerome --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index f1a5201d..ebff4442 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -443,7 +443,7 @@ class ScatterPlotItem(GraphicsObject): if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): brushes = args[0] - if kargs['mask'] is not None: + if 'mask' in kargs and kargs['mask'] is not None: brushes = brushes[kargs['mask']] if len(brushes) != len(dataSet): raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(dataSet))) From 7356126c3d2b11e7abcd7c0b34f03dbd81d69d51 Mon Sep 17 00:00:00 2001 From: Nicholas Tan Jerome Date: Fri, 17 Oct 2014 11:18:12 +0200 Subject: [PATCH 253/268] added "mask" key check on setPen as well Signed-off-by: Nicholas Tan Jerome --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index ebff4442..584d455e 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -421,7 +421,7 @@ class ScatterPlotItem(GraphicsObject): if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): pens = args[0] - if kargs['mask'] is not None: + if if 'mask' in kargs and kargs['mask'] is not None: pens = pens[kargs['mask']] if len(pens) != len(dataSet): raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet))) From 309133042019244b7f3e4baec1c2b4e3a3c4820d Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 21 Oct 2014 14:37:06 -0700 Subject: [PATCH 254/268] Add recursive submenu support for node library. --- pyqtgraph/flowchart/Flowchart.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 878f86ae..7b8cda33 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -823,16 +823,20 @@ class FlowchartWidget(dockarea.DockArea): self.buildMenu() def buildMenu(self, pos=None): + def buildSubMenu(node, rootMenu, subMenus, pos=None): + for section, node in node.items(): + menu = QtGui.QMenu(section) + rootMenu.addMenu(menu) + if isinstance(node, OrderedDict): + buildSubMenu(node, menu, subMenus, pos=pos) + subMenus.append(menu) + else: + act = rootMenu.addAction(section) + act.nodeType = section + act.pos = pos self.nodeMenu = QtGui.QMenu() - self.subMenus = [] - for section, nodes in self.chart.library.getNodeTree().items(): - menu = QtGui.QMenu(section) - self.nodeMenu.addMenu(menu) - for name in nodes: - act = menu.addAction(name) - act.nodeType = name - act.pos = pos - self.subMenus.append(menu) + self.subMenus = [] + buildSubMenu(library.getNodeTree(), self.nodeMenu, self.subMenus, pos=pos) self.nodeMenu.triggered.connect(self.nodeMenuTriggered) return self.nodeMenu From bcfbe9b4ecd07245693a1b44c73b5d831dd71e0d Mon Sep 17 00:00:00 2001 From: John David Reaver Date: Wed, 22 Oct 2014 16:33:40 -0700 Subject: [PATCH 255/268] Fix PySide error when ViewBox signal destroyed Fixes issue #107 --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index ceca62c8..ec9c20fe 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1718,6 +1718,8 @@ class ViewBox(GraphicsWidget): pass except TypeError: ## view has already been deleted (?) pass + except AttributeError: # PySide has deleted signal + pass def locate(self, item, timeout=3.0, children=False): """ From 6c6ba8454afcd2362292d819888f003fbd78a75b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 25 Oct 2014 13:01:10 -0400 Subject: [PATCH 256/268] Added unit tests --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- .../graphicsItems/tests/ScatterPlotItem.py | 23 ----- pyqtgraph/graphicsItems/tests/ViewBox.py | 95 ------------------- .../tests/test_ScatterPlotItem.py | 54 +++++++++++ 4 files changed, 55 insertions(+), 119 deletions(-) delete mode 100644 pyqtgraph/graphicsItems/tests/ScatterPlotItem.py delete mode 100644 pyqtgraph/graphicsItems/tests/ViewBox.py create mode 100644 pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 7cb4c0de..d7eb2bfc 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -431,7 +431,7 @@ class ScatterPlotItem(GraphicsObject): if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): pens = args[0] - if if 'mask' in kargs and kargs['mask'] is not None: + if 'mask' in kargs and kargs['mask'] is not None: pens = pens[kargs['mask']] if len(pens) != len(dataSet): raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet))) diff --git a/pyqtgraph/graphicsItems/tests/ScatterPlotItem.py b/pyqtgraph/graphicsItems/tests/ScatterPlotItem.py deleted file mode 100644 index ef8271bf..00000000 --- a/pyqtgraph/graphicsItems/tests/ScatterPlotItem.py +++ /dev/null @@ -1,23 +0,0 @@ -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() diff --git a/pyqtgraph/graphicsItems/tests/ViewBox.py b/pyqtgraph/graphicsItems/tests/ViewBox.py deleted file mode 100644 index 91d9b617..00000000 --- a/pyqtgraph/graphicsItems/tests/ViewBox.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -ViewBox test cases: - -* call setRange then resize; requested range must be fully visible -* lockAspect works correctly for arbitrary aspect ratio -* autoRange works correctly with aspect locked -* call setRange with aspect locked, then resize -* AutoRange with all the bells and whistles - * item moves / changes transformation / changes bounds - * pan only - * fractional range - - -""" - -import pyqtgraph as pg -app = pg.mkQApp() - -imgData = pg.np.zeros((10, 10)) -imgData[0] = 3 -imgData[-1] = 3 -imgData[:,0] = 3 -imgData[:,-1] = 3 - -def testLinkWithAspectLock(): - global win, vb - win = pg.GraphicsWindow() - vb = win.addViewBox(name="image view") - vb.setAspectLocked() - vb.enableAutoRange(x=False, y=False) - p1 = win.addPlot(name="plot 1") - p2 = win.addPlot(name="plot 2", row=1, col=0) - win.ci.layout.setRowFixedHeight(1, 150) - win.ci.layout.setColumnFixedWidth(1, 150) - - def viewsMatch(): - r0 = pg.np.array(vb.viewRange()) - r1 = pg.np.array(p1.vb.viewRange()[1]) - r2 = pg.np.array(p2.vb.viewRange()[1]) - match = (abs(r0[1]-r1) <= (abs(r1) * 0.001)).all() and (abs(r0[0]-r2) <= (abs(r2) * 0.001)).all() - return match - - p1.setYLink(vb) - p2.setXLink(vb) - print "link views match:", viewsMatch() - win.show() - print "show views match:", viewsMatch() - img = pg.ImageItem(imgData) - vb.addItem(img) - vb.autoRange() - p1.plot(x=imgData.sum(axis=0), y=range(10)) - p2.plot(x=range(10), y=imgData.sum(axis=1)) - print "add items views match:", viewsMatch() - #p1.setAspectLocked() - #grid = pg.GridItem() - #vb.addItem(grid) - pg.QtGui.QApplication.processEvents() - pg.QtGui.QApplication.processEvents() - #win.resize(801, 600) - -def testAspectLock(): - global win, vb - win = pg.GraphicsWindow() - vb = win.addViewBox(name="image view") - vb.setAspectLocked() - img = pg.ImageItem(imgData) - vb.addItem(img) - - -#app.processEvents() -#print "init views match:", viewsMatch() -#p2.setYRange(-300, 300) -#print "setRange views match:", viewsMatch() -#app.processEvents() -#print "setRange views match (after update):", viewsMatch() - -#print "--lock aspect--" -#p1.setAspectLocked(True) -#print "lockAspect views match:", viewsMatch() -#p2.setYRange(-200, 200) -#print "setRange views match:", viewsMatch() -#app.processEvents() -#print "setRange views match (after update):", viewsMatch() - -#win.resize(100, 600) -#app.processEvents() -#vb.setRange(xRange=[-10, 10], padding=0) -#app.processEvents() -#win.resize(600, 100) -#app.processEvents() -#print vb.viewRange() - - -if __name__ == '__main__': - testLinkWithAspectLock() diff --git a/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py new file mode 100644 index 00000000..eb5e43c6 --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py @@ -0,0 +1,54 @@ +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_scatterplotitem(): + 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]) + + # Test uniform spot updates + s.setSize(10) + s.setBrush('r') + s.setPen('g') + s.setSymbol('+') + app.processEvents() + + # Test list spot updates + s.setSize([10] * 6) + s.setBrush([pg.mkBrush('r')] * 6) + s.setPen([pg.mkPen('g')] * 6) + s.setSymbol(['+'] * 6) + s.setPointData([s] * 6) + app.processEvents() + + # Test array spot updates + s.setSize(np.array([10] * 6)) + s.setBrush(np.array([pg.mkBrush('r')] * 6)) + s.setPen(np.array([pg.mkPen('g')] * 6)) + s.setSymbol(np.array(['+'] * 6)) + s.setPointData(np.array([s] * 6)) + app.processEvents() + + # Test per-spot updates + spot = s.points()[0] + spot.setSize(20) + spot.setBrush('b') + spot.setPen('g') + spot.setSymbol('o') + spot.setData(None) + + +if __name__ == '__main__': + test_scatterplotitem() From 2ac343ac37de1355d9834566e409d5013009a3f4 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Mon, 27 Oct 2014 18:06:31 -0700 Subject: [PATCH 257/268] fixed missing namespace. --- pyqtgraph/flowchart/Flowchart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 7b8cda33..ab5f4a82 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -836,7 +836,7 @@ class FlowchartWidget(dockarea.DockArea): act.pos = pos self.nodeMenu = QtGui.QMenu() self.subMenus = [] - buildSubMenu(library.getNodeTree(), self.nodeMenu, self.subMenus, pos=pos) + buildSubMenu(self.chart.library.getNodeTree(), self.nodeMenu, self.subMenus, pos=pos) self.nodeMenu.triggered.connect(self.nodeMenuTriggered) return self.nodeMenu From 2d78ce6f87b6a314fc57b5bdb08fb7d230795135 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Nov 2014 07:42:17 -0500 Subject: [PATCH 258/268] Fix attributeerror when using spinbox in parametertree --- pyqtgraph/widgets/SpinBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 23516827..1d8600c4 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -239,7 +239,7 @@ class SpinBox(QtGui.QAbstractSpinBox): Select the numerical portion of the text to allow quick editing by the user. """ le = self.lineEdit() - text = le.text() + text = asUnicode(le.text()) try: index = text.index(' ') except ValueError: From ad10b066529c37f73bace755558908cdda52d35d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Nov 2014 07:46:10 -0500 Subject: [PATCH 259/268] Correction for spinbox auto-selection without suffix --- pyqtgraph/widgets/SpinBox.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 1d8600c4..47101405 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -240,11 +240,14 @@ class SpinBox(QtGui.QAbstractSpinBox): """ le = self.lineEdit() text = asUnicode(le.text()) - try: - index = text.index(' ') - except ValueError: - return - le.setSelection(0, index) + if self.opts['suffix'] == '': + le.setSelection(0, len(text)) + else: + try: + index = text.index(' ') + except ValueError: + return + le.setSelection(0, index) def value(self): """ From 85d6c86c677998aa10d642e4d505c147a954529c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Nov 2014 08:06:18 -0500 Subject: [PATCH 260/268] Test submenu creation in example --- examples/FlowchartCustomNode.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/FlowchartCustomNode.py b/examples/FlowchartCustomNode.py index 54c56622..1cf1ba10 100644 --- a/examples/FlowchartCustomNode.py +++ b/examples/FlowchartCustomNode.py @@ -127,7 +127,10 @@ class UnsharpMaskNode(CtrlNode): ## NodeLibrary: library = fclib.LIBRARY.copy() # start with the default node set library.addNodeType(ImageViewNode, [('Display',)]) -library.addNodeType(UnsharpMaskNode, [('Image',)]) +# Add the unsharp mask node to two locations in the menu to demonstrate +# that we can create arbitrary menu structures +library.addNodeType(UnsharpMaskNode, [('Image',), + ('Submenu_test','submenu2','submenu3')]) fc.setLibrary(library) From 2bf4a0eb7b8ddba8eef0e84668a602a72404c050 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 24 Nov 2014 13:09:59 -0500 Subject: [PATCH 261/268] Workaround for Qt bug: wrap setSpacing and setContentsMargins from internal layout of GraphicsLayout. http://stackoverflow.com/questions/27092164/margins-in-pyqtgraphs-graphicslayout/27105642#27105642 --- pyqtgraph/graphicsItems/GraphicsLayout.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index b8325736..6ec38fb5 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -160,4 +160,12 @@ class GraphicsLayout(GraphicsWidget): for i in list(self.items.keys()): self.removeItem(i) + def setContentsMargins(self, *args): + # Wrap calls to layout. This should happen automatically, but there + # seems to be a Qt bug: + # http://stackoverflow.com/questions/27092164/margins-in-pyqtgraphs-graphicslayout + self.layout.setContentsMargins(*args) + def setSpacing(self, *args): + self.layout.setSpacing(*args) + \ No newline at end of file From f6ded808efc89cb65d51edd2257c5a204b856317 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 26 Nov 2014 21:25:17 -0500 Subject: [PATCH 262/268] Fixed a few exit crashes, added unit tests to cover them --- pyqtgraph/GraphicsScene/GraphicsScene.py | 4 +-- pyqtgraph/__init__.py | 19 ++++++++++ pyqtgraph/graphicsItems/HistogramLUTItem.py | 4 +-- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 6 ++-- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 ++ pyqtgraph/tests/test_exit_crash.py | 38 ++++++++++++++++++++ pyqtgraph/widgets/GraphicsView.py | 11 ++++-- 7 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 pyqtgraph/tests/test_exit_crash.py diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index c6afbe0f..6f5354dc 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -84,8 +84,8 @@ class GraphicsScene(QtGui.QGraphicsScene): cls._addressCache[sip.unwrapinstance(sip.cast(obj, QtGui.QGraphicsItem))] = obj - def __init__(self, clickRadius=2, moveDistance=5): - QtGui.QGraphicsScene.__init__(self) + def __init__(self, clickRadius=2, moveDistance=5, parent=None): + QtGui.QGraphicsScene.__init__(self, parent) self.setClickRadius(clickRadius) self.setMoveDistance(moveDistance) self.exportDirectory = None diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f8983455..d539e06b 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -270,7 +270,12 @@ from .Qt import isQObjectAlive ## Attempts to work around exit crashes: import atexit +_cleanupCalled = False def cleanup(): + global _cleanupCalled + if _cleanupCalled: + return + if not getConfigOption('exitCleanup'): return @@ -295,8 +300,22 @@ def cleanup(): s.addItem(o) except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object continue + _cleanupCalled = True + atexit.register(cleanup) +# Call cleanup when QApplication quits. This is necessary because sometimes +# the QApplication will quit before the atexit callbacks are invoked. +# Note: cannot connect this function until QApplication has been created, so +# instead we have GraphicsView.__init__ call this for us. +_cleanupConnected = False +def _connectCleanup(): + global _cleanupConnected + if _cleanupConnected: + return + QtGui.QApplication.instance().aboutToQuit.connect(cleanup) + _cleanupConnected = True + ## Optional function for exiting immediately (with some manual teardown) def exit(): diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 6a915902..89ebef3e 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -49,7 +49,7 @@ class HistogramLUTItem(GraphicsWidget): self.setLayout(self.layout) self.layout.setContentsMargins(1,1,1,1) self.layout.setSpacing(0) - self.vb = ViewBox() + self.vb = ViewBox(parent=self) self.vb.setMaximumWidth(152) self.vb.setMinimumWidth(45) self.vb.setMouseEnabled(x=False, y=True) @@ -59,7 +59,7 @@ class HistogramLUTItem(GraphicsWidget): self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) self.region.setZValue(1000) self.vb.addItem(self.region) - self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10) + self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self) self.layout.addItem(self.axis, 0, 0) self.layout.addItem(self.vb, 0, 1) self.layout.addItem(self.gradient, 0, 2) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index f8959e22..4f10b0e3 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -145,7 +145,7 @@ class PlotItem(GraphicsWidget): self.layout.setVerticalSpacing(0) if viewBox is None: - viewBox = ViewBox() + viewBox = ViewBox(parent=self) self.vb = viewBox self.vb.sigStateChanged.connect(self.viewStateChanged) self.setMenuEnabled(enableMenu, enableMenu) ## en/disable plotitem and viewbox menus @@ -168,14 +168,14 @@ class PlotItem(GraphicsWidget): axisItems = {} self.axes = {} for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): - axis = axisItems.get(k, AxisItem(orientation=k)) + axis = axisItems.get(k, AxisItem(orientation=k, parent=self)) axis.linkToView(self.vb) self.axes[k] = {'item': axis, 'pos': pos} self.layout.addItem(axis, *pos) axis.setZValue(-1000) axis.setFlag(axis.ItemNegativeZStacksBehindParent) - self.titleLabel = LabelItem('', size='11pt') + self.titleLabel = LabelItem('', size='11pt', parent=self) self.layout.addItem(self.titleLabel, 0, 1) self.setTitle(None) ## hide diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index ec9c20fe..900c2038 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1696,6 +1696,8 @@ class ViewBox(GraphicsWidget): def forgetView(vid, name): if ViewBox is None: ## can happen as python is shutting down return + if QtGui.QApplication.instance() is None: + return ## Called with ID and name of view (the view itself is no longer available) for v in list(ViewBox.AllViews.keys()): if id(v) == vid: diff --git a/pyqtgraph/tests/test_exit_crash.py b/pyqtgraph/tests/test_exit_crash.py new file mode 100644 index 00000000..69181f21 --- /dev/null +++ b/pyqtgraph/tests/test_exit_crash.py @@ -0,0 +1,38 @@ +import os, sys, subprocess, tempfile +import pyqtgraph as pg + + +code = """ +import sys +sys.path.insert(0, '{path}') +import pyqtgraph as pg +app = pg.mkQApp() +w = pg.{classname}({args}) +""" + + +def test_exit_crash(): + # For each Widget subclass, run a simple python script that creates an + # instance and then shuts down. The intent is to check for segmentation + # faults when each script exits. + tmp = tempfile.mktemp(".py") + path = os.path.dirname(pg.__file__) + + initArgs = { + 'CheckTable': "[]", + 'ProgressDialog': '"msg"', + 'VerticalLabel': '"msg"', + } + + for name in dir(pg): + obj = getattr(pg, name) + if not isinstance(obj, type) or not issubclass(obj, pg.QtGui.QWidget): + continue + + print name + argstr = initArgs.get(name, "") + open(tmp, 'w').write(code.format(path=path, classname=name, args=argstr)) + proc = subprocess.Popen([sys.executable, tmp]) + assert proc.wait() == 0 + + os.remove(tmp) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 3273ac60..4062be94 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -71,6 +71,13 @@ class GraphicsView(QtGui.QGraphicsView): QtGui.QGraphicsView.__init__(self, parent) + # This connects a cleanup function to QApplication.aboutToQuit. It is + # called from here because we have no good way to react when the + # QApplication is created by the user. + # See pyqtgraph.__init__.py + from .. import _connectCleanup + _connectCleanup() + if useOpenGL is None: useOpenGL = getConfigOption('useOpenGL') @@ -102,7 +109,8 @@ class GraphicsView(QtGui.QGraphicsView): self.currentItem = None self.clearMouse() self.updateMatrix() - self.sceneObj = GraphicsScene() + # GraphicsScene must have parent or expect crashes! + self.sceneObj = GraphicsScene(parent=self) self.setScene(self.sceneObj) ## Workaround for PySide crash @@ -143,7 +151,6 @@ class GraphicsView(QtGui.QGraphicsView): def paintEvent(self, ev): self.scene().prepareForPaint() - #print "GV: paint", ev.rect() return QtGui.QGraphicsView.paintEvent(self, ev) def render(self, *args, **kwds): From f90565442c517c4251ee72320b355644c682fa31 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 Dec 2014 16:39:41 -0500 Subject: [PATCH 263/268] Correction in setup.py: do not raise exception if install location does not exist yet. --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index ea560959..4b5cab92 100644 --- a/setup.py +++ b/setup.py @@ -101,11 +101,12 @@ class Install(distutils.command.install.install): """ def run(self): name = self.config_vars['dist_name'] - if name in os.listdir(self.install_libbase): + path = self.install_libbase + if os.path.exists(path) and name in os.listdir(path): raise Exception("It appears another version of %s is already " "installed at %s; remove this before installing." - % (name, self.install_libbase)) - print("Installing to %s" % self.install_libbase) + % (name, path)) + print("Installing to %s" % path) return distutils.command.install.install.run(self) setup( From 41fa2f64d332a9e3d1b61fd366fa899383493618 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 4 Dec 2014 21:24:09 -0500 Subject: [PATCH 264/268] Fixed GL picking bug --- pyqtgraph/opengl/GLGraphicsItem.py | 5 +++++ pyqtgraph/opengl/GLViewWidget.py | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/opengl/GLGraphicsItem.py b/pyqtgraph/opengl/GLGraphicsItem.py index cdfaa683..12c5b707 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -28,8 +28,13 @@ GLOptions = { class GLGraphicsItem(QtCore.QObject): + _nextId = 0 + def __init__(self, parentItem=None): QtCore.QObject.__init__(self) + self._id = GLGraphicsItem._nextId + GLGraphicsItem._nextId += 1 + self.__parent = None self.__view = None self.__children = set() diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 788ab725..992aa73e 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -159,7 +159,6 @@ class GLViewWidget(QtOpenGL.QGLWidget): items = [(h.near, h.names[0]) for h in hits] items.sort(key=lambda i: i[0]) - return [self._itemNames[i[1]] for i in items] def paintGL(self, region=None, viewport=None, useItemNames=False): @@ -193,8 +192,8 @@ class GLViewWidget(QtOpenGL.QGLWidget): try: glPushAttrib(GL_ALL_ATTRIB_BITS) if useItemNames: - glLoadName(id(i)) - self._itemNames[id(i)] = i + glLoadName(i._id) + self._itemNames[i._id] = i i.paint() except: from .. import debug From f7a54ffd42f55cfcab866967babdd95b1d3f4a73 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 14 Dec 2014 20:01:00 -0500 Subject: [PATCH 265/268] Release 0.9.9 --- doc/source/conf.py | 4 ++-- pyqtgraph/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index bf35651d..604ea549 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -50,9 +50,9 @@ copyright = '2011, Luke Campagnola' # built documents. # # The short X.Y version. -version = '0.9.8' +version = '0.9.9' # The full version, including alpha/beta/rc tags. -release = '0.9.8' +release = '0.9.9' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index d539e06b..0f5333f0 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -4,7 +4,7 @@ PyQtGraph - Scientific Graphics and GUI Library for Python www.pyqtgraph.org """ -__version__ = '0.9.8' +__version__ = '0.9.9' ### import all the goodies and add some helper functions for easy CLI use From 9a951318be9a78061a76332349f6d3968813b751 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 22 Dec 2014 18:29:09 -0500 Subject: [PATCH 266/268] Add example subpackages to setup.py --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4b5cab92..f1f46f71 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,8 @@ 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'] +allPackages = (helpers.listAllPackages(pkgroot='pyqtgraph') + + ['pyqtgraph.'+x for x in helpers.listAllPackages(pkgroot='examples')]) ## Decide what version string to use in the build version, forcedVersion, gitVersion, initVersion = helpers.getVersionStrings(pkg='pyqtgraph') From 77906fc7a20917bed2a8fe025e160d2fe2c703db Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 23 Dec 2014 15:55:52 -0500 Subject: [PATCH 267/268] corrections to manifest Add pure-python integrator to verlet chain example --- MANIFEST.in | 2 +- examples/verlet_chain/chain.py | 13 ++++-- examples/verlet_chain/maths.so | Bin 8017 -> 0 bytes examples/verlet_chain/relax.py | 79 ++++++++++++++++++++++++++------- examples/verlet_chain_demo.py | 33 ++++++++++---- 5 files changed, 97 insertions(+), 30 deletions(-) delete mode 100755 examples/verlet_chain/maths.so diff --git a/MANIFEST.in b/MANIFEST.in index c6667d04..86ae0f60 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ recursive-include pyqtgraph *.py *.ui *.m README *.txt recursive-include tests *.py *.ui -recursive-include examples *.py *.ui +recursive-include examples *.py *.ui *.gz *.cfg recursive-include doc *.rst *.py *.svg *.png *.jpg recursive-include doc/build/html * recursive-include tools * diff --git a/examples/verlet_chain/chain.py b/examples/verlet_chain/chain.py index 896505ac..6eb3501a 100644 --- a/examples/verlet_chain/chain.py +++ b/examples/verlet_chain/chain.py @@ -1,7 +1,7 @@ import pyqtgraph as pg import numpy as np import time -from .relax import relax +from . import relax class ChainSim(pg.QtCore.QObject): @@ -52,7 +52,7 @@ class ChainSim(pg.QtCore.QObject): self.mrel1[self.fixed[l2]] = 0 self.mrel2 = 1.0 - self.mrel1 - for i in range(100): + for i in range(10): self.relax(n=10) self.initialized = True @@ -75,6 +75,10 @@ class ChainSim(pg.QtCore.QObject): else: dt = now - self.lasttime self.lasttime = now + + # limit amount of work to be done between frames + if not relax.COMPILED: + dt = self.maxTimeStep if self.lastpos is None: self.lastpos = self.pos @@ -103,8 +107,9 @@ class ChainSim(pg.QtCore.QObject): def relax(self, n=50): - # speed up with C magic - relax(self.pos, self.links, self.mrel1, self.mrel2, self.lengths, self.push, self.pull, n) + # speed up with C magic if possible + relax.relax(self.pos, self.links, self.mrel1, self.mrel2, self.lengths, self.push, self.pull, n) self.relaxed.emit() + diff --git a/examples/verlet_chain/maths.so b/examples/verlet_chain/maths.so deleted file mode 100755 index 62aff3214ec02e8d87bef57c290d33d7efc7f472..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8017 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 lengths)) + ##dist[mask] = lengths[mask] + #change = (lengths-dist) / dist + #change[mask] = 0 + + #dx *= change[:, np.newaxis] + #print dx + + ##pos[p1] -= mrel2 * dx + ##pos[p2] += mrel1 * dx + #for j in range(links.shape[0]): + #pos[links[j,0]] -= mrel2[j] * dx[j] + #pos[links[j,1]] += mrel1[j] * dx[j] + + + for l in range(links.shape[0]): + p1, p2 = links[l]; + x1 = pos[p1] + x2 = pos[p2] + + dx = x2 - x1 + dist2 = (dx**2).sum() + + if (push[l] and dist2 < lengths2[l]) or (pull[l] and dist2 > lengths2[l]): + dist = dist2 ** 0.5 + change = (lengths[l]-dist) / dist + dx *= change + pos[p1] -= mrel2[l] * dx + pos[p2] += mrel1[l] * dx diff --git a/examples/verlet_chain_demo.py b/examples/verlet_chain_demo.py index 6ed97d48..1197344d 100644 --- a/examples/verlet_chain_demo.py +++ b/examples/verlet_chain_demo.py @@ -1,26 +1,38 @@ """ Mechanical simulation of a chain using verlet integration. +Use the mouse to interact with one of the chains. +By default, this uses a slow, pure-python integrator to solve the chain link +positions. Unix users may compile a small math library to speed this up by +running the `examples/verlet_chain/make` script. """ + import initExample ## Add path to library (just for examples; you do not need this) import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -from verlet_chain import ChainSim +import verlet_chain -sim = ChainSim() +sim = verlet_chain.ChainSim() - -chlen1 = 80 -chlen2 = 60 +if verlet_chain.relax.COMPILED: + # Use more complex chain if compiled mad library is available. + chlen1 = 80 + chlen2 = 60 + linklen = 1 +else: + chlen1 = 10 + chlen2 = 8 + linklen = 8 + npts = chlen1 + chlen2 sim.mass = np.ones(npts) -sim.mass[chlen1-15] = 100 +sim.mass[int(chlen1 * 0.8)] = 100 sim.mass[chlen1-1] = 500 sim.mass[npts-1] = 200 @@ -31,8 +43,10 @@ sim.fixed[chlen1] = True sim.pos = np.empty((npts, 2)) sim.pos[:chlen1, 0] = 0 sim.pos[chlen1:, 0] = 10 -sim.pos[:chlen1, 1] = np.arange(chlen1) -sim.pos[chlen1:, 1] = np.arange(chlen2) +sim.pos[:chlen1, 1] = np.arange(chlen1) * linklen +sim.pos[chlen1:, 1] = np.arange(chlen2) * linklen +# to prevent miraculous balancing acts: +sim.pos += np.random.normal(size=sim.pos.shape, scale=1e-3) links1 = [(j, i+j+1) for i in range(chlen1) for j in range(chlen1-i-1)] links2 = [(j, i+j+1) for i in range(chlen2) for j in range(chlen2-i-1)] @@ -55,7 +69,8 @@ sim.push = np.concatenate([push1, push2, np.array([True], dtype=bool)]) sim.pull = np.ones(sim.links.shape[0], dtype=bool) sim.pull[-1] = False -mousepos = sim.pos[0] +# move chain initially just to generate some motion if the mouse is not over the window +mousepos = np.array([30, 20]) def display(): From 930c3a1c40c7430174b8e191086701522599a7dd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 23 Dec 2014 16:39:37 -0500 Subject: [PATCH 268/268] Add example data files to setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f1f46f71..4c1a6aca 100644 --- a/setup.py +++ b/setup.py @@ -121,7 +121,7 @@ setup( 'style': helpers.StyleCommand}, packages=allPackages, package_dir={'pyqtgraph.examples': 'examples'}, ## install examples along with the rest of the source - #package_data={'pyqtgraph': ['graphicsItems/PlotItem/*.png']}, + package_data={'pyqtgraph.examples': ['optics/*.gz', 'relativity/presets/*.cfg']}, install_requires = [ 'numpy', ],

-# You can also use any pixel decoder supported by PIL. For more -# information on available decoders, see the section Writing Your Own File Decoder. -#