From 9a9fc15873b1c9cc32a106f6c0b40b2786e55ff6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 12 Jan 2013 14:31:49 -0500 Subject: [PATCH 01/22] Bugfixes: - GraphicsItem.pixelVectors copies cached results before returning - Multiprocess fixes for Windows: - mmap/shm uses anonymous maps rather than tempfiles - avoid use of getppid and setpgrp - work around hmac authentication bug (use os.urandom to generate key) --- pyqtgraph/__init__.py | 4 +- pyqtgraph/functions.py | 9 ++-- pyqtgraph/graphicsItems/GraphicsItem.py | 6 +-- pyqtgraph/graphicsItems/ROI.py | 3 +- pyqtgraph/multiprocess/bootstrap.py | 8 ++-- pyqtgraph/multiprocess/processes.py | 23 ++++++---- pyqtgraph/widgets/RemoteGraphicsView.py | 59 ++++++++++++++++--------- 7 files changed, 71 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 93d9f7b8..c35992a2 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.5' ### import all the goodies and add some helper functions for easy CLI use @@ -49,6 +49,8 @@ CONFIG_OPTIONS = { 'background': (0, 0, 0), ## 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 + 'weaveDebug': False, ## Print full error message if weave compile fails } diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 23b2580c..00107927 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -23,6 +23,7 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' from .Qt import QtGui, QtCore, USE_PYSIDE +from pyqtgraph import getConfigOption import numpy as np import decimal, re import ctypes @@ -30,9 +31,10 @@ import ctypes try: import scipy.ndimage HAVE_SCIPY = True + WEAVE_DEBUG = getConfigOption('weaveDebug') try: import scipy.weave - USE_WEAVE = True + USE_WEAVE = getConfigOption('useWeave') except: USE_WEAVE = False except ImportError: @@ -631,7 +633,8 @@ def rescaleData(data, scale, offset, dtype=None): data = newData.reshape(data.shape) except: if USE_WEAVE: - debug.printExc("Error; disabling weave.") + if WEAVE_DEBUG: + debug.printExc("Error; disabling weave.") USE_WEAVE = False #p = np.poly1d([scale, -offset*scale]) @@ -1871,4 +1874,4 @@ def pseudoScatter(data, spacing=None, shuffle=True): yvals[i] = y - return yvals[np.argsort(inds)] ## un-shuffle values before returning \ No newline at end of file + return yvals[np.argsort(inds)] ## un-shuffle values before returning diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 2314709d..c90821a3 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -197,14 +197,14 @@ class GraphicsItem(object): ## check local cache if direction is None and dt == self._pixelVectorCache[0]: - return self._pixelVectorCache[1] + return map(Point, self._pixelVectorCache[1]) ## return a *copy* ## check global cache key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32()) pv = self._pixelVectorGlobalCache.get(key, None) if pv is not None: self._pixelVectorCache = [dt, pv] - return pv + return map(Point,pv) ## return a *copy* if direction is None: @@ -547,4 +547,4 @@ class GraphicsItem(object): #def update(self): #self._qtBaseClass.update(self) - #print "Update:", self \ No newline at end of file + #print "Update:", self diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index e3f094ff..c3620edb 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1783,8 +1783,7 @@ class LineSegmentROI(ROI): dh = h2-h1 if dh.length() == 0: return p - pxv = self.pixelVectors(h2-h1)[1] - + pxv = self.pixelVectors(dh)[1] if pxv is None: return p diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py index e0d1c02c..6ac9fce4 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -2,8 +2,10 @@ import sys, pickle, os if __name__ == '__main__': - os.setpgrp() ## prevents signals (notably keyboard interrupt) being forwarded from parent to this process - name, port, authkey, targetStr, path = pickle.load(sys.stdin) + if hasattr(os, 'setpgrp'): + os.setpgrp() ## prevents signals (notably keyboard interrupt) being forwarded from parent to this process + name, port, authkey, ppid, targetStr, path = pickle.load(sys.stdin) + #print "key:", ' '.join([str(ord(x)) for x in authkey]) if path is not None: ## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list. while len(sys.path) > 0: @@ -12,5 +14,5 @@ if __name__ == '__main__': #import pyqtgraph #import pyqtgraph.multiprocess.processes target = pickle.loads(targetStr) ## unpickling the target should import everything we need - target(name, port, authkey) + target(name, port, authkey, ppid) sys.exit(0) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index f95a3ec4..1322e78e 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -54,12 +54,13 @@ class Process(RemoteEventHandler): executable = sys.executable ## random authentication key - authkey = ''.join([chr(random.getrandbits(7)) for i in range(20)]) - + authkey = os.urandom(20) + #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: + ## hmac authentication appears to be broken on windows (says AuthenticationError: digest received was wrong) l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey) break except socket.error as ex: @@ -73,7 +74,8 @@ class Process(RemoteEventHandler): self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE) targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to ## set its sys.path properly before unpickling the target - pickle.dump((name+'_child', port, authkey, targetStr, sysPath), self.proc.stdin) + pid = os.getpid() # we must sent pid to child because windows does not have getppid + pickle.dump((name+'_child', port, authkey, pid, targetStr, sysPath), self.proc.stdin) self.proc.stdin.close() ## open connection for remote process @@ -92,10 +94,11 @@ class Process(RemoteEventHandler): time.sleep(0.05) -def startEventLoop(name, port, authkey): +def startEventLoop(name, port, authkey, ppid): conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) global HANDLER - HANDLER = RemoteEventHandler(conn, name, os.getppid()) + #ppid = 0 if not hasattr(os, 'getppid') else os.getppid() + HANDLER = RemoteEventHandler(conn, name, ppid) while True: try: HANDLER.processRequests() # exception raised when the loop should exit @@ -161,6 +164,7 @@ class ForkedProcess(RemoteEventHandler): proxyId = LocalObjectProxy.registerObject(v) proxyIDs[k] = proxyId + ppid = os.getpid() # write this down now; windows doesn't have getppid pid = os.fork() if pid == 0: self.isParent = False @@ -200,9 +204,9 @@ class ForkedProcess(RemoteEventHandler): if 'random' in sys.modules: sys.modules['random'].seed(os.getpid() ^ int(time.time()*10000%10000)) - RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=os.getppid()) + #ppid = 0 if not hasattr(os, 'getppid') else os.getppid() + RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=ppid) - ppid = os.getppid() self.forkedProxies = {} for name, proxyId in proxyIDs.iteritems(): self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name])) @@ -318,7 +322,7 @@ class QtProcess(Process): except ClosedError: self.timer.stop() -def startQtEventLoop(name, port, authkey): +def startQtEventLoop(name, port, authkey, ppid): conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) from pyqtgraph.Qt import QtGui, QtCore #from PyQt4 import QtGui, QtCore @@ -330,7 +334,8 @@ def startQtEventLoop(name, port, authkey): ## until it is explicitly closed by the parent process. global HANDLER - HANDLER = RemoteQtEventHandler(conn, name, os.getppid()) + #ppid = 0 if not hasattr(os, 'getppid') else os.getppid() + HANDLER = RemoteQtEventHandler(conn, name, ppid) HANDLER.startEventTimer() app.exec_() diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index d8e720b5..bea5a02d 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -3,7 +3,7 @@ import pyqtgraph.multiprocess as mp import pyqtgraph as pg from .GraphicsView import GraphicsView import numpy as np -import mmap, tempfile, ctypes, atexit +import mmap, tempfile, ctypes, atexit, sys, random __all__ = ['RemoteGraphicsView'] @@ -30,10 +30,12 @@ class RemoteGraphicsView(QtGui.QWidget): self.setFocusPolicy(self._view.focusPolicy()) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.setMouseTracking(True) - + self.shm = None shmFileName = self._view.shmFileName() - self.shmFile = open(shmFileName, 'r') - self.shm = mmap.mmap(self.shmFile.fileno(), mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_READ) + if 'win' in sys.platform: + self.shmtag = shmFileName + else: + self.shmFile = open(shmFileName, 'r') self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) #, callSync='off')) ## Note: we need synchronous signals @@ -53,11 +55,16 @@ class RemoteGraphicsView(QtGui.QWidget): return QtCore.QSize(*self._sizeHint) def remoteSceneChanged(self, data): - w, h, size = data + w, h, size, newfile = data #self._sizeHint = (whint, hhint) - if self.shm.size != size: - self.shm.close() - self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) + if self.shm is None or self.shm.size != size: + if self.shm is not None: + self.shm.close() + if 'win' in sys.platform: + self.shmtag = newfile ## on windows, we create a new tag for every resize + self.shm = mmap.mmap(-1, size, self.shmtag) ## can't use tmpfile on windows because the file can only be opened once. + else: + self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) self.shm.seek(0) self._img = QtGui.QImage(self.shm.read(w*h*4), w, h, QtGui.QImage.Format_ARGB32) self.update() @@ -112,13 +119,14 @@ class Renderer(GraphicsView): def __init__(self, *args, **kwds): ## Create shared memory for rendered image - #fd = os.open('/tmp/mmaptest', os.O_CREAT | os.O_TRUNC | os.O_RDWR) - #os.write(fd, '\x00' * mmap.PAGESIZE) - self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_') - self.shmFile.write('\x00' * mmap.PAGESIZE) - #fh.flush() - fd = self.shmFile.fileno() - self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE) + if 'win' in sys.platform: + self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)]) + self.shm = mmap.mmap(-1, mmap.PAGESIZE, self.shmtag) # use anonymous mmap on windows + else: + self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_') + self.shmFile.write('\x00' * mmap.PAGESIZE) + fd = self.shmFile.fileno() + self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE) atexit.register(self.close) GraphicsView.__init__(self, *args, **kwds) @@ -130,10 +138,14 @@ class Renderer(GraphicsView): def close(self): self.shm.close() - self.shmFile.close() + if 'win' not in sys.platform: + self.shmFile.close() def shmFileName(self): - return self.shmFile.name + if 'win' in sys.platform: + return self.shmtag + else: + return self.shmFile.name def update(self): self.img = None @@ -152,7 +164,14 @@ class Renderer(GraphicsView): return size = self.width() * self.height() * 4 if size > self.shm.size(): - self.shm.resize(size) + if 'win' in sys.platform: + ## windows says "WindowsError: [Error 87] the parameter is incorrect" if we try to resize the mmap + self.shm.close() + ## it also says (sometimes) 'access is denied' if we try to reuse the tag. + self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)]) + self.shm = mmap.mmap(-1, size, self.shmtag) + else: + self.shm.resize(size) address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0)) ## render the scene directly to shared memory @@ -161,7 +180,7 @@ class Renderer(GraphicsView): p = QtGui.QPainter(self.img) self.render(p, self.viewRect(), self.rect()) p.end() - self.sceneRendered.emit((self.width(), self.height(), self.shm.size())) + self.sceneRendered.emit((self.width(), self.height(), self.shm.size(), self.shmFileName())) def mousePressEvent(self, typ, pos, gpos, btn, btns, mods): typ = QtCore.QEvent.Type(typ) @@ -202,4 +221,4 @@ class Renderer(GraphicsView): - \ No newline at end of file + From c5dd0f4f634047b2ff85d8e1268197d82283609f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 12 Jan 2013 14:35:32 -0500 Subject: [PATCH 02/22] Fixed print statements for python 3 --- examples/__main__.py | 1 + examples/multiprocess.py | 32 +++++++++++++------------- examples/parallelize.py | 12 +++++----- pyqtgraph/debug.py | 8 +++---- pyqtgraph/exporters/SVGExporter.py | 2 +- pyqtgraph/flowchart/library/Data.py | 2 +- pyqtgraph/functions.py | 2 +- pyqtgraph/multiprocess/parallelizer.py | 4 ++-- pyqtgraph/multiprocess/processes.py | 4 ++-- pyqtgraph/multiprocess/remoteproxy.py | 22 +++++++++--------- pyqtgraph/opengl/glInfo.py | 8 +++---- pyqtgraph/reload.py | 12 +++++----- 12 files changed, 55 insertions(+), 54 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index 87673208..d8456781 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -68,6 +68,7 @@ examples = OrderedDict([ ('GraphicsScene', 'GraphicsScene.py'), ('Flowcharts', 'Flowchart.py'), + ('Custom Flowchart Nodes', 'FlowchartCustomNode.py'), #('Canvas', '../canvas'), #('MultiPlotWidget', 'MultiPlotWidget.py'), ]) diff --git a/examples/multiprocess.py b/examples/multiprocess.py index f6756345..ba550f7f 100644 --- a/examples/multiprocess.py +++ b/examples/multiprocess.py @@ -8,32 +8,32 @@ import time -print "\n=================\nStart Process" +print("\n=================\nStart Process") proc = mp.Process() import os -print "parent:", os.getpid(), "child:", proc.proc.pid -print "started" +print("parent:", os.getpid(), "child:", proc.proc.pid) +print("started") rnp = proc._import('numpy') arr = rnp.array([1,2,3,4]) -print repr(arr) -print str(arr) -print "return value:", repr(arr.mean(_returnType='value')) -print "return proxy:", repr(arr.mean(_returnType='proxy')) -print "return auto: ", repr(arr.mean(_returnType='auto')) +print(repr(arr)) +print(str(arr)) +print("return value:", repr(arr.mean(_returnType='value'))) +print( "return proxy:", repr(arr.mean(_returnType='proxy'))) +print( "return auto: ", repr(arr.mean(_returnType='auto'))) proc.join() -print "process finished" +print( "process finished") -print "\n=================\nStart ForkedProcess" +print( "\n=================\nStart ForkedProcess") proc = mp.ForkedProcess() rnp = proc._import('numpy') arr = rnp.array([1,2,3,4]) -print repr(arr) -print str(arr) -print repr(arr.mean()) +print( repr(arr)) +print( str(arr)) +print( repr(arr.mean())) proc.join() -print "process finished" +print( "process finished") @@ -42,10 +42,10 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui app = pg.QtGui.QApplication([]) -print "\n=================\nStart QtProcess" +print( "\n=================\nStart QtProcess") import sys if (sys.flags.interactive != 1): - print " (not interactive; remote process will exit immediately.)" + print( " (not interactive; remote process will exit immediately.)") proc = mp.QtProcess() d1 = proc.transfer(np.random.normal(size=1000)) d2 = proc.transfer(np.random.normal(size=1000)) diff --git a/examples/parallelize.py b/examples/parallelize.py index d2ba0ce0..768d6f00 100644 --- a/examples/parallelize.py +++ b/examples/parallelize.py @@ -5,7 +5,7 @@ import pyqtgraph.multiprocess as mp import pyqtgraph as pg import time -print "\n=================\nParallelize" +print( "\n=================\nParallelize") ## Do a simple task: ## for x in range(N): @@ -36,7 +36,7 @@ with pg.ProgressDialog('processing serially..', maximum=len(tasks)) as dlg: dlg += 1 if dlg.wasCanceled(): raise Exception('processing canceled') -print "Serial time: %0.2f" % (time.time() - start) +print( "Serial time: %0.2f" % (time.time() - start)) ### Use parallelize, but force a single worker ### (this simulates the behavior seen on windows, which lacks os.fork) @@ -47,8 +47,8 @@ with mp.Parallelize(enumerate(tasks), results=results2, workers=1, progressDialo for j in xrange(size): tot += j * x tasker.results[i] = tot -print "\nParallel time, 1 worker: %0.2f" % (time.time() - start) -print "Results match serial: ", results2 == results +print( "\nParallel time, 1 worker: %0.2f" % (time.time() - start)) +print( "Results match serial: %s" % str(results2 == results)) ### Use parallelize with multiple workers start = time.time() @@ -58,6 +58,6 @@ with mp.Parallelize(enumerate(tasks), results=results3, progressDialog='processi for j in xrange(size): tot += j * x tasker.results[i] = tot -print "\nParallel time, %d workers: %0.2f" % (mp.Parallelize.suggestedWorkerCount(), time.time() - start) -print "Results match serial: ", results3 == results +print( "\nParallel time, %d workers: %0.2f" % (mp.Parallelize.suggestedWorkerCount(), time.time() - start)) +print( "Results match serial: %s" % str(results3 == results)) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index d5f86139..7fa169a4 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -393,7 +393,7 @@ class Profiler: if self.delayed: self.msgs.append(msg2) else: - print msg2 + print(msg2) self.t0 = ptime.time() self.t1 = self.t0 @@ -410,7 +410,7 @@ class Profiler: if self.delayed: self.msgs.append(msg2) else: - print msg2 + print(msg2) self.t1 = ptime.time() ## don't measure time it took to print def finish(self, msg=None): @@ -425,10 +425,10 @@ class Profiler: self.msgs.append(msg) if self.depth == 0: for line in self.msgs: - print line + print(line) Profiler.msgs = [] else: - print msg + print(msg) Profiler.depth = self.depth self.finished = True diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 70f1f632..587282e0 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -220,7 +220,7 @@ def _generateItemSvg(item, nodes=None, root=None): ## get list of sub-groups g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g'] except: - print doc.toxml() + print(doc.toxml()) raise diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py index 85ab6232..1c612e08 100644 --- a/pyqtgraph/flowchart/library/Data.py +++ b/pyqtgraph/flowchart/library/Data.py @@ -236,7 +236,7 @@ class EvalNode(Node): text = fn + "\n".join([" "+l for l in str(self.text.toPlainText()).split('\n')]) + run exec(text) except: - print "Error processing node:", self.name() + print("Error processing node: %s" % self.name()) raise return output diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 00107927..27283631 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -798,7 +798,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): if levels.shape != (data.shape[-1], 2): raise Exception('levels must have shape (data.shape[-1], 2)') else: - print levels + print(levels) raise Exception("levels argument must be 1D or 2D.") #levels = np.array(levels) #if levels.shape == (2,): diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index 2d03c000..c805cfdb 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -19,7 +19,7 @@ class Parallelize(object): for task in tasks: result = processTask(task) results.append(result) - print results + print(results) ## Here is the parallelized version: @@ -30,7 +30,7 @@ class Parallelize(object): for task in tasker: result = processTask(task) tasker.results.append(result) - print results + print(results) The only major caveat is that *result* in the example above must be picklable, diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 1322e78e..1103ef15 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -232,7 +232,7 @@ class ForkedProcess(RemoteEventHandler): except ClosedError: break except: - print "Error occurred in forked event loop:" + print("Error occurred in forked event loop:") sys.excepthook(*sys.exc_info()) sys.exit(0) @@ -297,7 +297,7 @@ class QtProcess(Process): btn.show() def slot(): - print 'slot invoked on parent process' + print('slot invoked on parent process') btn.clicked.connect(proxy(slot)) # be sure to send a proxy of the slot """ diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 94cc6048..887d2e87 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -68,7 +68,7 @@ class RemoteEventHandler(object): try: return cls.handlers[pid] except: - print pid, cls.handlers + print(pid, cls.handlers) raise def getProxyOption(self, opt): @@ -103,7 +103,7 @@ class RemoteEventHandler(object): else: raise except: - print "Error in process %s" % self.name + print("Error in process %s" % self.name) sys.excepthook(*sys.exc_info()) return numProcessed @@ -239,7 +239,7 @@ class RemoteEventHandler(object): self.send(request='result', reqId=reqId, callSync='off', opts=dict(result=result)) def replyError(self, reqId, *exc): - print "error:", self.name, reqId, exc[1] + print("error: %s %s %s" % (self.name, str(reqId), str(exc[1]))) excStr = traceback.format_exception(*exc) try: self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=exc[1], excString=excStr)) @@ -352,9 +352,9 @@ class RemoteEventHandler(object): try: optStr = pickle.dumps(opts) except: - print "==== Error pickling this object: ====" - print opts - print "=======================================" + print("==== Error pickling this object: ====") + print(opts) + print("=======================================") raise nByteMsgs = 0 @@ -404,12 +404,12 @@ class RemoteEventHandler(object): #print ''.join(result) exc, excStr = result if exc is not None: - print "===== Remote process raised exception on request: =====" - print ''.join(excStr) - print "===== Local Traceback to request follows: =====" + print("===== Remote process raised exception on request: =====") + print(''.join(excStr)) + print("===== Local Traceback to request follows: =====") raise exc else: - print ''.join(excStr) + print(''.join(excStr)) raise Exception("Error getting result. See above for exception from remote process.") else: @@ -535,7 +535,7 @@ class Request(object): raise ClosedError() time.sleep(0.005) if timeout >= 0 and time.time() - start > timeout: - print "Request timed out:", self.description + print("Request timed out: %s" % self.description) import traceback traceback.print_stack() raise NoResultError() diff --git a/pyqtgraph/opengl/glInfo.py b/pyqtgraph/opengl/glInfo.py index 95f59630..28da1f69 100644 --- a/pyqtgraph/opengl/glInfo.py +++ b/pyqtgraph/opengl/glInfo.py @@ -6,10 +6,10 @@ class GLTest(QtOpenGL.QGLWidget): def __init__(self): QtOpenGL.QGLWidget.__init__(self) self.makeCurrent() - print "GL version:", glGetString(GL_VERSION) - print "MAX_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_TEXTURE_SIZE) - print "MAX_3D_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE) - print "Extensions:", glGetString(GL_EXTENSIONS) + print("GL version:" + glGetString(GL_VERSION)) + print("MAX_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_TEXTURE_SIZE)) + print("MAX_3D_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE)) + print("Extensions: " + glGetString(GL_EXTENSIONS)) GLTest() diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index b9459073..ccf83913 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -267,14 +267,14 @@ class A(object): object.__init__(self) self.msg = msg def fn(self, pfx = ""): - print pfx+"A class:", self.__class__, id(self.__class__) - print pfx+" %%s: %d" %% self.msg + print(pfx+"A class: %%s %%s" %% (str(self.__class__), str(id(self.__class__)))) + print(pfx+" %%s: %d" %% self.msg) class B(A): def fn(self, pfx=""): - print pfx+"B class:", self.__class__, id(self.__class__) - print pfx+" %%s: %d" %% self.msg - print pfx+" calling superclass.. (%%s)" %% id(A) + print(pfx+"B class:", self.__class__, id(self.__class__)) + print(pfx+" %%s: %d" %% self.msg) + print(pfx+" calling superclass.. (%%s)" %% id(A) ) A.fn(self, " ") """ @@ -294,7 +294,7 @@ class C(A): A.__init__(self, msg + "(init from C)") def fn(): - print "fn: %s" + print("fn: %s") """ open(modFile1, 'w').write(modCode1%(1,1)) From 296b709550a39cf1a99a07fcb0fd662aa1e310c4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 12 Jan 2013 14:39:23 -0500 Subject: [PATCH 03/22] import numpy in pyqtgraph.__init__ to avoid confusing import errors --- pyqtgraph/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index c35992a2..da197aa6 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -16,6 +16,9 @@ from .Qt import QtGui #if QtGui.QApplication.instance() is None: #app = QtGui.QApplication([]) +import numpy ## pyqtgraph requires numpy + ## (import here to avoid massive error dump later on if numpy is not available) + import os, sys ## check python version From 4dc9b838167b6f96f869fb7437c1ed2220ac47a4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 12 Jan 2013 18:07:35 -0500 Subject: [PATCH 04/22] Fixes for Python3, PySide --- pyqtgraph/functions.py | 2 +- pyqtgraph/graphicsItems/GraphicsItem.py | 8 ++++---- pyqtgraph/graphicsItems/ROI.py | 2 +- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- pyqtgraph/multiprocess/bootstrap.py | 8 +++++++- pyqtgraph/multiprocess/parallelizer.py | 4 ++-- pyqtgraph/multiprocess/processes.py | 13 ++++++++++--- pyqtgraph/multiprocess/remoteproxy.py | 11 ++++++++--- pyqtgraph/widgets/RemoteGraphicsView.py | 2 +- 10 files changed, 36 insertions(+), 18 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 27283631..d5899c8c 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1260,7 +1260,7 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): points[b[1]].append([b,a]) ## rearrange into chains - for k in points.keys(): + for k in list(points.keys()): try: chains = points[k] except KeyError: ## already used this point elsewhere diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index c90821a3..94615fe3 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -4,7 +4,7 @@ from pyqtgraph.Point import Point import pyqtgraph.functions as fn import weakref from pyqtgraph.pgcollections import OrderedDict -import operator +import operator, sys class FiniteCache(OrderedDict): """Caches a finite number of objects, removing @@ -17,7 +17,7 @@ class FiniteCache(OrderedDict): self.pop(item, None) # make sure item is added to end OrderedDict.__setitem__(self, item, val) while len(self) > self._length: - del self[self.keys()[0]] + del self[list(self.keys())[0]] def __getitem__(self, item): val = dict.__getitem__(self, item) @@ -197,14 +197,14 @@ class GraphicsItem(object): ## check local cache if direction is None and dt == self._pixelVectorCache[0]: - return map(Point, self._pixelVectorCache[1]) ## return a *copy* + return tuple(map(Point, self._pixelVectorCache[1])) ## return a *copy* ## check global cache key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32()) pv = self._pixelVectorGlobalCache.get(key, None) if pv is not None: self._pixelVectorCache = [dt, pv] - return map(Point,pv) ## return a *copy* + return tuple(map(Point,pv)) ## return a *copy* if direction is None: diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index c3620edb..4da8fa4a 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1808,7 +1808,7 @@ class LineSegmentROI(ROI): for i in range(len(imgPts)-1): d = Point(imgPts[i+1] - imgPts[i]) o = Point(imgPts[i]) - r = fn.affineSlice(data, shape=(int(d.length()),), vectors=[d.norm()], origin=o, axes=axes, order=1) + r = fn.affineSlice(data, shape=(int(d.length()),), vectors=[Point(d.norm())], origin=o, axes=axes, order=1) rgns.append(r) return np.concatenate(rgns, axis=axes[0]) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 2e41cb7c..0b422596 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -41,7 +41,7 @@ def drawSymbol(painter, symbol, size, pen, brush): if isinstance(symbol, basestring): symbol = Symbols[symbol] if np.isscalar(symbol): - symbol = Symbols.values()[symbol % len(Symbols)] + symbol = list(Symbols.values())[symbol % len(Symbols)] painter.drawPath(symbol) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index ae7298ba..f5aa03b8 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1198,7 +1198,7 @@ class ViewBox(GraphicsWidget): if ViewBox is None: ## can happen as python is shutting down return ## Called with ID and name of view (the view itself is no longer available) - for v in ViewBox.AllViews.keys(): + for v in list(ViewBox.AllViews.keys()): if id(v) == vid: ViewBox.AllViews.pop(v) break diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py index 6ac9fce4..28818135 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -4,13 +4,19 @@ import sys, pickle, os if __name__ == '__main__': if hasattr(os, 'setpgrp'): os.setpgrp() ## prevents signals (notably keyboard interrupt) being forwarded from parent to this process - name, port, authkey, ppid, targetStr, path = pickle.load(sys.stdin) + if sys.version[0] == '3': + name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin.buffer) + else: + name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin) #print "key:", ' '.join([str(ord(x)) for x in authkey]) if path is not None: ## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list. while len(sys.path) > 0: sys.path.pop() sys.path.extend(path) + + if pyside: + import PySide #import pyqtgraph #import pyqtgraph.multiprocess.processes target = pickle.loads(targetStr) ## unpickling the target should import everything we need diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index c805cfdb..9925a573 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -1,6 +1,6 @@ import os, sys, time, multiprocessing, re -from processes import ForkedProcess -from remoteproxy import ClosedError +from .processes import ForkedProcess +from .remoteproxy import ClosedError class CanceledError(Exception): """Raised when the progress dialog is canceled during a processing operation.""" diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 1103ef15..4c3be4e9 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -1,7 +1,11 @@ -from remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy +from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy import subprocess, atexit, os, sys, time, random, socket, signal -import cPickle as pickle import multiprocessing.connection +from pyqtgraph.Qt import USE_PYSIDE +try: + import cPickle as pickle +except ImportError: + import pickle __all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ClosedError', 'NoResultError'] @@ -75,7 +79,10 @@ class Process(RemoteEventHandler): targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to ## set its sys.path properly before unpickling the target pid = os.getpid() # we must sent pid to child because windows does not have getppid - pickle.dump((name+'_child', port, authkey, pid, targetStr, sysPath), self.proc.stdin) + pyside = USE_PYSIDE + + ## Send everything the remote process needs to start correctly + pickle.dump((name+'_child', port, authkey, pid, targetStr, sysPath, pyside), self.proc.stdin) self.proc.stdin.close() ## open connection for remote process diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 887d2e87..096f2006 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -1,6 +1,11 @@ -import os, __builtin__, time, sys, traceback, weakref -import cPickle as pickle +import os, time, sys, traceback, weakref import numpy as np +try: + import __builtin__ as builtins + import cPickle as pickle +except ImportError: + import builtins + import pickle class ClosedError(Exception): """Raised when an event handler receives a request to close the connection @@ -181,7 +186,7 @@ class RemoteEventHandler(object): elif cmd == 'import': name = opts['module'] fromlist = opts.get('fromlist', []) - mod = __builtin__.__import__(name, fromlist=fromlist) + mod = builtins.__import__(name, fromlist=fromlist) if len(fromlist) == 0: parts = name.lstrip('.').split('.') diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index bea5a02d..3722e87e 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -27,7 +27,7 @@ class RemoteGraphicsView(QtGui.QWidget): rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') self._view = rpgRemote.Renderer(*args, **kwds) self._view._setProxyOptions(deferGetattr=True) - self.setFocusPolicy(self._view.focusPolicy()) + self.setFocusPolicy(QtCore.Qt.FocusPolicy(self._view.focusPolicy())) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.setMouseTracking(True) self.shm = None From 18d5c6644be06d7886bd8e0e0bd8c9630002eeb4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 19 Jan 2013 07:48:31 -0500 Subject: [PATCH 05/22] Added more documentation for parametertree and AxisItem Fixed linearRegionItem hilight when not movable --- pyqtgraph/graphicsItems/AxisItem.py | 26 ++++- pyqtgraph/graphicsItems/LinearRegionItem.py | 2 +- pyqtgraph/parametertree/Parameter.py | 103 +++++++++++++------- 3 files changed, 92 insertions(+), 39 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index aba5fa8c..9c4130de 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -139,7 +139,31 @@ class AxisItem(GraphicsWidget): self.setScale() def setLabel(self, text=None, units=None, unitPrefix=None, **args): - """Set the text displayed adjacent to the axis.""" + """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. + ============= ============================================================= + + The final text generated for the label will look like:: + + {text} (prefix{units}) + + Each extra keyword argument will become a CSS option in the above template. + For example, you can set the font size and color of the label:: + + labelStyle = {'color': '#FFF', 'font-size': '14pt'} + axis.setLabel('label text', units='V', **labelStyle) + + """ if text is not None: self.labelText = text self.showLabel() diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py index 0b44c815..a35e8efc 100644 --- a/pyqtgraph/graphicsItems/LinearRegionItem.py +++ b/pyqtgraph/graphicsItems/LinearRegionItem.py @@ -245,7 +245,7 @@ class LinearRegionItem(UIGraphicsItem): def hoverEvent(self, ev): - if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): + if self.movable and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): self.setMouseHover(True) else: self.setMouseHover(False) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index c8ed4902..f7da0dbe 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -88,9 +88,10 @@ class Parameter(QtCore.QObject): @staticmethod def create(**opts): """ - Create a new Parameter (or subclass) instance using opts['type'] to select the - appropriate class. + Static method that creates a new Parameter (or subclass) instance using + opts['type'] to select the appropriate class. + All options are passed directly to the new Parameter's __init__ method. Use registerParameterType() to add new class types. """ typ = opts.get('type', None) @@ -101,6 +102,41 @@ class Parameter(QtCore.QObject): return cls(**opts) def __init__(self, **opts): + """ + Initialize a Parameter object. Although it is rare to directly create a + 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) + ================= ========================================================= + """ + + QtCore.QObject.__init__(self) self.opts = { @@ -111,6 +147,7 @@ class Parameter(QtCore.QObject): 'renamable': False, 'removable': False, 'strictNaming': False, # forces name to be usable as a python variable + 'expanded': True, #'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits. } self.opts.update(opts) @@ -148,6 +185,7 @@ class Parameter(QtCore.QObject): #self.watchParam(self) ## emit treechange signals if our own state changes def name(self): + """Return the name of this Parameter.""" return self.opts['name'] def setName(self, name): @@ -165,6 +203,7 @@ class Parameter(QtCore.QObject): return name def type(self): + """Return the type string for this Parameter.""" return self.opts['type'] def isType(self, typ): @@ -197,8 +236,10 @@ class Parameter(QtCore.QObject): return path def setValue(self, value, blockSignal=None): - ## return the actual value that was set - ## (this may be different from the value that was requested) + """ + Set the value of this Parameter; return the actual value that was set. + (this may be different from the value that was requested) + """ try: if blockSignal is not None: self.sigValueChanged.disconnect(blockSignal) @@ -213,6 +254,9 @@ class Parameter(QtCore.QObject): return value def value(self): + """ + Return the value of this Parameter. + """ return self.opts['value'] def getValues(self): @@ -352,9 +396,12 @@ class Parameter(QtCore.QObject): return not self.opts.get('readonly', False) 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 setReadonly(self, readonly=True): + """Set whether this Parameter's value may be edited by the user.""" self.setOpts(readonly=readonly) def setOpts(self, **opts): @@ -362,7 +409,10 @@ class Parameter(QtCore.QObject): Set any arbitrary options on this parameter. The exact behavior of this function will depend on the parameter type, but most parameters will accept a common set of options: value, name, limits, - default, readonly, removable, renamable, visible, and enabled. + default, readonly, removable, renamable, visible, enabled, and expanded. + + See :func:`Parameter.__init__ ` + for more information on default options. """ changed = OrderedDict() for k in opts: @@ -390,7 +440,10 @@ class Parameter(QtCore.QObject): self.emitTreeChanges() def makeTreeItem(self, depth): - """Return a TreeWidgetItem suitable for displaying/controlling the content of this parameter. + """ + Return a TreeWidgetItem suitable for displaying/controlling the content of + this parameter. This is called automatically when a ParameterTree attempts + to display this Parameter. Most subclasses will want to override this function. """ if hasattr(self, 'itemClass'): @@ -424,7 +477,8 @@ class Parameter(QtCore.QObject): """ 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 as Parameter(\*\*child) + If child is a dict, then a parameter is constructed using + :func:`Parameter.create `. """ if isinstance(child, dict): child = Parameter.create(**child) @@ -476,6 +530,7 @@ class Parameter(QtCore.QObject): return self.childs[:] def hasChildren(self): + """Return True if this Parameter has children.""" return len(self.childs) > 0 def parentChanged(self, parent): @@ -553,6 +608,10 @@ class Parameter(QtCore.QObject): def __getattr__(self, attr): ## Leaving this undocumented because I might like to remove it in the future.. #print type(self), attr + import traceback + traceback.print_stack() + print "Warning: Use of Parameter.subParam is deprecated. Use Parameter.param(name) instead." + if 'names' not in self.__dict__: raise AttributeError(attr) if attr in self.names: @@ -582,36 +641,6 @@ class Parameter(QtCore.QObject): self.sigOptionsChanged.emit(self, {'visible': s}) - #def monitorChildren(self): - #if self.monitoringChildren: - #raise Exception("Already monitoring children.") - #self.watchParam(self) - #self.monitoringChildren = True - - #def watchParam(self, param): - #param.sigChildAdded.connect(self.grandchildAdded) - #param.sigChildRemoved.connect(self.grandchildRemoved) - #param.sigStateChanged.connect(self.grandchildChanged) - #for ch in param: - #self.watchParam(ch) - - #def unwatchParam(self, param): - #param.sigChildAdded.disconnect(self.grandchildAdded) - #param.sigChildRemoved.disconnect(self.grandchildRemoved) - #param.sigStateChanged.disconnect(self.grandchildChanged) - #for ch in param: - #self.unwatchParam(ch) - - #def grandchildAdded(self, parent, child): - #self.watchParam(child) - - #def grandchildRemoved(self, parent, child): - #self.unwatchParam(child) - - #def grandchildChanged(self, param, change, data): - ##self.sigTreeStateChanged.emit(self, param, change, data) - #self.emitTreeChange((param, change, data)) - def treeChangeBlocker(self): """ Return an object that can be used to temporarily block and accumulate From 899663c6ca18f8375b5089d74c19c131f58cf0ba Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 24 Jan 2013 13:47:05 -0500 Subject: [PATCH 06/22] bugfixes for scatterplot boundary miss added method for setting axis tick font --- pyqtgraph/graphicsItems/AxisItem.py | 33 +++++++++++++++++----- pyqtgraph/graphicsItems/GraphicsItem.py | 2 +- pyqtgraph/graphicsItems/ScatterPlotItem.py | 5 +++- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index aba5fa8c..8d8ed11b 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -58,13 +58,14 @@ class AxisItem(GraphicsWidget): self.labelUnitPrefix='' self.labelStyle = {} self.logMode = False + self.tickFont = None self.textHeight = 18 self.tickLength = maxTickLength self._tickLevels = None ## used to override the automatic ticking system with explicit ticks self.scale = 1.0 self.autoScale = True - + self.setRange(0, 1) self.setPen(pen) @@ -72,12 +73,12 @@ class AxisItem(GraphicsWidget): self._linkedView = None if linkView is not None: self.linkToView(linkView) - + self.showLabel(False) self.grid = False #self.setCacheMode(self.DeviceCoordinateCache) - + def close(self): self.scene().removeItem(self.label) self.label = None @@ -100,6 +101,14 @@ class AxisItem(GraphicsWidget): self.picture = None self.update() + def setTickFont(self, font): + self.tickFont = font + self.picture = None + self.prepareGeometryChange() + ## Need to re-allocate space depending on font size? + + self.update() + def resizeEvent(self, ev=None): #s = self.size() @@ -287,14 +296,21 @@ class AxisItem(GraphicsWidget): if linkedView is None or self.grid is False: rect = self.mapRectFromParent(self.geometry()) ## extend rect if ticks go in negative direction + ## also extend to account for text that flows past the edges if self.orientation == 'left': - rect.setRight(rect.right() - min(0,self.tickLength)) + #rect.setRight(rect.right() - min(0,self.tickLength)) + #rect.setTop(rect.top() - 15) + #rect.setBottom(rect.bottom() + 15) + rect = rect.adjusted(0, -15, -min(0,self.tickLength), 15) elif self.orientation == 'right': - rect.setLeft(rect.left() + min(0,self.tickLength)) + #rect.setLeft(rect.left() + min(0,self.tickLength)) + rect = rect.adjusted(min(0,self.tickLength), -15, 0, 15) elif self.orientation == 'top': - rect.setBottom(rect.bottom() - min(0,self.tickLength)) + #rect.setBottom(rect.bottom() - min(0,self.tickLength)) + rect = rect.adjusted(-15, 0, 15, -min(0,self.tickLength)) elif self.orientation == 'bottom': - rect.setTop(rect.top() + min(0,self.tickLength)) + #rect.setTop(rect.top() + min(0,self.tickLength)) + rect = rect.adjusted(-15, min(0,self.tickLength), 15, 0) return rect else: return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) @@ -623,6 +639,9 @@ class AxisItem(GraphicsWidget): prof.mark('draw ticks') ## Draw text until there is no more room (or no more text) + if self.tickFont is not None: + p.setFont(self.tickFont) + textRects = [] for i in range(len(tickLevels)): ## Get the list of strings to display for this level diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 94615fe3..75e72177 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -202,7 +202,7 @@ class GraphicsItem(object): ## check global cache key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32()) pv = self._pixelVectorGlobalCache.get(key, None) - if pv is not None: + if direction is None and pv is not None: self._pixelVectorCache = [dt, pv] return tuple(map(Point,pv)) ## return a *copy* diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 0b422596..d606cfdf 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -620,9 +620,12 @@ class ScatterPlotItem(GraphicsObject): if frac >= 1.0: ## increase size of bounds based on spot size and pen width - px = self.pixelLength(Point(1, 0) if ax == 0 else Point(0, 1)) ## determine length of pixel along this axis + #px = self.pixelLength(Point(1, 0) if ax == 0 else Point(0, 1)) ## determine length of pixel along this axis + px = self.pixelVectors()[ax] if px is None: px = 0 + else: + px = px.length() minIndex = np.argmin(d) maxIndex = np.argmax(d) minVal = d[minIndex] From 413a8f930e4cdbbd15ca94bbdc67eb99d46eb2ca Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 30 Jan 2013 15:51:38 -0500 Subject: [PATCH 07/22] Bugfixes: - ViewBox ignore bounds on zoom box - Fixed improper pixel size caching - Fixed check for 'win' in sys.platform (matches 'darwin' as well) --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- pyqtgraph/ptime.py | 2 +- pyqtgraph/widgets/RemoteGraphicsView.py | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 0b422596..ccb6229f 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -599,7 +599,7 @@ class ScatterPlotItem(GraphicsObject): self.invalidate() def dataBounds(self, ax, frac=1.0, orthoRange=None): - if frac >= 1.0 and self.bounds[ax] is not None: + if frac >= 1.0 and orthoRange is None and self.bounds[ax] is not None: return self.bounds[ax] #self.prepareGeometryChange() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index f5aa03b8..8b4ba2af 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -138,7 +138,7 @@ class ViewBox(GraphicsWidget): self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100)) self.rbScaleBox.hide() - self.addItem(self.rbScaleBox) + self.addItem(self.rbScaleBox, ignoreBounds=True) self.axHistory = [] # maintain a history of zoom locations self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo" diff --git a/pyqtgraph/ptime.py b/pyqtgraph/ptime.py index ac61f57f..1de8282f 100644 --- a/pyqtgraph/ptime.py +++ b/pyqtgraph/ptime.py @@ -20,7 +20,7 @@ def unixTime(): """Return the current time in seconds with high precision (unix version, use Manager.time() to stay platform independent).""" return systime.time() -if 'win' in sys.platform: +if sys.platform.startswith('win'): cstart = systime.clock() ### Required to start the clock in windows START_TIME = systime.time() - cstart diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index 3722e87e..2dd1fe9b 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -32,7 +32,7 @@ class RemoteGraphicsView(QtGui.QWidget): self.setMouseTracking(True) self.shm = None shmFileName = self._view.shmFileName() - if 'win' in sys.platform: + if sys.platform.startswith('win'): self.shmtag = shmFileName else: self.shmFile = open(shmFileName, 'r') @@ -60,7 +60,7 @@ class RemoteGraphicsView(QtGui.QWidget): if self.shm is None or self.shm.size != size: if self.shm is not None: self.shm.close() - if 'win' in sys.platform: + if sys.platform.startswith('win'): self.shmtag = newfile ## on windows, we create a new tag for every resize self.shm = mmap.mmap(-1, size, self.shmtag) ## can't use tmpfile on windows because the file can only be opened once. else: @@ -119,7 +119,7 @@ class Renderer(GraphicsView): def __init__(self, *args, **kwds): ## Create shared memory for rendered image - if 'win' in sys.platform: + if sys.platform.startswith('win'): self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)]) self.shm = mmap.mmap(-1, mmap.PAGESIZE, self.shmtag) # use anonymous mmap on windows else: @@ -138,11 +138,11 @@ class Renderer(GraphicsView): def close(self): self.shm.close() - if 'win' not in sys.platform: + if sys.platform.startswith('win'): self.shmFile.close() def shmFileName(self): - if 'win' in sys.platform: + if sys.platform.startswith('win'): return self.shmtag else: return self.shmFile.name @@ -164,7 +164,7 @@ class Renderer(GraphicsView): return size = self.width() * self.height() * 4 if size > self.shm.size(): - if 'win' in sys.platform: + if sys.platform.startswith('win'): ## windows says "WindowsError: [Error 87] the parameter is incorrect" if we try to resize the mmap self.shm.close() ## it also says (sometimes) 'access is denied' if we try to reuse the tag. From ee21e2d0540a88ee35f9f74b4578f664f9319859 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 30 Jan 2013 15:56:08 -0500 Subject: [PATCH 08/22] fixed scatterplotitem segfault added graphitem --- examples/GraphItem.py | 63 +++++++++++ pyqtgraph/functions.py | 92 ++++++++++++++++ pyqtgraph/graphicsItems/GraphItem.py | 109 +++++++++++++++++++ pyqtgraph/graphicsItems/PlotCurveItem.py | 55 +--------- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 15 +++ pyqtgraph/graphicsItems/ScatterPlotItem.py | 5 +- 6 files changed, 284 insertions(+), 55 deletions(-) create mode 100644 examples/GraphItem.py create mode 100644 pyqtgraph/graphicsItems/GraphItem.py diff --git a/examples/GraphItem.py b/examples/GraphItem.py new file mode 100644 index 00000000..baeaf6c4 --- /dev/null +++ b/examples/GraphItem.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" +Simple example of GridItem use. +""" + + +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 + +w = pg.GraphicsWindow() +v = w.addViewBox() +v.setAspectLocked() + +g = pg.GraphItem() +v.addItem(g) + +## Define positions of nodes +pos = np.array([ + [0,0], + [10,0], + [0,10], + [10,10], + [5,5], + [15,5] + ]) + +## 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)]) + +## Update the graph +g.setData(pos=pos, adj=adj, pen=lines, size=1, symbol=symbols, pxMode=False) + + + + +## 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/functions.py b/pyqtgraph/functions.py index d5899c8c..86298ea2 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -27,6 +27,7 @@ from pyqtgraph import getConfigOption import numpy as np import decimal, re import ctypes +import sys, struct try: import scipy.ndimage @@ -1041,6 +1042,97 @@ def colorToAlpha(data, color): +def arrayToQPath(x, y, connect='all'): + """Convert an array of x,y coordinats to QPath as efficiently as possible. + The *connect* argument may be 'all', indicating that each point should be + connected to the next; 'pairs', indicating that each pair of points + should be connected, or an array of int32 values (0 or 1) indicating + connections. + """ + + ## Create all vertices in path. The method used below creates a binary format so that all + ## vertices can be read in at once. This binary format may change in future versions of Qt, + ## so the original (slower) method is left here for emergencies: + #path.moveTo(x[0], y[0]) + #for i in range(1, y.shape[0]): + # path.lineTo(x[i], y[i]) + + ## Speed this up using >> operator + ## Format is: + ## numVerts(i4) 0(i4) + ## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect + ## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex + ## ... + ## 0(i4) + ## + ## All values are big endian--pack using struct.pack('>d') or struct.pack('>i') + + path = QtGui.QPainterPath() + + #prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True) + if sys.version_info[0] == 2: ## So this is disabled for python 3... why?? + 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') + arr.data[12:20] = struct.pack('>ii', n, 0) + #prof.mark('pack header') + # Fill array with vertex values + arr[1:-1]['x'] = x + arr[1:-1]['y'] = y + + # decide which points are connected by lines + if connect == 'pairs': + connect = np.empty((n/2,2), dtype=np.int32) + connect[:,0] = 1 + connect[:,1] = 0 + connect = connect.flatten() + + if connect == 'all': + arr[1:-1]['c'] = 1 + elif isinstance(connect, np.ndarray): + arr[1:-1]['c'] = connect + else: + raise Exception('connect argument must be "all", "pairs", or array') + + #prof.mark('fill array') + # write last 0 + lastInd = 20*(n+1) + arr.data[lastInd:lastInd+4] = struct.pack('>i', 0) + #prof.mark('footer') + # create datastream object and stream into path + buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here + #prof.mark('create buffer') + ds = QtCore.QDataStream(buf) + #prof.mark('create datastream') + ds >> path + #prof.mark('load') + + #prof.finish() + else: + ## This does exactly the same as above, but less efficiently (and more simply). + path.moveTo(x[0], y[0]) + if connect == 'all': + for i in range(1, y.shape[0]): + path.lineTo(x[i], y[i]) + elif connect == 'pairs': + for i in range(1, y.shape[0]): + if i%2 == 0: + path.lineTo(x[i], y[i]) + else: + path.moveTo(x[i], y[i]) + elif isinstance(connect, np.ndarray): + for i in range(1, y.shape[0]): + if connect[i] == 1: + path.lineTo(x[i], y[i]) + else: + path.moveTo(x[i], y[i]) + else: + raise Exception('connect argument must be "all", "pairs", or array') + + return path + #def isosurface(data, level): #""" #Generate isosurface from volumetric data using marching tetrahedra algorithm. diff --git a/pyqtgraph/graphicsItems/GraphItem.py b/pyqtgraph/graphicsItems/GraphItem.py new file mode 100644 index 00000000..9a0f1244 --- /dev/null +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -0,0 +1,109 @@ +from .. import functions as fn +from GraphicsObject import GraphicsObject +from ScatterPlotItem import ScatterPlotItem +import pyqtgraph as pg +import numpy as np + +__all__ = ['GraphItem'] + + +class GraphItem(GraphicsObject): + """A GraphItem displays graph information (as in 'graph theory', not 'graphics') as + a set of nodes connected by lines. + """ + + def __init__(self, **kwds): + GraphicsObject.__init__(self) + self.scatter = ScatterPlotItem() + self.scatter.setParentItem(self) + self.adjacency = None + self.pos = None + self.picture = None + self.pen = 'default' + self.setData(**kwds) + + def setData(self, **kwds): + """ + 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: + * QPen + * a single argument to pass to pg.mkPen + * a record array of length M + with fields (red, green, blue, alpha, width). + * 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 ScatterPlotItem + to affect the appearance of nodes (symbol, size, brush, + etc.) + ============ ========================================================= + """ + if 'adj' in kwds: + self.adjacency = kwds.pop('adj') + assert self.adjacency.dtype.kind in 'iu' + self.picture = None + if 'pos' in kwds: + self.pos = kwds['pos'] + self.picture = None + if 'pen' in kwds: + self.setPen(kwds.pop('pen')) + self.picture = None + if 'symbolPen' in kwds: + kwds['pen'] = kwds.pop('symbolPen') + self.scatter.setData(**kwds) + self.informViewBoundsChanged() + + def setPen(self, pen): + self.pen = pen + self.picture = None + + def generatePicture(self): + self.picture = pg.QtGui.QPicture() + if self.pen is None or self.pos is None or self.adjacency is None: + return + + p = pg.QtGui.QPainter(self.picture) + try: + pts = self.pos[self.adjacency] + pen = self.pen + if isinstance(pen, np.ndarray): + lastPen = None + for i in range(pts.shape[0]): + pen = self.pen[i] + 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)) + 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])) + else: + if pen == 'default': + pen = pg.getConfigOption('foreground') + p.setPen(pg.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) + finally: + p.end() + + def paint(self, p, *args): + if self.picture == None: + self.generatePicture() + self.picture.play(p) + + def boundingRect(self): + return self.scatter.boundingRect() + + + + + + diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index c54671bb..b321714a 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -249,26 +249,6 @@ class PlotCurveItem(GraphicsObject): prof.finish() def generatePath(self, x, y): - prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True) - path = QtGui.QPainterPath() - - ## Create all vertices in path. The method used below creates a binary format so that all - ## vertices can be read in at once. This binary format may change in future versions of Qt, - ## so the original (slower) method is left here for emergencies: - #path.moveTo(x[0], y[0]) - #for i in range(1, y.shape[0]): - # path.lineTo(x[i], y[i]) - - ## Speed this up using >> operator - ## Format is: - ## numVerts(i4) 0(i4) - ## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect - ## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex - ## ... - ## 0(i4) - ## - ## All values are big endian--pack using struct.pack('>d') or struct.pack('>i') - if self.opts['stepMode']: ## each value in the x/y arrays generates 2 points. x2 = np.empty((len(x),2), dtype=x.dtype) @@ -286,41 +266,8 @@ class PlotCurveItem(GraphicsObject): y = y2.reshape(y2.size)[1:-1] y[0] = self.opts['fillLevel'] y[-1] = self.opts['fillLevel'] - - - - - if sys.version_info[0] == 2: ## So this is disabled for python 3... why?? - 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') - arr.data[12:20] = struct.pack('>ii', n, 0) - prof.mark('pack header') - # Fill array with vertex values - arr[1:-1]['x'] = x - arr[1:-1]['y'] = y - arr[1:-1]['c'] = 1 - prof.mark('fill array') - # write last 0 - lastInd = 20*(n+1) - arr.data[lastInd:lastInd+4] = struct.pack('>i', 0) - prof.mark('footer') - # create datastream object and stream into path - buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here - prof.mark('create buffer') - ds = QtCore.QDataStream(buf) - prof.mark('create datastream') - ds >> path - prof.mark('load') - - prof.finish() - else: - path.moveTo(x[0], y[0]) - for i in range(1, y.shape[0]): - path.lineTo(x[i], y[i]) + path = fn.arrayToQPath(x, y, connect='all') return path diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index c362ffb5..63b4bf03 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -1054,6 +1054,21 @@ class PlotItem(GraphicsWidget): """ self.getAxis(axis).setLabel(text=text, units=units, **args) + def setLabels(self, **kwds): + """ + Convenience function allowing multiple labels and/or title to be set in one call. + Keyword arguments can be 'title', 'left', 'bottom', 'right', or 'top'. + Values may be strings or a tuple of arguments to pass to setLabel. + """ + for k,v in kwds.items(): + if k == 'title': + self.setTitle(v) + else: + if isinstance(v, basestring): + v = (v,) + self.setLabel(k, *v) + + def showLabel(self, axis, show=True): """ Show or hide one of the plot's axis labels (the axis itself will be unaffected). diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index d606cfdf..32fac052 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -671,6 +671,8 @@ class ScatterPlotItem(GraphicsObject): pts[1] = self.data['y'] pts = fn.transformCoordinates(tr, pts) self.fragments = [] + pts = np.clip(pts, -2**31, 2**31) ## 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]) @@ -683,8 +685,10 @@ class ScatterPlotItem(GraphicsObject): self.invalidate() def paint(self, p, *args): + #p.setPen(fn.mkPen('r')) #p.drawRect(self.boundingRect()) + if self._exportOpts is not False: aa = self._exportOpts.get('antialias', True) scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed @@ -731,7 +735,6 @@ class ScatterPlotItem(GraphicsObject): p2.end() self.picture.play(p) - def points(self): for rec in self.data: From 175aef2b75225d308cbdc93ca2fdcb3e0a0e1dd8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 10 Feb 2013 13:56:42 -0500 Subject: [PATCH 09/22] Bugfix: "QGraphicsScene is not defined" --- pyqtgraph/GraphicsScene/GraphicsScene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index d0a75d16..8729d085 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -117,7 +117,7 @@ class GraphicsScene(QtGui.QGraphicsScene): def render(self, *args): self.prepareForPaint() - return QGraphicsScene.render(self, *args) + return QtGui.QGraphicsScene.render(self, *args) def prepareForPaint(self): """Called before every render. This method will inform items that the scene is about to From 4c887c8f50fd4d505cd10a0851f9850487bb4b2a Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 10 Feb 2013 14:10:30 -0500 Subject: [PATCH 10/22] Merge new fixes and features from acq4 --- examples/parametertree.py | 1 + pyqtgraph/GraphicsScene/exportDialog.py | 6 + .../GraphicsScene/exportDialogTemplate.ui | 7 + .../exportDialogTemplate_pyqt.py | 8 +- .../exportDialogTemplate_pyside.py | 8 +- pyqtgraph/PlotData.py | 55 ++++ pyqtgraph/colormap.py | 262 ++++++++++++++++++ pyqtgraph/debug.py | 18 ++ pyqtgraph/exporters/Exporter.py | 8 +- pyqtgraph/exporters/ImageExporter.py | 14 +- pyqtgraph/exporters/SVGExporter.py | 29 +- pyqtgraph/graphicsItems/AxisItem.py | 5 +- pyqtgraph/graphicsItems/GradientEditorItem.py | 31 ++- pyqtgraph/graphicsItems/GraphicsItem.py | 41 ++- pyqtgraph/graphicsItems/PlotDataItem.py | 17 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 26 ++ pyqtgraph/graphicsItems/ScatterPlotItem.py | 18 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 9 +- pyqtgraph/parametertree/Parameter.py | 11 +- pyqtgraph/parametertree/parameterTypes.py | 16 +- pyqtgraph/rebuildUi.py | 4 +- pyqtgraph/widgets/ColorButton.py | 10 +- pyqtgraph/widgets/ColorMapWidget.py | 173 ++++++++++++ pyqtgraph/widgets/DataFilterWidget.py | 115 ++++++++ pyqtgraph/widgets/ScatterPlotWidget.py | 183 ++++++++++++ 25 files changed, 1024 insertions(+), 51 deletions(-) create mode 100644 pyqtgraph/PlotData.py create mode 100644 pyqtgraph/colormap.py create mode 100644 pyqtgraph/widgets/ColorMapWidget.py create mode 100644 pyqtgraph/widgets/DataFilterWidget.py create mode 100644 pyqtgraph/widgets/ScatterPlotWidget.py diff --git a/examples/parametertree.py b/examples/parametertree.py index 243fd0fe..9bcbc5d2 100644 --- a/examples/parametertree.py +++ b/examples/parametertree.py @@ -70,6 +70,7 @@ params = [ {'name': 'Named List', 'type': 'list', 'values': {"one": 1, "two": 2, "three": 3}, 'value': 2}, {'name': 'Boolean', 'type': 'bool', 'value': True, 'tip': "This is a checkbox"}, {'name': 'Color', 'type': 'color', 'value': "FF0", 'tip': "This is a color button"}, + {'name': 'Gradient', 'type': 'colormap'}, {'name': 'Subgroup', 'type': 'group', 'children': [ {'name': 'Sub-param 1', 'type': 'int', 'value': 10}, {'name': 'Sub-param 2', 'type': 'float', 'value': 1.2e6}, diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py index dafcd501..73a8c83f 100644 --- a/pyqtgraph/GraphicsScene/exportDialog.py +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -27,6 +27,7 @@ class ExportDialog(QtGui.QWidget): self.ui.closeBtn.clicked.connect(self.close) self.ui.exportBtn.clicked.connect(self.exportClicked) + self.ui.copyBtn.clicked.connect(self.copyClicked) self.ui.itemTree.currentItemChanged.connect(self.exportItemChanged) self.ui.formatList.currentItemChanged.connect(self.exportFormatChanged) @@ -116,11 +117,16 @@ class ExportDialog(QtGui.QWidget): else: self.ui.paramTree.setParameters(params) self.currentExporter = exp + self.ui.copyBtn.setEnabled(exp.allowCopy) def exportClicked(self): self.selectBox.hide() self.currentExporter.export() + def copyClicked(self): + self.selectBox.hide() + self.currentExporter.export(copy=True) + def close(self): self.selectBox.setVisible(False) self.setVisible(False) diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate.ui b/pyqtgraph/GraphicsScene/exportDialogTemplate.ui index c81c8831..c91fbc3f 100644 --- a/pyqtgraph/GraphicsScene/exportDialogTemplate.ui +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate.ui @@ -79,6 +79,13 @@ + + + + Copy + + + diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py index 20609b51..c3056d1c 100644 --- a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py @@ -2,8 +2,8 @@ # Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui' # -# Created: Sun Sep 9 14:41:31 2012 -# by: PyQt4 UI code generator 4.9.1 +# Created: Wed Jan 30 21:02:28 2013 +# by: PyQt4 UI code generator 4.9.3 # # WARNING! All changes made in this file will be lost! @@ -49,6 +49,9 @@ class Ui_Form(object): self.label_3 = QtGui.QLabel(Form) self.label_3.setObjectName(_fromUtf8("label_3")) self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) + self.copyBtn = QtGui.QPushButton(Form) + self.copyBtn.setObjectName(_fromUtf8("copyBtn")) + self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) @@ -60,5 +63,6 @@ class Ui_Form(object): 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)) from pyqtgraph.parametertree import ParameterTree diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py index 4ffc0b9a..cf27f60a 100644 --- a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py @@ -2,8 +2,8 @@ # Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui' # -# Created: Sun Sep 9 14:41:31 2012 -# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# Created: Wed Jan 30 21:02:28 2013 +# by: pyside-uic 0.2.13 running on PySide 1.1.1 # # WARNING! All changes made in this file will be lost! @@ -44,6 +44,9 @@ class Ui_Form(object): self.label_3 = QtGui.QLabel(Form) self.label_3.setObjectName("label_3") self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) + self.copyBtn = QtGui.QPushButton(Form) + self.copyBtn.setObjectName("copyBtn") + self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) @@ -55,5 +58,6 @@ class Ui_Form(object): 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)) from pyqtgraph.parametertree import ParameterTree diff --git a/pyqtgraph/PlotData.py b/pyqtgraph/PlotData.py new file mode 100644 index 00000000..18531c14 --- /dev/null +++ b/pyqtgraph/PlotData.py @@ -0,0 +1,55 @@ + + +class PlotData(object): + """ + Class used for managing plot data + - allows data sharing between multiple graphics items (curve, scatter, graph..) + - each item may define the columns it needs + - column groupings ('pos' or x, y, z) + - efficiently appendable + - log, fft transformations + - color mode conversion (float/byte/qcolor) + - pen/brush conversion + - per-field cached masking + - allows multiple masking fields (different graphics need to mask on different criteria) + - removal of nan/inf values + - option for single value shared by entire column + - cached downsampling + """ + def __init__(self): + self.fields = {} + + self.maxVals = {} ## cache for max/min + self.minVals = {} + + def addFields(self, fields): + for f in fields: + if f not in self.fields: + self.fields[f] = None + + def hasField(self, f): + return f in self.fields + + def __getitem__(self, field): + return self.fields[field] + + def __setitem__(self, field, val): + self.fields[field] = val + + def max(self, field): + mx = self.maxVals.get(field, None) + if mx is None: + mx = np.max(self[field]) + self.maxVals[field] = mx + return mx + + def min(self, field): + mn = self.minVals.get(field, None) + if mn is None: + mn = np.min(self[field]) + self.minVals[field] = mn + return mn + + + + \ No newline at end of file diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py new file mode 100644 index 00000000..c7e683fb --- /dev/null +++ b/pyqtgraph/colormap.py @@ -0,0 +1,262 @@ +import numpy as np +import scipy.interpolate +from pyqtgraph.Qt import QtGui, QtCore + +class ColorMap(object): + + ## color interpolation modes + RGB = 1 + HSV_POS = 2 + HSV_NEG = 3 + + ## boundary modes + CLIP = 1 + REPEAT = 2 + MIRROR = 3 + + ## return types + BYTE = 1 + FLOAT = 2 + QCOLOR = 3 + + enumMap = { + 'rgb': RGB, + 'hsv+': HSV_POS, + 'hsv-': HSV_NEG, + 'clip': CLIP, + 'repeat': REPEAT, + 'mirror': MIRROR, + 'byte': BYTE, + 'float': FLOAT, + 'qcolor': QCOLOR, + } + + 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. + ========= ============================================================== + """ + self.pos = pos + self.color = color + if mode is None: + mode = np.ones(len(pos)) + self.mode = mode + self.stopsCache = {} + + def map(self, data, mode='byte'): + """ + Data must be either a scalar position or an array (any shape) of positions. + """ + if isinstance(mode, basestring): + mode = self.enumMap[mode.lower()] + + if mode == self.QCOLOR: + pos, color = self.getStops(self.BYTE) + else: + pos, color = self.getStops(mode) + + data = np.clip(data, pos.min(), pos.max()) + + if not isinstance(data, np.ndarray): + interp = scipy.interpolate.griddata(pos, color, np.array([data]))[0] + else: + interp = scipy.interpolate.griddata(pos, color, data) + + if mode == self.QCOLOR: + if not isinstance(data, np.ndarray): + return QtGui.QColor(*interp) + else: + return [QtGui.QColor(*x) for x in interp] + else: + return interp + + def mapToQColor(self, data): + return self.map(data, mode=self.QCOLOR) + + def mapToByte(self, data): + return self.map(data, mode=self.BYTE) + + def mapToFloat(self, data): + return self.map(data, mode=self.FLOAT) + + def getGradient(self, p1=None, p2=None): + """Return a QLinearGradient object.""" + if p1 == None: + p1 = QtCore.QPointF(0,0) + if p2 == None: + p2 = QtCore.QPointF(self.pos.max()-self.pos.min(),0) + g = QtGui.QLinearGradient(p1, p2) + + pos, color = self.getStops(mode=self.BYTE) + color = [QtGui.QColor(*x) for x in color] + g.setStops(zip(pos, color)) + + #if self.colorMode == 'rgb': + #ticks = self.listTicks() + #g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks]) + #elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop + #ticks = self.listTicks() + #stops = [] + #stops.append((ticks[0][1], ticks[0][0].color)) + #for i in range(1,len(ticks)): + #x1 = ticks[i-1][1] + #x2 = ticks[i][1] + #dx = (x2-x1) / 10. + #for j in range(1,10): + #x = x1 + dx*j + #stops.append((x, self.getColor(x))) + #stops.append((x2, self.getColor(x2))) + #g.setStops(stops) + return g + + def getColors(self, mode=None): + """Return list of all colors converted to the specified mode. + If mode is None, then no conversion is done.""" + if isinstance(mode, basestring): + mode = self.enumMap[mode.lower()] + + color = self.color + if mode in [self.BYTE, self.QCOLOR] and color.dtype.kind == 'f': + color = (color * 255).astype(np.ubyte) + elif mode == self.FLOAT and color.dtype.kind != 'f': + color = color.astype(float) / 255. + + if mode == self.QCOLOR: + color = [QtGui.QColor(*x) for x in color] + + return color + + def getStops(self, mode): + ## Get fully-expanded set of RGBA stops in either float or byte mode. + if mode not in self.stopsCache: + color = self.color + if mode == self.BYTE and color.dtype.kind == 'f': + color = (color * 255).astype(np.ubyte) + elif mode == self.FLOAT and color.dtype.kind != 'f': + color = color.astype(float) / 255. + + ## to support HSV mode, we need to do a little more work.. + #stops = [] + #for i in range(len(self.pos)): + #pos = self.pos[i] + #color = color[i] + + #imode = self.mode[i] + #if imode == self.RGB: + #stops.append((x,color)) + #else: + #ns = + self.stopsCache[mode] = (self.pos, color) + return self.stopsCache[mode] + + #def getColor(self, x, toQColor=True): + #""" + #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. + #============= ================================================================== + #""" + #ticks = self.listTicks() + #if x <= ticks[0][1]: + #c = ticks[0][0].color + #if toQColor: + #return QtGui.QColor(c) # always copy colors before handing them out + #else: + #return (c.red(), c.green(), c.blue(), c.alpha()) + #if x >= ticks[-1][1]: + #c = ticks[-1][0].color + #if toQColor: + #return QtGui.QColor(c) # always copy colors before handing them out + #else: + #return (c.red(), c.green(), c.blue(), c.alpha()) + + #x2 = ticks[0][1] + #for i in range(1,len(ticks)): + #x1 = x2 + #x2 = ticks[i][1] + #if x1 <= x and x2 >= x: + #break + + #dx = (x2-x1) + #if dx == 0: + #f = 0. + #else: + #f = (x-x1) / dx + #c1 = ticks[i-1][0].color + #c2 = ticks[i][0].color + #if self.colorMode == 'rgb': + #r = c1.red() * (1.-f) + c2.red() * f + #g = c1.green() * (1.-f) + c2.green() * f + #b = c1.blue() * (1.-f) + c2.blue() * f + #a = c1.alpha() * (1.-f) + c2.alpha() * f + #if toQColor: + #return QtGui.QColor(int(r), int(g), int(b), int(a)) + #else: + #return (r,g,b,a) + #elif self.colorMode == 'hsv': + #h1,s1,v1,_ = c1.getHsv() + #h2,s2,v2,_ = c2.getHsv() + #h = h1 * (1.-f) + h2 * f + #s = s1 * (1.-f) + s2 * f + #v = v1 * (1.-f) + v2 * f + #c = QtGui.QColor() + #c.setHsv(h,s,v) + #if toQColor: + #return c + #else: + #return (c.red(), c.green(), c.blue(), c.alpha()) + + def getLookupTable(self, start=0.0, stop=1.0, nPts=512, alpha=None, mode='byte'): + """ + 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, it will be automatically determined. + ============= ============================================================================ + """ + if isinstance(mode, basestring): + mode = self.enumMap[mode.lower()] + + if alpha is None: + alpha = self.usesAlpha() + + x = np.linspace(start, stop, nPts) + table = self.map(x, mode) + + if not alpha: + return table[:,:3] + else: + return table + + def usesAlpha(self): + """Return True if any stops have an alpha < 255""" + max = 1.0 if self.color.dtype.kind == 'f' else 255 + return np.any(self.color[:,3] != max) + + def isMapTrivial(self): + """Return True if the gradient has exactly two stops in it: black at 0.0 and white at 1.0""" + if len(self.pos) != 2: + return False + if self.pos[0] != 0.0 or self.pos[1] != 1.0: + return False + if self.color.dtype.kind == 'f': + return np.all(self.color == np.array([[0.,0.,0.,1.], [1.,1.,1.,1.]])) + else: + return np.all(self.color == np.array([[0,0,0,255], [255,255,255,255]])) + + diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 7fa169a4..ae2b21ac 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -917,3 +917,21 @@ def qObjectReport(verbose=False): for t in typs: print(count[t], "\t", t) + +class PrintDetector(object): + def __init__(self): + self.stdout = sys.stdout + sys.stdout = self + + def remove(self): + sys.stdout = self.stdout + + def __del__(self): + self.remove() + + def write(self, x): + self.stdout.write(x) + traceback.print_stack() + + def flush(self): + self.stdout.flush() \ No newline at end of file diff --git a/pyqtgraph/exporters/Exporter.py b/pyqtgraph/exporters/Exporter.py index b1a663bc..81930670 100644 --- a/pyqtgraph/exporters/Exporter.py +++ b/pyqtgraph/exporters/Exporter.py @@ -9,7 +9,8 @@ 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 + def __init__(self, item): """ Initialize with the item to be exported. @@ -25,10 +26,11 @@ class Exporter(object): """Return the parameters used to configure this exporter.""" raise Exception("Abstract method must be overridden in subclass.") - def export(self, fileName=None, toBytes=False): + def export(self, fileName=None, toBytes=False, copy=False): """ If *fileName* is None, pop-up a file dialog. - If *toString* is True, return a bytes object rather than writing to file. + If *toBytes* is True, return a bytes object rather than writing to file. + If *copy* is True, export to the copy buffer rather than writing to file. """ raise Exception("Abstract method must be overridden in subclass.") diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index cb6cf396..bdb8b9be 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -8,6 +8,8 @@ __all__ = ['ImageExporter'] class ImageExporter(Exporter): Name = "Image File (PNG, TIF, JPG, ...)" + allowCopy = True + def __init__(self, item): Exporter.__init__(self, item) tr = self.getTargetRect() @@ -38,8 +40,8 @@ class ImageExporter(Exporter): def parameters(self): return self.params - def export(self, fileName=None): - if fileName is None: + def export(self, fileName=None, toBytes=False, copy=False): + if fileName is None and not toBytes and not copy: filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()] preferred = ['*.png', '*.tif', '*.jpg'] for p in preferred[::-1]: @@ -78,6 +80,12 @@ class ImageExporter(Exporter): finally: self.setExportMode(False) painter.end() - self.png.save(fileName) + + if copy: + QtGui.QApplication.clipboard().setImage(self.png) + elif toBytes: + return self.png + else: + self.png.save(fileName) \ No newline at end of file diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 587282e0..b284db89 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -11,6 +11,8 @@ __all__ = ['SVGExporter'] class SVGExporter(Exporter): Name = "Scalable Vector Graphics (SVG)" + allowCopy=True + def __init__(self, item): Exporter.__init__(self, item) #tr = self.getTargetRect() @@ -37,8 +39,8 @@ class SVGExporter(Exporter): def parameters(self): return self.params - def export(self, fileName=None, toBytes=False): - if toBytes is False and fileName is None: + def export(self, fileName=None, toBytes=False, copy=False): + if toBytes is False and copy is False and fileName is None: self.fileSaveDialog(filter="Scalable Vector Graphics (*.svg)") return #self.svg = QtSvg.QSvgGenerator() @@ -83,11 +85,16 @@ class SVGExporter(Exporter): xml = generateSvg(self.item) if toBytes: - return bytes(xml) + return xml.encode('UTF-8') + elif copy: + md = QtCore.QMimeData() + md.setData('image/svg+xml', QtCore.QByteArray(xml.encode('UTF-8'))) + QtGui.QApplication.clipboard().setMimeData(md) else: with open(fileName, 'w') as fh: fh.write(xml.encode('UTF-8')) + xmlHeader = """\ @@ -148,7 +155,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) if nodes is None: ## nodes maps all node IDs to their XML element. ## this allows us to ensure all elements receive unique names. @@ -170,8 +177,12 @@ def _generateItemSvg(item, nodes=None, root=None): tr = QtGui.QTransform() if isinstance(item, QtGui.QGraphicsScene): xmlStr = "\n\n" - childs = [i for i in item.items() if i.parentItem() is None] doc = xml.parseString(xmlStr) + childs = [i for i in item.items() if i.parentItem() is None] + elif item.__class__.paint == QtGui.QGraphicsItem.paint: + xmlStr = "\n\n" + doc = xml.parseString(xmlStr) + childs = item.childItems() else: childs = item.childItems() tr = itemTransform(item, item.scene()) @@ -223,11 +234,12 @@ def _generateItemSvg(item, nodes=None, root=None): print(doc.toxml()) raise + prof.mark('render') ## Get rid of group transformation matrices by applying ## transformation to inner coordinates correctCoordinates(g1, item) - + prof.mark('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) @@ -277,6 +289,8 @@ 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') + ## Add all child items as sub-elements. childs.sort(key=lambda c: c.zValue()) for ch in childs: @@ -284,7 +298,8 @@ 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() return g1 def correctCoordinates(node, item): diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index d5b09915..9ef64763 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -683,7 +683,7 @@ class AxisItem(GraphicsWidget): if tickPositions[i][j] is None: strings[j] = None - textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None]) + textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s)) for s in strings if s is not None]) if i > 0: ## always draw top level ## measure all text, make sure there's enough room if axis == 0: @@ -699,8 +699,9 @@ class AxisItem(GraphicsWidget): #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 vstr is None: ## this tick was ignored because it is out of bounds continue + vstr = str(vstr) x = tickPositions[i][j] textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) height = textRect.height() diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index 3c078ede..5439c731 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -5,6 +5,8 @@ from .GraphicsObject import GraphicsObject from .GraphicsWidget import GraphicsWidget import weakref from pyqtgraph.pgcollections import OrderedDict +from pyqtgraph.colormap import ColorMap + import numpy as np __all__ = ['TickSliderItem', 'GradientEditorItem'] @@ -22,6 +24,9 @@ Gradients = OrderedDict([ ]) + + + class TickSliderItem(GraphicsWidget): ## public class """**Bases:** :class:`GraphicsWidget ` @@ -490,6 +495,18 @@ class GradientEditorItem(TickSliderItem): self.colorMode = cm self.updateGradient() + def colorMap(self): + """Return a ColorMap object representing the current state of the editor.""" + if self.colorMode == 'hsv': + raise NotImplementedError('hsv colormaps not yet supported') + pos = [] + color = [] + for t,x in self.listTicks(): + pos.append(x) + c = t.color + color.append([c.red(), c.green(), c.blue(), c.alpha()]) + return ColorMap(np.array(pos), np.array(color, dtype=np.ubyte)) + def updateGradient(self): #private self.gradient = self.getGradient() @@ -611,7 +628,7 @@ class GradientEditorItem(TickSliderItem): b = c1.blue() * (1.-f) + c2.blue() * f a = c1.alpha() * (1.-f) + c2.alpha() * f if toQColor: - return QtGui.QColor(r, g, b,a) + return QtGui.QColor(int(r), int(g), int(b), int(a)) else: return (r,g,b,a) elif self.colorMode == 'hsv': @@ -751,6 +768,18 @@ class GradientEditorItem(TickSliderItem): self.addTick(t[0], c, finish=False) self.updateGradient() self.sigGradientChangeFinished.emit(self) + + def setColorMap(self, cm): + self.setColorMode('rgb') + for t in list(self.ticks.keys()): + self.removeTick(t, finish=False) + colors = cm.getColors(mode='qcolor') + for i in range(len(cm.pos)): + x = cm.pos[i] + c = colors[i] + self.addTick(x, c, finish=False) + self.updateGradient() + self.sigGradientChangeFinished.emit(self) class Tick(GraphicsObject): diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 75e72177..1795e79e 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -20,7 +20,7 @@ class FiniteCache(OrderedDict): del self[list(self.keys())[0]] def __getitem__(self, item): - val = dict.__getitem__(self, item) + val = OrderedDict.__getitem__(self, item) del self[item] self[item] = val ## promote this key return val @@ -194,6 +194,10 @@ class GraphicsItem(object): dt = self.deviceTransform() if dt is None: return None, None + + ## Ignore translation. If the translation is much larger than the scale + ## (such as when looking at unix timestamps), we can get floating-point errors. + dt.setMatrix(dt.m11(), dt.m12(), 0, dt.m21(), dt.m22(), 0, 0, 0, 1) ## check local cache if direction is None and dt == self._pixelVectorCache[0]: @@ -213,15 +217,32 @@ class GraphicsItem(object): raise Exception("Cannot compute pixel length for 0-length vector.") ## attempt to re-scale direction vector to fit within the precision of the coordinate system - if direction.x() == 0: - r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22())) - #r = 1.0/(abs(dt.m12()) + abs(dt.m22())) - elif direction.y() == 0: - r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21())) - #r = 1.0/(abs(dt.m11()) + abs(dt.m21())) - else: - r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5 - directionr = direction * r + ## Here's the problem: we need to map the vector 'direction' from the item to the device, via transform 'dt'. + ## In some extreme cases, this mapping can fail unless the length of 'direction' is cleverly chosen. + ## Example: + ## dt = [ 1, 0, 2 + ## 0, 2, 1e20 + ## 0, 0, 1 ] + ## Then we map the origin (0,0) and direction (0,1) and get: + ## o' = 2,1e20 + ## d' = 2,1e20 <-- should be 1e20+2, but this can't be represented with a 32-bit float + ## + ## |o' - d'| == 0 <-- this is the problem. + + ## Perhaps the easiest solution is to exclude the transformation column from dt. Does this cause any other problems? + + #if direction.x() == 0: + #r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22())) + ##r = 1.0/(abs(dt.m12()) + abs(dt.m22())) + #elif direction.y() == 0: + #r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21())) + ##r = 1.0/(abs(dt.m11()) + abs(dt.m21())) + #else: + #r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5 + #if r == 0: + #r = 1. ## shouldn't need to do this; probably means the math above is wrong? + #directionr = direction * r + directionr = direction ## map direction vector onto device #viewDir = Point(dt.map(directionr) - dt.map(Point(0,0))) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 8e6162f2..83afbbfe 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -104,6 +104,7 @@ class PlotDataItem(GraphicsObject): self.yData = None self.xDisp = None self.yDisp = None + self.dataMask = None #self.curves = [] #self.scatters = [] self.curve = PlotCurveItem() @@ -393,6 +394,7 @@ class PlotDataItem(GraphicsObject): scatterArgs[v] = self.opts[k] x,y = self.getData() + scatterArgs['mask'] = self.dataMask if curveArgs['pen'] is not None or (curveArgs['brush'] is not None and curveArgs['fillLevel'] is not None): self.curve.setData(x=x, y=y, **curveArgs) @@ -413,11 +415,15 @@ class PlotDataItem(GraphicsObject): if self.xDisp is None: nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData) if any(nanMask): - x = self.xData[~nanMask] - y = self.yData[~nanMask] + self.dataMask = ~nanMask + x = self.xData[self.dataMask] + y = self.yData[self.dataMask] else: + self.dataMask = None x = self.xData y = self.yData + + ds = self.opts['downsample'] if ds > 1: x = x[::ds] @@ -435,8 +441,11 @@ class PlotDataItem(GraphicsObject): if any(self.opts['logMode']): ## re-check for NANs after log nanMask = np.isinf(x) | np.isinf(y) | np.isnan(x) | np.isnan(y) if any(nanMask): - x = x[~nanMask] - y = y[~nanMask] + self.dataMask = ~nanMask + x = x[self.dataMask] + y = y[self.dataMask] + else: + self.dataMask = None self.xDisp = x self.yDisp = y #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 63b4bf03..3100087a 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -36,6 +36,7 @@ from .. LabelItem import LabelItem from .. LegendItem import LegendItem from .. GraphicsWidget import GraphicsWidget from .. ButtonItem import ButtonItem +from .. InfiniteLine import InfiniteLine from pyqtgraph.WidgetGroup import WidgetGroup __all__ = ['PlotItem'] @@ -548,10 +549,35 @@ class PlotItem(GraphicsWidget): print("PlotItem.addDataItem is deprecated. Use addItem instead.") self.addItem(item, *args) + def listDataItems(self): + """Return a list of all data items (PlotDataItem, PlotCurveItem, ScatterPlotItem, etc) + contained in this PlotItem.""" + return self.dataItems[:] + def addCurve(self, c, params=None): print("PlotItem.addCurve is deprecated. Use addItem instead.") self.addItem(c, params) + def addLine(self, x=None, y=None, z=None, **kwds): + """ + Create an InfiniteLine and add to the plot. + + If *x* is specified, + the line will be vertical. If *y* is specified, the line will be + horizontal. All extra keyword arguments are passed to + :func:`InfiniteLine.__init__() `. + Returns the item created. + """ + angle = 0 if x is None else 90 + pos = x if x is not None else y + line = InfiniteLine(pos, angle, **kwds) + self.addItem(line) + if z is not None: + line.setZValue(z) + return line + + + def removeItem(self, item): """ Remove an item from the internal ViewBox. diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 45adcf4d..5af82a00 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -384,7 +384,7 @@ class ScatterPlotItem(GraphicsObject): for k in ['pen', 'brush', 'symbol', 'size']: if k in kargs: setMethod = getattr(self, 'set' + k[0].upper() + k[1:]) - setMethod(kargs[k], update=False, dataSet=newData) + setMethod(kargs[k], update=False, dataSet=newData, mask=kargs.get('mask', None)) if 'data' in kargs: self.setPointData(kargs['data'], dataSet=newData) @@ -425,6 +425,8 @@ 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: + 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))) dataSet['pen'] = pens @@ -445,6 +447,8 @@ 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: + 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))) #for i in xrange(len(brushes)): @@ -458,7 +462,7 @@ class ScatterPlotItem(GraphicsObject): if update: self.updateSpots(dataSet) - def setSymbol(self, symbol, update=True, dataSet=None): + def setSymbol(self, symbol, update=True, dataSet=None, mask=None): """Set the symbol(s) used to draw each spot. If a list or array is provided, then the symbol for each spot will be set separately. Otherwise, the argument will be used as the default symbol for @@ -468,6 +472,8 @@ class ScatterPlotItem(GraphicsObject): if isinstance(symbol, np.ndarray) or isinstance(symbol, list): symbols = symbol + if kargs['mask'] is not None: + symbols = symbols[kargs['mask']] if len(symbols) != len(dataSet): raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(dataSet))) dataSet['symbol'] = symbols @@ -479,7 +485,7 @@ class ScatterPlotItem(GraphicsObject): if update: self.updateSpots(dataSet) - def setSize(self, size, update=True, dataSet=None): + def setSize(self, size, update=True, dataSet=None, mask=None): """Set the size(s) used to draw each spot. If a list or array is provided, then the size for each spot will be set separately. Otherwise, the argument will be used as the default size for @@ -489,6 +495,8 @@ class ScatterPlotItem(GraphicsObject): if isinstance(size, np.ndarray) or isinstance(size, list): sizes = size + if kargs['mask'] is not None: + sizes = sizes[kargs['mask']] if len(sizes) != len(dataSet): raise Exception("Number of sizes does not match number of points (%d != %d)" % (len(sizes), len(dataSet))) dataSet['size'] = sizes @@ -505,6 +513,8 @@ class ScatterPlotItem(GraphicsObject): dataSet = self.data if isinstance(data, np.ndarray) or isinstance(data, list): + if kargs['mask'] is not None: + data = data[kargs['mask']] if len(data) != len(dataSet): raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet))) @@ -881,7 +891,7 @@ class SpotItem(object): def updateItem(self): self._data['fragCoords'] = None - self._plot.updateSpots([self._data]) + self._plot.updateSpots(self._data.reshape(1)) self._plot.invalidate() #class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem): diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 8b4ba2af..37f21182 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -576,9 +576,12 @@ class ViewBox(GraphicsWidget): w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2. childRange[ax] = [x-w2, x+w2] else: - wp = (xr[1] - xr[0]) * 0.02 - childRange[ax][0] -= wp - childRange[ax][1] += wp + l = self.width() if ax==0 else self.height() + if l > 0: + padding = np.clip(1./(l**0.5), 0.02, 0.1) + wp = (xr[1] - xr[0]) * padding + childRange[ax][0] -= wp + childRange[ax][1] += wp targetRect[ax] = childRange[ax] args['xRange' if ax == 0 else 'yRange'] = targetRect[ax] if len(args) == 0: diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index f7da0dbe..c8e19f16 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -525,8 +525,9 @@ class Parameter(QtCore.QObject): self.removeChild(ch) def children(self): - """Return a list of this parameter's children.""" - ## warning -- this overrides QObject.children + """Return a list of this parameter's children. + Warning: this overrides QObject.children + """ return self.childs[:] def hasChildren(self): @@ -608,13 +609,13 @@ class Parameter(QtCore.QObject): def __getattr__(self, attr): ## Leaving this undocumented because I might like to remove it in the future.. #print type(self), attr - import traceback - traceback.print_stack() - print "Warning: Use of Parameter.subParam is deprecated. Use Parameter.param(name) instead." if 'names' not in self.__dict__: raise AttributeError(attr) if attr in self.names: + import traceback + traceback.print_stack() + print "Warning: Use of Parameter.subParam is deprecated. Use Parameter.param(name) instead." return self.param(attr) else: raise AttributeError(attr) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 3aab5a6d..84db9f06 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -4,6 +4,7 @@ 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 import os @@ -61,7 +62,11 @@ class WidgetParameterItem(ParameterItem): w.sigChanging.connect(self.widgetValueChanging) ## update value shown in widget. - self.valueChanged(self, opts['value'], force=True) + if opts.get('value', None) is not None: + self.valueChanged(self, opts['value'], force=True) + else: + ## no starting value was given; use whatever the widget has + self.widgetValueChanged() def makeWidget(self): @@ -125,6 +130,14 @@ class WidgetParameterItem(ParameterItem): w.setValue = w.setColor self.hideWidget = False w.setFlat(True) + elif t == 'colormap': + from pyqtgraph.widgets.GradientWidget import GradientWidget ## need this here to avoid import loop + w = GradientWidget(orientation='bottom') + w.sigChanged = w.sigGradientChangeFinished + w.sigChanging = w.sigGradientChanged + w.value = w.colorMap + w.setValue = w.setColorMap + self.hideWidget = False else: raise Exception("Unknown type '%s'" % asUnicode(t)) return w @@ -294,6 +307,7 @@ registerParameterType('float', SimpleParameter, override=True) registerParameterType('bool', SimpleParameter, override=True) registerParameterType('str', SimpleParameter, override=True) registerParameterType('color', SimpleParameter, override=True) +registerParameterType('colormap', SimpleParameter, override=True) diff --git a/pyqtgraph/rebuildUi.py b/pyqtgraph/rebuildUi.py index 92d5991a..1e4cbf9c 100644 --- a/pyqtgraph/rebuildUi.py +++ b/pyqtgraph/rebuildUi.py @@ -13,11 +13,11 @@ for path, sd, files in os.walk('.'): ui = os.path.join(path, f) py = os.path.join(path, base + '_pyqt.py') - if os.stat(ui).st_mtime > os.stat(py).st_mtime: + 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) py = os.path.join(path, base + '_pyside.py') - if os.stat(ui).st_mtime > os.stat(py).st_mtime: + if not os.path.exists(py) or os.stat(ui).st_mtime > os.stat(py).st_mtime: os.system('%s %s > %s' % (pysideuic, ui, py)) print(py) diff --git a/pyqtgraph/widgets/ColorButton.py b/pyqtgraph/widgets/ColorButton.py index fafe2ae7..ee91801a 100644 --- a/pyqtgraph/widgets/ColorButton.py +++ b/pyqtgraph/widgets/ColorButton.py @@ -77,8 +77,14 @@ class ColorButton(QtGui.QPushButton): def restoreState(self, state): self.setColor(state) - def color(self): - return functions.mkColor(self._color) + def color(self, mode='qcolor'): + color = functions.mkColor(self._color) + if mode == 'qcolor': + return color + elif mode == 'byte': + return (color.red(), color.green(), color.blue(), color.alpha()) + elif mode == 'float': + return (color.red()/255., color.green()/255., color.blue()/255., color.alpha()/255.) def widgetGroupInterface(self): return (self.sigColorChanged, ColorButton.saveState, ColorButton.restoreState) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py new file mode 100644 index 00000000..69a5e10a --- /dev/null +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -0,0 +1,173 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.parametertree as ptree +import numpy as np +from pyqtgraph.pgcollections import OrderedDict +import pyqtgraph.functions as fn + +__all__ = ['ColorMapWidget'] + +class ColorMapWidget(ptree.ParameterTree): + """ + This class provides a widget allowing the user to customize color mapping + for multi-column data. + """ + + sigColorMapChanged = QtCore.Signal(object) + + def __init__(self): + ptree.ParameterTree.__init__(self, showHeader=False) + + self.params = ColorMapParameter() + self.setParameters(self.params) + self.params.sigTreeStateChanged.connect(self.mapChanged) + + ## wrap a couple methods + self.setFields = self.params.setFields + self.map = self.params.map + + def mapChanged(self): + self.sigColorMapChanged.emit(self) + + +class ColorMapParameter(ptree.types.GroupParameter): + sigColorMapChanged = QtCore.Signal(object) + + def __init__(self): + self.fields = {} + ptree.types.GroupParameter.__init__(self, name='Color Map', addText='Add Mapping..', addList=[]) + self.sigTreeStateChanged.connect(self.mapChanged) + + def mapChanged(self): + self.sigColorMapChanged.emit(self) + + def addNew(self, name): + mode = self.fields[name].get('mode', 'range') + if mode == 'range': + self.addChild(RangeColorMapItem(name, self.fields[name])) + elif mode == 'enum': + self.addChild(EnumColorMapItem(name, self.fields[name])) + + def fieldNames(self): + return self.fields.keys() + + def setFields(self, fields): + self.fields = OrderedDict(fields) + #self.fields = fields + #self.fields.sort() + names = self.fieldNames() + self.setAddList(names) + + def map(self, data, mode='byte'): + colors = np.zeros((len(data),4)) + for item in self.children(): + if not item['Enabled']: + continue + chans = item.param('Channels..') + mask = np.empty((len(data), 4), dtype=bool) + for i,f in enumerate(['Red', 'Green', 'Blue', 'Alpha']): + mask[:,i] = chans[f] + + colors2 = item.map(data) + + op = item['Operation'] + if op == 'Add': + colors[mask] = colors[mask] + colors2[mask] + elif op == 'Multiply': + colors[mask] *= colors2[mask] + elif op == 'Overlay': + a = colors2[:,3:4] + c3 = colors * (1-a) + colors2 * a + c3[:,3:4] = colors[:,3:4] + (1-colors[:,3:4]) * a + colors = c3 + elif op == 'Set': + colors[mask] = colors2[mask] + + + colors = np.clip(colors, 0, 1) + if mode == 'byte': + colors = (colors * 255).astype(np.ubyte) + + return colors + + +class RangeColorMapItem(ptree.types.SimpleParameter): + def __init__(self, name, opts): + self.fieldName = name + units = opts.get('units', '') + ptree.types.SimpleParameter.__init__(self, + name=name, autoIncrementName=True, type='colormap', removable=True, renamable=True, + children=[ + #dict(name="Field", type='list', value=name, values=fields), + dict(name='Min', type='float', value=0.0, suffix=units, siPrefix=True), + dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True), + dict(name='Operation', type='list', value='Overlay', values=['Overlay', 'Add', 'Multiply', 'Set']), + dict(name='Channels..', type='group', expanded=False, children=[ + dict(name='Red', type='bool', value=True), + dict(name='Green', type='bool', value=True), + dict(name='Blue', type='bool', value=True), + dict(name='Alpha', type='bool', value=True), + ]), + dict(name='Enabled', type='bool', value=True), + dict(name='NaN', type='color'), + ]) + + 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') + + mask = np.isnan(data) | np.isinf(data) + nanColor = self['NaN'] + nanColor = (nanColor.red()/255., nanColor.green()/255., nanColor.blue()/255., nanColor.alpha()/255.) + colors[mask] = nanColor + + return colors + + +class EnumColorMapItem(ptree.types.GroupParameter): + def __init__(self, name, opts): + self.fieldName = name + vals = opts.get('values', []) + childs = [{'name': v, 'type': 'color'} for v in vals] + ptree.types.GroupParameter.__init__(self, + name=name, autoIncrementName=True, removable=True, renamable=True, + children=[ + dict(name='Values', type='group', children=childs), + dict(name='Operation', type='list', value='Overlay', values=['Overlay', 'Add', 'Multiply', 'Set']), + dict(name='Channels..', type='group', expanded=False, children=[ + dict(name='Red', type='bool', value=True), + dict(name='Green', type='bool', value=True), + dict(name='Blue', type='bool', value=True), + dict(name='Alpha', type='bool', value=True), + ]), + dict(name='Enabled', type='bool', value=True), + dict(name='Default', type='color'), + ]) + + def map(self, data): + data = data[self.fieldName] + colors = np.empty((len(data), 4)) + default = np.array(fn.colorTuple(self['Default'])) / 255. + colors[:] = default + + for v in self.param('Values'): + n = v.name() + mask = data == n + c = np.array(fn.colorTuple(v.value())) / 255. + colors[mask] = c + #scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1) + #cmap = self.value() + #colors = cmap.map(scaled, mode='float') + + #mask = np.isnan(data) | np.isinf(data) + #nanColor = self['NaN'] + #nanColor = (nanColor.red()/255., nanColor.green()/255., nanColor.blue()/255., nanColor.alpha()/255.) + #colors[mask] = nanColor + + return colors + + diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py new file mode 100644 index 00000000..a2e1a7b8 --- /dev/null +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -0,0 +1,115 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.parametertree as ptree +import numpy as np +from pyqtgraph.pgcollections import OrderedDict + +__all__ = ['DataFilterWidget'] + +class DataFilterWidget(ptree.ParameterTree): + """ + This class allows the user to filter multi-column data sets by specifying + multiple criteria + """ + + sigFilterChanged = QtCore.Signal(object) + + def __init__(self): + ptree.ParameterTree.__init__(self, showHeader=False) + self.params = DataFilterParameter() + + self.setParameters(self.params) + self.params.sigTreeStateChanged.connect(self.filterChanged) + + self.setFields = self.params.setFields + self.filterData = self.params.filterData + + def filterChanged(self): + self.sigFilterChanged.emit(self) + + def parameters(self): + return self.params + + +class DataFilterParameter(ptree.types.GroupParameter): + + sigFilterChanged = QtCore.Signal(object) + + def __init__(self): + self.fields = {} + ptree.types.GroupParameter.__init__(self, name='Data Filter', addText='Add filter..', addList=[]) + self.sigTreeStateChanged.connect(self.filterChanged) + + def filterChanged(self): + self.sigFilterChanged.emit(self) + + def addNew(self, name): + mode = self.fields[name].get('mode', 'range') + if mode == 'range': + self.addChild(RangeFilterItem(name, self.fields[name])) + elif mode == 'enum': + self.addChild(EnumFilterItem(name, self.fields[name])) + + + def fieldNames(self): + return self.fields.keys() + + def setFields(self, fields): + self.fields = OrderedDict(fields) + names = self.fieldNames() + self.setAddList(names) + + def filterData(self, data): + if len(data) == 0: + return data + return data[self.generateMask(data)] + + def generateMask(self, data): + mask = np.ones(len(data), dtype=bool) + if len(data) == 0: + return mask + for fp in self: + if fp.value() is False: + continue + mask &= fp.generateMask(data) + #key, mn, mx = fp.fieldName, fp['Min'], fp['Max'] + + #vals = data[key] + #mask &= (vals >= mn) + #mask &= (vals < mx) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections + return mask + +class RangeFilterItem(ptree.types.SimpleParameter): + def __init__(self, name, opts): + self.fieldName = name + units = opts.get('units', '') + ptree.types.SimpleParameter.__init__(self, + name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True, + children=[ + #dict(name="Field", type='list', value=name, values=fields), + dict(name='Min', type='float', value=0.0, suffix=units, siPrefix=True), + dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True), + ]) + + def generateMask(self, data): + vals = data[self.fieldName] + return (vals >= mn) & (vals < mx) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections + + +class EnumFilterItem(ptree.types.SimpleParameter): + def __init__(self, name, opts): + self.fieldName = name + vals = opts.get('values', []) + childs = [{'name': v, 'type': 'bool', 'value': True} for v in vals] + ptree.types.SimpleParameter.__init__(self, + name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True, + children=childs) + + def generateMask(self, data): + vals = data[self.fieldName] + mask = np.ones(len(data), dtype=bool) + for c in self: + if c.value() is True: + continue + key = c.name() + mask &= vals != key + return mask diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py new file mode 100644 index 00000000..85f5489a --- /dev/null +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -0,0 +1,183 @@ +from pyqtgraph.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 +import numpy as np +from pyqtgraph.pgcollections import OrderedDict + +__all__ = ['ScatterPlotWidget'] + +class ScatterPlotWidget(QtGui.QSplitter): + """ + Given a record array, display a scatter plot of a specific set of data. + This widget includes controls for selecting the columns to plot, + filtering data, and determining symbol color and shape. This widget allows + the user to explore relationships between columns in a record array. + + The widget consists of four components: + + 1) A list of column names from which the user may select 1 or 2 columns + to plot. If one column is selected, the data for that column will be + plotted in a histogram-like manner by using :func:`pseudoScatter() + `. If two columns are selected, then the + scatter plot will be generated with x determined by the first column + that was selected and y by the second. + 2) A DataFilter that allows the user to select a subset of the data by + specifying multiple selection criteria. + 3) A ColorMap that allows the user to determine how points are colored by + specifying multiple criteria. + 4) A PlotWidget for displaying the data. + """ + def __init__(self, parent=None): + QtGui.QSplitter.__init__(self, QtCore.Qt.Horizontal) + self.ctrlPanel = QtGui.QSplitter(QtCore.Qt.Vertical) + self.addWidget(self.ctrlPanel) + self.fieldList = QtGui.QListWidget() + self.fieldList.setSelectionMode(self.fieldList.ExtendedSelection) + self.ptree = ptree.ParameterTree(showHeader=False) + self.filter = DataFilterParameter() + self.colorMap = ColorMapParameter() + self.params = ptree.Parameter.create(name='params', type='group', children=[self.filter, self.colorMap]) + self.ptree.setParameters(self.params, showTop=False) + + self.plot = PlotWidget() + self.ctrlPanel.addWidget(self.fieldList) + self.ctrlPanel.addWidget(self.ptree) + self.addWidget(self.plot) + + self.data = None + self.style = dict(pen=None, symbol='o') + + self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged) + self.filter.sigFilterChanged.connect(self.filterChanged) + self.colorMap.sigColorMapChanged.connect(self.updatePlot) + + def setFields(self, fields): + """ + Set the list of field names/units to be processed. + Format is: [(name, units), ...] + """ + self.fields = OrderedDict(fields) + self.fieldList.clear() + for f,opts in fields: + item = QtGui.QListWidgetItem(f) + item.opts = opts + item = self.fieldList.addItem(item) + self.filter.setFields(fields) + self.colorMap.setFields(fields) + + def setData(self, data): + """ + Set the data to be processed and displayed. + Argument must be a numpy record array. + """ + self.data = data + self.filtered = None + self.updatePlot() + + def fieldSelectionChanged(self): + sel = self.fieldList.selectedItems() + if len(sel) > 2: + self.fieldList.blockSignals(True) + try: + for item in sel[1:-1]: + item.setSelected(False) + finally: + self.fieldList.blockSignals(False) + + self.updatePlot() + + def filterChanged(self, f): + self.filtered = None + self.updatePlot() + + def updatePlot(self): + self.plot.clear() + if self.data is None: + return + + if self.filtered is None: + self.filtered = self.filter.filterData(self.data) + data = self.filtered + if len(data) == 0: + return + + colors = np.array([fn.mkBrush(*x) for x in self.colorMap.map(data)]) + + style = self.style.copy() + + ## Look up selected columns and units + sel = list([str(item.text()) for item in self.fieldList.selectedItems()]) + units = list([item.opts.get('units', '') for item in self.fieldList.selectedItems()]) + if len(sel) == 0: + self.plot.setTitle('') + return + + + if len(sel) == 1: + self.plot.setLabels(left=('N', ''), bottom=(sel[0], units[0]), title='') + if len(data) == 0: + return + x = data[sel[0]] + #if x.dtype.kind == 'f': + #mask = ~np.isnan(x) + #else: + #mask = np.ones(len(x), dtype=bool) + #x = x[mask] + #style['symbolBrush'] = colors[mask] + y = None + elif len(sel) == 2: + self.plot.setLabels(left=(sel[1],units[1]), bottom=(sel[0],units[0])) + if len(data) == 0: + return + + xydata = [] + for ax in [0,1]: + d = data[sel[ax]] + ## scatter catecorical values just a bit so they show up better in the scatter plot. + #if sel[ax] in ['MorphologyBSMean', 'MorphologyTDMean', 'FIType']: + #d += np.random.normal(size=len(cells), scale=0.1) + xydata.append(d) + x,y = xydata + #mask = np.ones(len(x), dtype=bool) + #if x.dtype.kind == 'f': + #mask |= ~np.isnan(x) + #if y.dtype.kind == 'f': + #mask |= ~np.isnan(y) + #x = x[mask] + #y = y[mask] + #style['symbolBrush'] = colors[mask] + + ## convert enum-type fields to float, set axis labels + xy = [x,y] + for i in [0,1]: + axis = self.plot.getAxis(['bottom', 'left'][i]) + if xy[i] is not None and xy[i].dtype.kind in ('S', 'O'): + vals = self.fields[sel[i]].get('values', list(set(xy[i]))) + xy[i] = np.array([vals.index(x) if x in vals else None for x in xy[i]], dtype=float) + axis.setTicks([list(enumerate(vals))]) + else: + axis.setTicks(None) # reset to automatic ticking + x,y = xy + + ## mask out any nan values + mask = np.ones(len(x), dtype=bool) + if x.dtype.kind == 'f': + mask &= ~np.isnan(x) + if y is not None and y.dtype.kind == 'f': + mask &= ~np.isnan(y) + x = x[mask] + style['symbolBrush'] = colors[mask] + + ## Scatter y-values for a histogram-like appearance + if y is None: + y = fn.pseudoScatter(x) + else: + y = y[mask] + + + self.plot.plot(x, y, **style) + + From 510b1b3fadfe32f167400435218fc9a4e5e27d23 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 10 Feb 2013 14:12:56 -0500 Subject: [PATCH 11/22] Added some documentation for ArrowItem --- pyqtgraph/graphicsItems/ArrowItem.py | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index 22d0065b..0c6c0718 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -12,6 +12,10 @@ class ArrowItem(QtGui.QGraphicsPathItem): def __init__(self, **opts): + """ + Arrows can be initialized with any keyword arguments accepted by + the setStyle() method. + """ QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None)) if 'size' in opts: opts['headLen'] = opts['size'] @@ -40,6 +44,32 @@ class ArrowItem(QtGui.QGraphicsPathItem): self.moveBy(*self.opts['pos']) def setStyle(self, **opts): + """ + 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 shaft. + tailLen Length of the arrow tail, measured from the base + of the arrow head to the tip 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 = opts opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) From 97da32c4ecdbdaf6d381ffd8f7e98c541519ae91 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 10 Feb 2013 14:16:21 -0500 Subject: [PATCH 12/22] minor edit --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 86298ea2..467649e8 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1043,7 +1043,7 @@ def colorToAlpha(data, color): def arrayToQPath(x, y, connect='all'): - """Convert an array of x,y coordinats to QPath as efficiently as possible. + """Convert an array of x,y coordinats to QPainterPath as efficiently as possible. The *connect* argument may be 'all', indicating that each point should be connected to the next; 'pairs', indicating that each pair of points should be connected, or an array of int32 values (0 or 1) indicating From 6e5c5e402b60b5338df4b2a18476abd8efee71d0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 10 Feb 2013 17:45:16 -0500 Subject: [PATCH 13/22] merged many new features from ACQ4 --- doc/source/apireference.rst | 2 + doc/source/colormap.rst | 8 ++ doc/source/functions.rst | 2 + doc/source/graphicsItems/graphitem.rst | 8 ++ doc/source/graphicsItems/index.rst | 1 + doc/source/index.rst | 1 + doc/source/qtcrashcourse.rst | 66 ++++++++++++- doc/source/widgets/colormapwidget.rst | 12 +++ doc/source/widgets/index.rst | 2 + doc/source/widgets/scatterplotwidget.rst | 8 ++ pyqtgraph/__init__.py | 1 + pyqtgraph/colormap.py | 109 +++++++++------------- pyqtgraph/functions.py | 8 +- pyqtgraph/graphicsItems/GraphItem.py | 16 +++- pyqtgraph/parametertree/parameterTypes.py | 18 +++- pyqtgraph/widgets/ColorMapWidget.py | 40 +++++++- pyqtgraph/widgets/GradientWidget.py | 20 +++- pyqtgraph/widgets/ScatterPlotWidget.py | 4 +- 18 files changed, 240 insertions(+), 86 deletions(-) create mode 100644 doc/source/colormap.rst create mode 100644 doc/source/graphicsItems/graphitem.rst create mode 100644 doc/source/widgets/colormapwidget.rst create mode 100644 doc/source/widgets/scatterplotwidget.rst diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst index 777e6ad4..9742568a 100644 --- a/doc/source/apireference.rst +++ b/doc/source/apireference.rst @@ -10,5 +10,7 @@ Contents: graphicsItems/index widgets/index 3dgraphics/index + colormap parametertree/index graphicsscene/index + flowchart/index diff --git a/doc/source/colormap.rst b/doc/source/colormap.rst new file mode 100644 index 00000000..86ffe4a2 --- /dev/null +++ b/doc/source/colormap.rst @@ -0,0 +1,8 @@ +ColorMap +======== + +.. autoclass:: pyqtgraph.ColorMap + :members: + + .. automethod:: pyqtgraph.ColorMap.__init__ + diff --git a/doc/source/functions.rst b/doc/source/functions.rst index 65f2c202..966fd926 100644 --- a/doc/source/functions.rst +++ b/doc/source/functions.rst @@ -91,6 +91,8 @@ Mesh Generation Functions Miscellaneous Functions ----------------------- +.. autofunction:: pyqtgraph.arrayToQPath + .. autofunction:: pyqtgraph.pseudoScatter .. autofunction:: pyqtgraph.systemInfo diff --git a/doc/source/graphicsItems/graphitem.rst b/doc/source/graphicsItems/graphitem.rst new file mode 100644 index 00000000..95e31e28 --- /dev/null +++ b/doc/source/graphicsItems/graphitem.rst @@ -0,0 +1,8 @@ +GraphItem +========= + +.. autoclass:: pyqtgraph.GraphItem + :members: + + .. automethod:: pyqtgraph.GraphItem.__init__ + diff --git a/doc/source/graphicsItems/index.rst b/doc/source/graphicsItems/index.rst index 70786d20..b15c205c 100644 --- a/doc/source/graphicsItems/index.rst +++ b/doc/source/graphicsItems/index.rst @@ -12,6 +12,7 @@ Contents: plotdataitem plotitem imageitem + graphitem viewbox linearregionitem infiniteline diff --git a/doc/source/index.rst b/doc/source/index.rst index 9727aaab..08c5528e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -15,6 +15,7 @@ Contents: mouse_interaction how_to_use installation + qtcrashcourse plotting images 3dgraphics diff --git a/doc/source/qtcrashcourse.rst b/doc/source/qtcrashcourse.rst index 58b88de4..ca2da797 100644 --- a/doc/source/qtcrashcourse.rst +++ b/doc/source/qtcrashcourse.rst @@ -3,20 +3,76 @@ 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. - QWidgets and Layouts -------------------- +A Qt GUI is almost always composed of a few basic components: + +* A window. This is often provided by QMainWindow, but note that all QWidgets can be displayed in their window by simply calling widget.show() if the widget does not have a parent. +* 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. + + +Example:: + + from PyQt4 import QtGui # (the example applies equally well to PySide) + import pyqtgraph as pg + + ## Always start by initializing Qt (only once per application) + app = QtGui.QApplication([]) + + ## Define a top-level widget to hold everything + w = QtGui.QWidget() + + ## Create some widgets to be placed inside + btn = QtGui.QPushButton('press me') + text = QtGui.QLineEdit('enter text') + listw = QtGui.QListWidget() + plot = pg.PlotWidget() + + ## Create a grid layout to manage the widgets size and position + layout = QtGui.QGridLayout() + w.setLayout(layout) + + ## Add widgets to the layout in their proper positions + layout.addWidget(btn, 0, 0) # button goes in upper-left + layout.addWidget(text, 1, 0) # text edit goes in middle-left + layout.addWidget(listw, 2, 0) # list widget goes in bottom-left + layout.addWidget(plot, 0, 1, 3, 1) # plot goes on right side, spanning 3 rows + + ## Display the widget as a new window + w.show() + + ## Start the Qt event loop + app.exec_() + +More complex interfaces may be designed graphically using Qt Designer, which allows you to simply drag widgets into your window to define its appearance. + + +Naming Conventions +------------------ + +Virtually every class in pyqtgraph is an extension of base classes provided by Qt. When reading the documentation, remember that all of Qt's classes start with the letter 'Q', whereas pyqtgraph's classes do not. When reading through the methods for any class, it is often helpful to see which Qt base classes are used and look through the Qt documentation as well. + +Most of Qt's classes define signals which can be difficult to tell apart from regular methods. Almost all signals explicity defined by pyqtgraph are named beginning with 'sig' to indicate that these signals are not defined at the Qt level. + +In most cases, classes which end in 'Widget' are subclassed from QWidget and can therefore be used as a GUI element in a Qt window. Classes which end in 'Item' are subclasses of QGraphicsItem and can only be displayed within a QGraphicsView instance (such as GraphicsLayoutWidget or PlotWidget). + + Signals, Slots, and Events -------------------------- +[ to be continued.. please post a request on the pyqtgraph forum if you'd like to read more ] + GraphicsView and GraphicsItems ------------------------------ -Coordinate Systems ------------------- +Coordinate Systems and Transformations +-------------------------------------- Mouse and Keyboard Input @@ -26,3 +82,7 @@ Mouse and Keyboard Input QTimer, the Event Loop, and Multi-Threading ------------------------------------------- + +Multi-threading vs Multi-processing in Qt +----------------------------------------- + diff --git a/doc/source/widgets/colormapwidget.rst b/doc/source/widgets/colormapwidget.rst new file mode 100644 index 00000000..255ca238 --- /dev/null +++ b/doc/source/widgets/colormapwidget.rst @@ -0,0 +1,12 @@ +ColorMapWidget +============== + +.. autoclass:: pyqtgraph.ColorMapWidget + :members: + + .. automethod:: pyqtgraph.ColorMapWidget.__init__ + + .. automethod:: pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.setFields + + .. automethod:: pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.map + \ No newline at end of file diff --git a/doc/source/widgets/index.rst b/doc/source/widgets/index.rst index 913557b7..7e6973a2 100644 --- a/doc/source/widgets/index.rst +++ b/doc/source/widgets/index.rst @@ -17,6 +17,8 @@ Contents: gradientwidget histogramlutwidget parametertree + colormapwidget + scatterplotwidget graphicsview rawimagewidget datatreewidget diff --git a/doc/source/widgets/scatterplotwidget.rst b/doc/source/widgets/scatterplotwidget.rst new file mode 100644 index 00000000..c9d40764 --- /dev/null +++ b/doc/source/widgets/scatterplotwidget.rst @@ -0,0 +1,8 @@ +ScatterPlotWidget +================= + +.. autoclass:: pyqtgraph.ScatterPlotWidget + :members: + + .. automethod:: pyqtgraph.ScatterPlotWidget.__init__ + diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index da197aa6..b72777ed 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -187,6 +187,7 @@ from .SRTTransform3D import SRTTransform3D from .functions import * from .graphicsWindows import * from .SignalProxy import * +from .colormap import * from .ptime import time diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index c7e683fb..d6169209 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -3,6 +3,25 @@ import scipy.interpolate from pyqtgraph.Qt import QtGui, QtCore class ColorMap(object): + """ + A ColorMap defines a relationship between a scalar value and a range of colors. + ColorMaps are commonly used for false-coloring monochromatic images, coloring + scatter-plot points, and coloring surface plots by height. + + Each color map is defined by a set of colors, each corresponding to a + particular scalar value. For example: + + | 0.0 -> black + | 0.2 -> red + | 0.6 -> yellow + | 1.0 -> white + + The colors for intermediate values are determined by interpolating between + the two nearest colors in either RGB or HSV color space. + + To provide user-defined color mappings, see :class:`GradientWidget `. + """ + ## color interpolation modes RGB = 1 @@ -54,7 +73,16 @@ class ColorMap(object): def map(self, data, mode='byte'): """ + Return an array of colors corresponding to the values in *data*. Data must be either a scalar position or an array (any shape) of positions. + + The *mode* argument determines the type of data returned: + + =========== =============================================================== + byte (default) Values are returned as 0-255 unsigned bytes. + float Values are returned as 0.0-1.0 floats. + qcolor Values are returned as an array of QColor objects. + =========== =============================================================== """ if isinstance(mode, basestring): mode = self.enumMap[mode.lower()] @@ -80,16 +108,19 @@ class ColorMap(object): return interp def mapToQColor(self, data): + """Convenience function; see :func:`map() `.""" return self.map(data, mode=self.QCOLOR) def mapToByte(self, data): + """Convenience function; see :func:`map() `.""" return self.map(data, mode=self.BYTE) def mapToFloat(self, data): + """Convenience function; see :func:`map() `.""" return self.map(data, mode=self.FLOAT) def getGradient(self, p1=None, p2=None): - """Return a QLinearGradient object.""" + """Return a QLinearGradient object spanning from QPoints p1 to p2.""" if p1 == None: p1 = QtCore.QPointF(0,0) if p2 == None: @@ -119,7 +150,7 @@ class ColorMap(object): return g def getColors(self, mode=None): - """Return list of all colors converted to the specified mode. + """Return list of all color stops converted to the specified mode. If mode is None, then no conversion is done.""" if isinstance(mode, basestring): mode = self.enumMap[mode.lower()] @@ -158,75 +189,19 @@ class ColorMap(object): self.stopsCache[mode] = (self.pos, color) return self.stopsCache[mode] - #def getColor(self, x, toQColor=True): - #""" - #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. - #============= ================================================================== - #""" - #ticks = self.listTicks() - #if x <= ticks[0][1]: - #c = ticks[0][0].color - #if toQColor: - #return QtGui.QColor(c) # always copy colors before handing them out - #else: - #return (c.red(), c.green(), c.blue(), c.alpha()) - #if x >= ticks[-1][1]: - #c = ticks[-1][0].color - #if toQColor: - #return QtGui.QColor(c) # always copy colors before handing them out - #else: - #return (c.red(), c.green(), c.blue(), c.alpha()) - - #x2 = ticks[0][1] - #for i in range(1,len(ticks)): - #x1 = x2 - #x2 = ticks[i][1] - #if x1 <= x and x2 >= x: - #break - - #dx = (x2-x1) - #if dx == 0: - #f = 0. - #else: - #f = (x-x1) / dx - #c1 = ticks[i-1][0].color - #c2 = ticks[i][0].color - #if self.colorMode == 'rgb': - #r = c1.red() * (1.-f) + c2.red() * f - #g = c1.green() * (1.-f) + c2.green() * f - #b = c1.blue() * (1.-f) + c2.blue() * f - #a = c1.alpha() * (1.-f) + c2.alpha() * f - #if toQColor: - #return QtGui.QColor(int(r), int(g), int(b), int(a)) - #else: - #return (r,g,b,a) - #elif self.colorMode == 'hsv': - #h1,s1,v1,_ = c1.getHsv() - #h2,s2,v2,_ = c2.getHsv() - #h = h1 * (1.-f) + h2 * f - #s = s1 * (1.-f) + s2 * f - #v = v1 * (1.-f) + v2 * f - #c = QtGui.QColor() - #c.setHsv(h,s,v) - #if toQColor: - #return c - #else: - #return (c.red(), c.green(), c.blue(), c.alpha()) - def getLookupTable(self, start=0.0, stop=1.0, nPts=512, alpha=None, mode='byte'): """ 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, it will be automatically determined. + 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): @@ -249,7 +224,9 @@ class ColorMap(object): return np.any(self.color[:,3] != max) def isMapTrivial(self): - """Return True if the gradient has exactly two stops in it: black at 0.0 and white at 1.0""" + """ + Return True if the gradient has exactly two stops in it: black at 0.0 and white at 1.0. + """ if len(self.pos) != 2: return False if self.pos[0] != 0.0 or self.pos[1] != 1.0: diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 467649e8..9e35b35b 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -566,8 +566,8 @@ 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. + 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.") @@ -583,8 +583,8 @@ def solve3DTransform(points1, points2): 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. + 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]:: diff --git a/pyqtgraph/graphicsItems/GraphItem.py b/pyqtgraph/graphicsItems/GraphItem.py index 9a0f1244..bf03fef6 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -8,8 +8,9 @@ __all__ = ['GraphItem'] class GraphItem(GraphicsObject): - """A GraphItem displays graph information (as in 'graph theory', not 'graphics') as - a set of nodes connected by lines. + """A GraphItem displays graph information as + a set of nodes connected by lines (as in 'graph theory', not 'graphics'). + Useful for drawing networks, trees, etc. """ def __init__(self, **kwds): @@ -28,19 +29,24 @@ class GraphItem(GraphicsObject): ============ ========================================================= Arguments - pos (N,2) array of the positions of each node in the graph + 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). + 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 ScatterPlotItem + ``**opts`` All other keyword arguments are given to + :func:`ScatterPlotItem.setData() ` to affect the appearance of nodes (symbol, size, brush, etc.) ============ ========================================================= diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 84db9f06..28e1e618 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -14,10 +14,20 @@ class WidgetParameterItem(ParameterItem): """ ParameterTree item with: - - label in second column for displaying value - - simple widget for editing value (displayed instead of label when item is selected) - - button that resets value to default - - provides SpinBox, CheckBox, LineEdit, and ColorButton types + * label in second column for displaying value + * 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 ` + ================= ============================================================= This class can be subclassed by overriding makeWidget() to provide a custom widget. """ diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index 69a5e10a..619d639a 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -9,9 +9,14 @@ __all__ = ['ColorMapWidget'] class ColorMapWidget(ptree.ParameterTree): """ This class provides a widget allowing the user to customize color mapping - for multi-column data. - """ + for multi-column data. Given a list of field names, the user may specify + multiple criteria for assigning colors to each record in a numpy record array. + Multiple criteria are evaluated and combined into a single color for each + record by user-defined compositing methods. + For simpler color mapping using a single gradient editor, see + :class:`GradientWidget ` + """ sigColorMapChanged = QtCore.Signal(object) def __init__(self): @@ -51,6 +56,25 @@ class ColorMapParameter(ptree.types.GroupParameter): return self.fields.keys() def setFields(self, fields): + """ + Set the list of fields to be used by the mapper. + + The format of *fields* is:: + + [ (fieldName, {options}), ... ] + + ============== ============================================================ + Field Options: + mode Either 'range' or 'enum' (default is range). For 'range', + The user may specify a gradient of colors to be applied + linearly across a specific range of values. For 'enum', + the user specifies a single color for each unique value + (see *values* option). + units String indicating the units of the data for this field. + values List of unique values for which the user may assign a + color when mode=='enum'. + ============== ============================================================ + """ self.fields = OrderedDict(fields) #self.fields = fields #self.fields.sort() @@ -58,6 +82,18 @@ class ColorMapParameter(ptree.types.GroupParameter): self.setAddList(names) def map(self, data, mode='byte'): + """ + 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. + ========= ================================================================= + """ colors = np.zeros((len(data),4)) for item in self.children(): if not item['Enabled']: diff --git a/pyqtgraph/widgets/GradientWidget.py b/pyqtgraph/widgets/GradientWidget.py index 2b9b52d2..1723a94b 100644 --- a/pyqtgraph/widgets/GradientWidget.py +++ b/pyqtgraph/widgets/GradientWidget.py @@ -9,11 +9,27 @@ __all__ = ['TickSlider', 'GradientWidget', 'BlackWhiteSlider'] class GradientWidget(GraphicsView): - + """ + Widget displaying an editable color gradient. The user may add, move, recolor, + or remove colors from the gradient. Additionally, a context menu allows the + user to select from pre-defined gradients. + """ sigGradientChanged = QtCore.Signal(object) sigGradientChangeFinished = QtCore.Signal(object) def __init__(self, parent=None, orientation='bottom', *args, **kargs): + """ + The *orientation* argument may be 'bottom', 'top', 'left', or 'right' + indicating whether the gradient is displayed horizontally (top, bottom) + or vertically (left, right) and on what side of the gradient the editable + ticks will appear. + + All other arguments are passed to + :func:`GradientEditorItem.__init__ `. + + Note: For convenience, this class wraps methods from + :class:`GradientEditorItem `. + """ GraphicsView.__init__(self, parent, useOpenGL=False, background=None) self.maxDim = 31 kargs['tickPen'] = 'k' @@ -32,6 +48,8 @@ class GradientWidget(GraphicsView): #self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, True) def setOrientation(self, ort): + """Set the orientation of the widget. May be one of 'bottom', 'top', + 'left', or 'right'.""" self.item.setOrientation(ort) self.orientation = ort self.setMaxDim() diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index 85f5489a..2e1c1918 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -57,7 +57,9 @@ class ScatterPlotWidget(QtGui.QSplitter): def setFields(self, fields): """ Set the list of field names/units to be processed. - Format is: [(name, units), ...] + + The format of *fields* is the same as used by + :func:`ColorMapWidget.setFields ` """ self.fields = OrderedDict(fields) self.fieldList.clear() From 22bc2333a8863a3bec168277bac6b90d9016576a Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 10 Feb 2013 21:04:00 -0500 Subject: [PATCH 14/22] bugfixes --- examples/__main__.py | 1 + pyqtgraph/__init__.py | 2 +- pyqtgraph/graphicsItems/GraphicsItem.py | 3 ++- pyqtgraph/graphicsItems/ScatterPlotItem.py | 6 ++++-- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 5 ++++- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index d8456781..80f23f7d 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -27,6 +27,7 @@ examples = OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), ('IsocurveItem', 'isocurve.py'), + ('GraphItem', 'GraphItem.py'), ('ImageItem - video', 'ImageItem.py'), ('ImageItem - draw', 'Draw.py'), ('Region-of-Interest', 'ROIExamples.py'), diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index b72777ed..d3aefa83 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.5' +__version__ = None ### import all the goodies and add some helper functions for easy CLI use diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 1795e79e..3a63afa7 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -204,7 +204,8 @@ class GraphicsItem(object): return tuple(map(Point, self._pixelVectorCache[1])) ## return a *copy* ## check global cache - key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32()) + #key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32()) + key = (dt.m11(), dt.m21(), dt.m12(), dt.m22()) pv = self._pixelVectorGlobalCache.get(key, None) if direction is None and pv is not None: self._pixelVectorCache = [dt, pv] diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 5af82a00..7c204479 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -472,8 +472,8 @@ class ScatterPlotItem(GraphicsObject): if isinstance(symbol, np.ndarray) or isinstance(symbol, list): symbols = symbol - if kargs['mask'] is not None: - symbols = symbols[kargs['mask']] + if mask is not None: + symbols = symbols[mask] if len(symbols) != len(dataSet): raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(dataSet))) dataSet['symbol'] = symbols @@ -554,6 +554,7 @@ class ScatterPlotItem(GraphicsObject): #rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec)) if invalidate: self.invalidate() + self.informViewBoundsChanged() def getSpotOpts(self, recs, scale=1.0): if recs.ndim == 0: @@ -671,6 +672,7 @@ class ScatterPlotItem(GraphicsObject): GraphicsObject.viewTransformChanged(self) self.bounds = [None, None] self.fragments = None + self.informViewBoundsChanged() def generateFragments(self): tr = self.deviceTransform() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 37f21182..44f98e77 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -298,9 +298,11 @@ class ViewBox(GraphicsWidget): def resizeEvent(self, ev): #self.setRange(self.range, padding=0) #self.updateAutoRange() + self._itemBoundsCache.clear() self.updateMatrix() self.sigStateChanged.emit(self) self.background.setRect(self.rect()) + #self.linkedXChanged() #self.linkedYChanged() @@ -728,7 +730,8 @@ class ViewBox(GraphicsWidget): def itemBoundsChanged(self, item): self._itemBoundsCache.pop(item, None) - self.updateAutoRange() + if item in self.addedItems: + self.updateAutoRange() def invertY(self, b=True): """ From c80bfb334cb78303b866655149f24060aa392d89 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 11 Feb 2013 16:38:13 -0500 Subject: [PATCH 15/22] Fix for new QImage API in PyQt 4.9.6 --- pyqtgraph/functions.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 467649e8..6fc4cbd8 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -951,8 +951,15 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): ch = ctypes.c_char.from_buffer(imgData, 0) img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) else: - addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0)) - img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) + #addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0)) + ## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was) + ## So we first attempt the 4.9.6 API, then fall back to 4.9.3 + addr = ctypes.c_char.from_buffer(imgData, 0) + try: + img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) + except TypeError: + addr = ctypes.addressof(addr) + img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) img.data = imgData return img #try: From 4cec9ff044292a408a3a442f67b289d5e3df52cb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 11 Feb 2013 21:45:41 -0500 Subject: [PATCH 16/22] Added ErrorBarItem to repository --- examples/ErrorBarItem.py | 30 ++++++ pyqtgraph/graphicsItems/ErrorBarItem.py | 133 ++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 examples/ErrorBarItem.py create mode 100644 pyqtgraph/graphicsItems/ErrorBarItem.py diff --git a/examples/ErrorBarItem.py b/examples/ErrorBarItem.py new file mode 100644 index 00000000..9c1bbf1e --- /dev/null +++ b/examples/ErrorBarItem.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +""" +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 +import numpy as np + +import pyqtgraph as pg +import numpy as np + +x = np.arange(10) +y = np.arange(10) %3 +top = np.linspace(1.0, 3.0, 10) +bottom = np.linspace(2, 0.5, 10) + +plt = pg.plot() +err = pg.ErrorBarItem(x=x, y=y, top=top, bottom=bottom, beam=0.5) +plt.addItem(err) +plt.plot(x, y, symbol='o') + +## 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/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py new file mode 100644 index 00000000..ccb38774 --- /dev/null +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -0,0 +1,133 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +from .GraphicsObject import GraphicsObject + +__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. + """ + GraphicsObject.__init__(self) + self.opts = dict( + x=None, + y=None, + height=None, + width=None, + top=None, + bottom=None, + left=None, + right=None, + beam=None, + pen=None + ) + self.setOpts(**opts) + + def setOpts(self, **opts): + self.opts.update(opts) + self.path = None + self.update() + self.informViewBoundsChanged() + + def drawPath(self): + p = QtGui.QPainterPath() + + x, y = self.opts['x'], self.opts['y'] + if x is None or y is None: + return + + beam = self.opts['beam'] + + + height, top, bottom = self.opts['height'], self.opts['top'], self.opts['bottom'] + if height is not None or top is not None or bottom is not None: + ## draw vertical error bars + if height is not None: + y1 = y - height/2. + y2 = y + height/2. + else: + if bottom is None: + y1 = y + else: + y1 = y - bottom + if top is None: + y2 = y + else: + y2 = y + top + + for i in range(len(x)): + p.moveTo(x[i], y1[i]) + p.lineTo(x[i], y2[i]) + + if beam is not None and beam > 0: + x1 = x - beam/2. + x2 = x + beam/2. + if height is not None or top is not None: + for i in range(len(x)): + p.moveTo(x1[i], y2[i]) + p.lineTo(x2[i], y2[i]) + if height is not None or bottom is not None: + for i in range(len(x)): + p.moveTo(x1[i], y1[i]) + p.lineTo(x2[i], y1[i]) + + width, right, left = self.opts['width'], self.opts['right'], self.opts['left'] + if width is not None or right is not None or left is not None: + ## draw vertical error bars + if width is not None: + x1 = x - width/2. + x2 = x + width/2. + else: + if left is None: + x1 = x + else: + x1 = x - left + if right is None: + x2 = x + else: + x2 = x + right + + for i in range(len(x)): + p.moveTo(x1[i], y[i]) + p.lineTo(x2[i], y[i]) + + if beam is not None and beam > 0: + y1 = y - beam/2. + y2 = y + beam/2. + if width is not None or right is not None: + for i in range(len(x)): + p.moveTo(x2[i], y1[i]) + p.lineTo(x2[i], y2[i]) + if width is not None or left is not None: + for i in range(len(x)): + p.moveTo(x1[i], y1[i]) + p.lineTo(x1[i], y2[i]) + + self.path = p + self.prepareGeometryChange() + + + def paint(self, p, *args): + if self.path is None: + self.drawPath() + pen = self.opts['pen'] + if pen is None: + pen = pg.getConfigOption('foreground') + p.setPen(pg.mkPen(pen)) + p.drawPath(self.path) + + def boundingRect(self): + if self.path is None: + return QtCore.QRectF() + return self.path.boundingRect() + + \ No newline at end of file From 93a5753f5d3e06c5dfb1e6c29ffa0d9b8484e319 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 12 Feb 2013 19:15:45 -0500 Subject: [PATCH 17/22] Fixed auto ranging for scatter plots --- examples/ErrorBarItem.py | 4 +- examples/ScatterPlot.py | 3 +- examples/__main__.py | 1 + pyqtgraph/exporters/Exporter.py | 2 +- pyqtgraph/graphicsItems/ErrorBarItem.py | 2 +- pyqtgraph/graphicsItems/ScatterPlotItem.py | 42 ++++--- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 127 +++++++++++++-------- 7 files changed, 115 insertions(+), 66 deletions(-) diff --git a/examples/ErrorBarItem.py b/examples/ErrorBarItem.py index 9c1bbf1e..816e8474 100644 --- a/examples/ErrorBarItem.py +++ b/examples/ErrorBarItem.py @@ -13,6 +13,8 @@ import numpy as np import pyqtgraph as pg import numpy as np +pg.setConfigOptions(antialias=True) + x = np.arange(10) y = np.arange(10) %3 top = np.linspace(1.0, 3.0, 10) @@ -21,7 +23,7 @@ bottom = np.linspace(2, 0.5, 10) plt = pg.plot() err = pg.ErrorBarItem(x=x, y=y, top=top, bottom=bottom, beam=0.5) plt.addItem(err) -plt.plot(x, y, symbol='o') +plt.plot(x, y, symbol='o', pen={'color': 0.8, 'width': 2}) ## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 03e849ad..2a15164f 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -59,7 +59,6 @@ pos = np.random.normal(size=(2,n), scale=1e-5) spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)] s2.addPoints(spots) w2.addItem(s2) -w2.setRange(s2.boundingRect()) s2.sigClicked.connect(clicked) @@ -71,7 +70,7 @@ s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to tr spots3 = [] for i in range(10): for j in range(10): - spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'brush':pg.intColor(i*10+j, 100)}) + spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'pen': {'color': 'w', 'width': 8}, 'brush':pg.intColor(i*10+j, 100)}) s3.addPoints(spots3) w3.addItem(s3) s3.sigClicked.connect(clicked) diff --git a/examples/__main__.py b/examples/__main__.py index 80f23f7d..096edba0 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -28,6 +28,7 @@ examples = OrderedDict([ #('PlotItem', 'PlotItem.py'), ('IsocurveItem', 'isocurve.py'), ('GraphItem', 'GraphItem.py'), + ('ErrorBarItem', 'ErrorBarItem.py'), ('ImageItem - video', 'ImageItem.py'), ('ImageItem - draw', 'Draw.py'), ('Region-of-Interest', 'ROIExamples.py'), diff --git a/pyqtgraph/exporters/Exporter.py b/pyqtgraph/exporters/Exporter.py index 81930670..f5a93088 100644 --- a/pyqtgraph/exporters/Exporter.py +++ b/pyqtgraph/exporters/Exporter.py @@ -66,7 +66,7 @@ class Exporter(object): if selectedExt is not None: selectedExt = selectedExt.groups()[0].lower() if ext != selectedExt: - fileName = fileName + selectedExt + fileName = fileName + '.' + selectedExt.lstrip('.') self.export(fileName=fileName, **self.fileDialog.opts) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index ccb38774..656b9e2e 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -127,7 +127,7 @@ class ErrorBarItem(GraphicsObject): def boundingRect(self): if self.path is None: - return QtCore.QRectF() + self.drawPath() return self.path.boundingRect() \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 7c204479..93653869 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -554,7 +554,6 @@ class ScatterPlotItem(GraphicsObject): #rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec)) if invalidate: self.invalidate() - self.informViewBoundsChanged() def getSpotOpts(self, recs, scale=1.0): if recs.ndim == 0: @@ -632,23 +631,26 @@ class ScatterPlotItem(GraphicsObject): if frac >= 1.0: ## increase size of bounds based on spot size and pen width #px = self.pixelLength(Point(1, 0) if ax == 0 else Point(0, 1)) ## determine length of pixel along this axis - px = self.pixelVectors()[ax] - if px is None: - px = 0 - else: - px = px.length() - minIndex = np.argmin(d) - maxIndex = np.argmax(d) - minVal = d[minIndex] - maxVal = d[maxIndex] - spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth) - self.bounds[ax] = (minVal-spotSize, maxVal+spotSize) + #px = self.pixelVectors()[ax] + #if px is None: + #px = 0 + #else: + #px = px.length() + #minIndex = np.argmin(d) + #maxIndex = np.argmax(d) + #minVal = d[minIndex] + #maxVal = d[maxIndex] + #spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth) + #self.bounds[ax] = (minVal-spotSize, maxVal+spotSize) + self.bounds[ax] = (d.min() - 0.5*self._maxSpotWidth, d.max() + 0.5*self._maxSpotWidth) return self.bounds[ax] elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + def pixelPadding(self): + return self._maxSpotPxWidth #def defaultSpotPixmap(self): ### Return the default spot pixmap @@ -665,14 +667,26 @@ class ScatterPlotItem(GraphicsObject): if ymn is None or ymx is None: ymn = 0 ymx = 0 - return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) + + px = py = 0.0 + if self._maxSpotPxWidth > 0: + # determine length of pixel in local x, y directions + px, py = self.pixelVectors() + px = 0 if px is None else px.length() * 0.5 + py = 0 if py is None else py.length() * 0.5 + + # return bounds expanded by pixel size + px *= self._maxSpotPxWidth + py *= self._maxSpotPxWidth + px += self._maxSpotWidth * 0.5 + py += self._maxSpotWidth * 0.5 + return QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn) def viewTransformChanged(self): self.prepareGeometryChange() GraphicsObject.viewTransformChanged(self) self.bounds = [None, None] self.fragments = None - self.informViewBoundsChanged() def generateFragments(self): tr = self.deviceTransform() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 44f98e77..ce1d61f9 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -297,12 +297,11 @@ class ViewBox(GraphicsWidget): def resizeEvent(self, ev): #self.setRange(self.range, padding=0) - #self.updateAutoRange() - self._itemBoundsCache.clear() + self.updateAutoRange() self.updateMatrix() self.sigStateChanged.emit(self) self.background.setRect(self.rect()) - + #self._itemBoundsCache.clear() #self.linkedXChanged() #self.linkedYChanged() @@ -730,8 +729,7 @@ class ViewBox(GraphicsWidget): def itemBoundsChanged(self, item): self._itemBoundsCache.pop(item, None) - if item in self.addedItems: - self.updateAutoRange() + self.updateAutoRange() def invertY(self, b=True): """ @@ -1003,63 +1001,71 @@ class ViewBox(GraphicsWidget): Values may be None if there are no specific bounds for an axis. """ prof = debug.Profiler('updateAutoRange', disabled=True) - - - #items = self.allChildren() items = self.addedItems - #if item is None: - ##print "children bounding rect:" - #item = self.childGroup - - range = [None, None] - + ## measure pixel dimensions in view box + px, py = [v.length() if v is not None else 0 for v in self.childGroup.pixelVectors()] + + ## First collect all boundary information + itemBounds = [] for item in items: if not item.isVisible(): continue useX = True useY = True + if hasattr(item, 'dataBounds'): - bounds = self._itemBoundsCache.get(item, None) - if bounds is None: - if frac is None: - frac = (1.0, 1.0) - xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) - yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) - if xr is None or xr == (None, None): - useX = False - xr = (0,0) - if yr is None or yr == (None, None): - useY = False - yr = (0,0) + #bounds = self._itemBoundsCache.get(item, None) + #if bounds is None: + if frac is None: + frac = (1.0, 1.0) + xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) + yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) + pxPad = 0 if not hasattr(item, 'pixelPadding') else item.pixelPadding() + if xr is None or xr == (None, None): + useX = False + xr = (0,0) + if yr is None or yr == (None, None): + useY = False + yr = (0,0) - bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0]) - bounds = self.mapFromItemToView(item, bounds).boundingRect() - self._itemBoundsCache[item] = (bounds, useX, useY) - else: - bounds, useX, useY = bounds + bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0]) + bounds = self.mapFromItemToView(item, bounds).boundingRect() + + if not any([useX, useY]): + continue + + ## If we are ignoring only one axis, we need to check for rotations + if useX != useY: ## != means xor + ang = round(item.transformAngle()) + if ang == 0 or ang == 180: + pass + elif ang == 90 or ang == 270: + useX, useY = useY, useX + else: + ## Item is rotated at non-orthogonal angle, ignore bounds entirely. + ## Not really sure what is the expected behavior in this case. + continue ## need to check for item rotations and decide how best to apply this boundary. + + + itemBounds.append((bounds, useX, useY, pxPad)) + #self._itemBoundsCache[item] = (bounds, useX, useY) + #else: + #bounds, useX, useY = bounds else: if int(item.flags() & item.ItemHasNoContents) > 0: continue else: bounds = item.boundingRect() bounds = self.mapFromItemToView(item, bounds).boundingRect() - - prof.mark('1') - - if not any([useX, useY]): - continue - - if useX != useY: ## != means xor - ang = item.transformAngle() - if ang == 0 or ang == 180: - pass - elif ang == 90 or ang == 270: - useX, useY = useY, useX - else: - continue ## need to check for item rotations and decide how best to apply this boundary. - + itemBounds.append((bounds, True, True, 0)) + + #print itemBounds + + ## determine tentative new range + range = [None, None] + for bounds, useX, useY, px in itemBounds: if useY: if range[1] is not None: range[1] = [min(bounds.top(), range[1][0]), max(bounds.bottom(), range[1][1])] @@ -1071,7 +1077,32 @@ class ViewBox(GraphicsWidget): else: range[0] = [bounds.left(), bounds.right()] prof.mark('2') - + + #print "range", range + + ## Now expand any bounds that have a pixel margin + ## This must be done _after_ we have a good estimate of the new range + ## to ensure that the pixel size is roughly accurate. + w = self.width() + h = self.height() + #print "w:", w, "h:", h + if w > 0 and range[0] is not None: + pxSize = (range[0][1] - range[0][0]) / w + for bounds, useX, useY, px in itemBounds: + if px == 0 or not useX: + continue + range[0][0] = min(range[0][0], bounds.left() - px*pxSize) + range[0][1] = max(range[0][1], bounds.right() + px*pxSize) + if h > 0 and range[1] is not None: + pxSize = (range[1][1] - range[1][0]) / h + for bounds, useX, useY, px in itemBounds: + if px == 0 or not useY: + 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 @@ -1089,6 +1120,8 @@ class ViewBox(GraphicsWidget): def updateMatrix(self, changed=None): + ## Make the childGroup's transform match the requested range. + if changed is None: changed = [False, False] changed = list(changed) From 9f55a27fdd6db90e13202b210814497e12cf0170 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 12 Feb 2013 21:44:42 -0500 Subject: [PATCH 18/22] More boundingRect / dataBounds bugfixes --- examples/ScatterPlot.py | 2 +- pyqtgraph/graphicsItems/PlotCurveItem.py | 124 +++++++++++++++------ pyqtgraph/graphicsItems/PlotDataItem.py | 70 ++++++++---- pyqtgraph/graphicsItems/ScatterPlotItem.py | 46 +++----- 4 files changed, 149 insertions(+), 93 deletions(-) diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 2a15164f..e72e2631 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -70,7 +70,7 @@ s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to tr spots3 = [] for i in range(10): for j in range(10): - spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'pen': {'color': 'w', 'width': 8}, 'brush':pg.intColor(i*10+j, 100)}) + spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'pen': {'color': 'w', 'width': 2}, 'brush':pg.intColor(i*10+j, 100)}) s3.addPoints(spots3) w3.addItem(s3) s3.sigClicked.connect(clicked) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index b321714a..35a38ae7 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -93,7 +93,7 @@ class PlotCurveItem(GraphicsObject): (x, y) = self.getData() if x is None or len(x) == 0: - return (0, 0) + return (None, None) if ax == 0: d = x @@ -102,20 +102,106 @@ class PlotCurveItem(GraphicsObject): d = y d2 = x + ## If an orthogonal range is specified, mask the data now if orthoRange is not None: mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) d = d[mask] d2 = d2[mask] - + ## Get min/max (or percentiles) of the requested data range if frac >= 1.0: b = (d.min(), d.max()) elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: b = (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + + ## adjust for fill level + if ax == 1 and self.opts['fillLevel'] is not None: + b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel'])) + + ## Add pen width only if it is non-cosmetic. + pen = self.opts['pen'] + spen = self.opts['shadowPen'] + if not pen.isCosmetic(): + b = (b[0] - pen.widthF()*0.7072, b[1] + pen.widthF()*0.7072) + if spen is not None and not spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen: + b = (b[0] - spen.widthF()*0.7072, b[1] + spen.widthF()*0.7072) + self._boundsCache[ax] = [(frac, orthoRange), b] return b + + def pixelPadding(self): + pen = self.opts['pen'] + spen = self.opts['shadowPen'] + w = 0 + if pen.isCosmetic(): + 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) + return w + + def boundingRect(self): + if self._boundingRect is None: + (xmn, xmx) = self.dataBounds(ax=0) + (ymn, ymx) = self.dataBounds(ax=1) + if xmn is None: + return QtCore.QRectF() + + px = py = 0.0 + pxPad = self.pixelPadding() + 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() + + # return bounds expanded by pixel size + px *= pxPad + py *= pxPad + #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): + self.invalidateBounds() + self.prepareGeometryChange() + + #def boundingRect(self): + #if self._boundingRect is None: + #(x, y) = self.getData() + #if x is None or y is None or len(x) == 0 or len(y) == 0: + #return QtCore.QRectF() + + + #if self.opts['shadowPen'] is not None: + #lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1) + #else: + #lineWidth = (self.opts['pen'].width()+1) + + + #pixels = self.pixelVectors() + #if pixels == (None, None): + #pixels = [Point(0,0), Point(0,0)] + + #xmin = x.min() + #xmax = x.max() + #ymin = y.min() + #ymax = y.max() + + #if self.opts['fillLevel'] is not None: + #ymin = min(ymin, self.opts['fillLevel']) + #ymax = max(ymax, self.opts['fillLevel']) + + #xmin -= pixels[0].x() * lineWidth + #xmax += pixels[0].x() * lineWidth + #ymin -= abs(pixels[1].y()) * lineWidth + #ymax += abs(pixels[1].y()) * lineWidth + + #self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) + #return self._boundingRect + def invalidateBounds(self): self._boundingRect = None @@ -280,40 +366,6 @@ class PlotCurveItem(GraphicsObject): return QtGui.QPainterPath() return self.path - def boundingRect(self): - if self._boundingRect is None: - (x, y) = self.getData() - if x is None or y is None or len(x) == 0 or len(y) == 0: - return QtCore.QRectF() - - - if self.opts['shadowPen'] is not None: - lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1) - else: - lineWidth = (self.opts['pen'].width()+1) - - - pixels = self.pixelVectors() - if pixels == (None, None): - pixels = [Point(0,0), Point(0,0)] - - xmin = x.min() - xmax = x.max() - ymin = y.min() - ymax = y.max() - - if self.opts['fillLevel'] is not None: - ymin = min(ymin, self.opts['fillLevel']) - ymax = max(ymax, self.opts['fillLevel']) - - xmin -= pixels[0].x() * lineWidth - xmax += pixels[0].x() * lineWidth - ymin -= abs(pixels[1].y()) * lineWidth - ymax += abs(pixels[1].y()) * lineWidth - - self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) - return self._boundingRect - def paint(self, p, opt, widget): prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) if self.xData is None: diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 83afbbfe..c0d5f2f3 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -471,33 +471,57 @@ class PlotDataItem(GraphicsObject): and max) =============== ============================================================= """ - if frac <= 0.0: - raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) - (x, y) = self.getData() - if x is None or len(x) == 0: - return None + range = [None, None] + if self.curve.isVisible(): + range = self.curve.dataBounds(ax, frac, orthoRange) + elif self.scatter.isVisible(): + r2 = self.scatter.dataBounds(ax, frac, orthoRange) + range = [ + r2[0] if range[0] is None else (range[0] if r2[0] is None else min(r2[0], range[0])), + r2[1] if range[1] is None else (range[1] if r2[1] is None else min(r2[1], range[1])) + ] + return range + + #if frac <= 0.0: + #raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) + + #(x, y) = self.getData() + #if x is None or len(x) == 0: + #return None - if ax == 0: - d = x - d2 = y - elif ax == 1: - d = y - d2 = x + #if ax == 0: + #d = x + #d2 = y + #elif ax == 1: + #d = y + #d2 = x - if orthoRange is not None: - mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) - d = d[mask] - #d2 = d2[mask] + #if orthoRange is not None: + #mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) + #d = d[mask] + ##d2 = d2[mask] - if len(d) > 0: - if frac >= 1.0: - return (np.min(d), np.max(d)) - else: - return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) - else: - return None - + #if len(d) > 0: + #if frac >= 1.0: + #return (np.min(d), np.max(d)) + #else: + #return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + #else: + #return None + + def pixelPadding(self): + """ + Return the size in pixels that this item may draw beyond the values returned by dataBounds(). + This method is called by ViewBox when auto-scaling. + """ + pad = 0 + if self.curve.isVisible(): + pad = max(pad, self.curve.pixelPadding()) + elif self.scatter.isVisible(): + pad = max(pad, self.scatter.pixelPadding()) + return pad + def clear(self): #for i in self.curves+self.scatters: diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 93653869..18d9ebf3 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -60,7 +60,7 @@ def renderSymbol(symbol, size, pen, brush, device=None): #return SymbolPixmapCache[key] ## Render a spot with the given parameters to a pixmap - penPxWidth = max(np.ceil(pen.width()), 1) + penPxWidth = max(np.ceil(pen.widthF()), 1) image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32) image.fill(0) p = QtGui.QPainter(image) @@ -115,7 +115,7 @@ class SymbolAtlas(object): symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush'] pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush - key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color())) + 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 @@ -589,13 +589,13 @@ class ScatterPlotItem(GraphicsObject): width = 0 pxWidth = 0 if self.opts['pxMode']: - pxWidth = size + pen.width() + pxWidth = size + pen.widthF() else: width = size if pen.isCosmetic(): - pxWidth += pen.width() + pxWidth += pen.widthF() else: - width += pen.width() + width += pen.widthF() self._maxSpotWidth = max(self._maxSpotWidth, width) self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth) self.bounds = [None, None] @@ -629,20 +629,7 @@ class ScatterPlotItem(GraphicsObject): d2 = d2[mask] if frac >= 1.0: - ## increase size of bounds based on spot size and pen width - #px = self.pixelLength(Point(1, 0) if ax == 0 else Point(0, 1)) ## determine length of pixel along this axis - #px = self.pixelVectors()[ax] - #if px is None: - #px = 0 - #else: - #px = px.length() - #minIndex = np.argmin(d) - #maxIndex = np.argmax(d) - #minVal = d[minIndex] - #maxVal = d[maxIndex] - #spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth) - #self.bounds[ax] = (minVal-spotSize, maxVal+spotSize) - self.bounds[ax] = (d.min() - 0.5*self._maxSpotWidth, d.max() + 0.5*self._maxSpotWidth) + self.bounds[ax] = (d.min() - self._maxSpotWidth*0.7072, d.max() + self._maxSpotWidth*0.7072) return self.bounds[ax] elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) @@ -650,13 +637,7 @@ class ScatterPlotItem(GraphicsObject): return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) def pixelPadding(self): - return self._maxSpotPxWidth - - #def defaultSpotPixmap(self): - ### Return the default spot pixmap - #if self._spotPixmap is None: - #self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol']) - #return self._spotPixmap + return self._maxSpotPxWidth*0.7072 def boundingRect(self): (xmn, xmx) = self.dataBounds(ax=0) @@ -669,17 +650,16 @@ class ScatterPlotItem(GraphicsObject): ymx = 0 px = py = 0.0 - if self._maxSpotPxWidth > 0: + pxPad = self.pixelPadding() + 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() * 0.5 - py = 0 if py is None else py.length() * 0.5 + px = 0 if px is None else px.length() + py = 0 if py is None else py.length() # return bounds expanded by pixel size - px *= self._maxSpotPxWidth - py *= self._maxSpotPxWidth - px += self._maxSpotWidth * 0.5 - py += self._maxSpotWidth * 0.5 + px *= pxPad + py *= pxPad return QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn) def viewTransformChanged(self): From 4dbc411d19dd7f3359903cd18ec9c1f1ad72b962 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 12 Feb 2013 23:10:25 -0500 Subject: [PATCH 19/22] minor fixes --- pyqtgraph/graphicsItems/GraphItem.py | 4 ++-- pyqtgraph/parametertree/Parameter.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphItem.py b/pyqtgraph/graphicsItems/GraphItem.py index bf03fef6..be6138ce 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -1,6 +1,6 @@ from .. import functions as fn -from GraphicsObject import GraphicsObject -from ScatterPlotItem import ScatterPlotItem +from .GraphicsObject import GraphicsObject +from .ScatterPlotItem import ScatterPlotItem import pyqtgraph as pg import numpy as np diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index c8e19f16..9a7ece25 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -615,7 +615,7 @@ class Parameter(QtCore.QObject): if attr in self.names: import traceback traceback.print_stack() - print "Warning: Use of Parameter.subParam is deprecated. Use Parameter.param(name) instead." + print("Warning: Use of Parameter.subParam is deprecated. Use Parameter.param(name) instead.") return self.param(attr) else: raise AttributeError(attr) From ccc81c691981323ac9f6f7e54723ff5a4b6306ac Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Feb 2013 11:43:22 -0500 Subject: [PATCH 20/22] mp fixes --- pyqtgraph/multiprocess/bootstrap.py | 13 +++-- pyqtgraph/multiprocess/processes.py | 47 ++++++++++++---- pyqtgraph/multiprocess/remoteproxy.py | 73 +++++++++++++++++++------ pyqtgraph/widgets/RemoteGraphicsView.py | 15 +++-- 4 files changed, 114 insertions(+), 34 deletions(-) diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py index 28818135..4ecfb7da 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -5,20 +5,25 @@ if __name__ == '__main__': if hasattr(os, 'setpgrp'): os.setpgrp() ## prevents signals (notably keyboard interrupt) being forwarded from parent to this process if sys.version[0] == '3': - name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin.buffer) + #name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin.buffer) + opts = pickle.load(sys.stdin.buffer) else: - name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin) + #name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin) + opts = pickle.load(sys.stdin) #print "key:", ' '.join([str(ord(x)) for x in authkey]) + path = opts.pop('path', None) if path is not None: ## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list. while len(sys.path) > 0: sys.path.pop() sys.path.extend(path) - if pyside: + if opts.pop('pyside', False): import PySide #import pyqtgraph #import pyqtgraph.multiprocess.processes + targetStr = opts.pop('targetStr') target = pickle.loads(targetStr) ## unpickling the target should import everything we need - target(name, port, authkey, ppid) + #target(name, port, authkey, ppid) + target(**opts) ## Send all other options to the target function sys.exit(0) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 4c3be4e9..93a109ed 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -35,7 +35,7 @@ class Process(RemoteEventHandler): ProxyObject for more information. """ - def __init__(self, name=None, target=None, executable=None, copySysPath=True): + def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False): """ ============ ============================================================= Arguments: @@ -46,7 +46,9 @@ class Process(RemoteEventHandler): 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 + 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. ============ ============================================================= """ @@ -56,6 +58,7 @@ class Process(RemoteEventHandler): name = str(self) if executable is None: executable = sys.executable + self.debug = debug ## random authentication key authkey = os.urandom(20) @@ -75,23 +78,46 @@ class Process(RemoteEventHandler): ## start remote process, instruct it to run target function 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)) self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE) targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to ## set its sys.path properly before unpickling the target - pid = os.getpid() # we must sent pid to child because windows does not have getppid + pid = os.getpid() # we must send pid to child because windows does not have getppid pyside = USE_PYSIDE ## Send everything the remote process needs to start correctly - pickle.dump((name+'_child', port, authkey, pid, targetStr, sysPath, pyside), self.proc.stdin) + data = dict( + name=name+'_child', + port=port, + authkey=authkey, + ppid=pid, + targetStr=targetStr, + path=sysPath, + pyside=pyside, + debug=debug + ) + pickle.dump(data, self.proc.stdin) self.proc.stdin.close() ## open connection for remote process - conn = l.accept() - RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid) + self.debugMsg('Listening for child process..') + while True: + try: + conn = l.accept() + break + except IOError as err: + if err.errno == 4: # interrupted; try again + continue + else: + raise + + RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid, debug=debug) + self.debugMsg('Connected to child process.') atexit.register(self.join) def join(self, timeout=10): + self.debugMsg('Joining child process..') if self.proc.poll() is None: self.close() start = time.time() @@ -99,13 +125,14 @@ class Process(RemoteEventHandler): if timeout is not None and time.time() - start > timeout: raise Exception('Timed out waiting for remote process to end.') time.sleep(0.05) + self.debugMsg('Child process exited. (%d)' % self.proc.returncode) -def startEventLoop(name, port, authkey, ppid): +def startEventLoop(name, port, authkey, ppid, debug=False): conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) global HANDLER #ppid = 0 if not hasattr(os, 'getppid') else os.getppid() - HANDLER = RemoteEventHandler(conn, name, ppid) + HANDLER = RemoteEventHandler(conn, name, ppid, debug=debug) while True: try: HANDLER.processRequests() # exception raised when the loop should exit @@ -329,7 +356,7 @@ class QtProcess(Process): except ClosedError: self.timer.stop() -def startQtEventLoop(name, port, authkey, ppid): +def startQtEventLoop(name, port, authkey, ppid, debug=False): conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) from pyqtgraph.Qt import QtGui, QtCore #from PyQt4 import QtGui, QtCore @@ -342,7 +369,7 @@ def startQtEventLoop(name, port, authkey, ppid): global HANDLER #ppid = 0 if not hasattr(os, 'getppid') else os.getppid() - HANDLER = RemoteQtEventHandler(conn, name, ppid) + HANDLER = RemoteQtEventHandler(conn, name, ppid, debug=debug) HANDLER.startEventTimer() app.exec_() diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 096f2006..6cd65f6e 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -42,7 +42,8 @@ class RemoteEventHandler(object): handlers = {} ## maps {process ID : handler}. This allows unpickler to determine which process ## an object proxy belongs to - def __init__(self, connection, name, pid): + def __init__(self, connection, name, pid, debug=False): + self.debug = debug self.conn = connection self.name = name self.results = {} ## reqId: (status, result); cache of request results received from the remote process @@ -76,6 +77,11 @@ class RemoteEventHandler(object): print(pid, cls.handlers) raise + def debugMsg(self, msg): + if not self.debug: + return + print("[%d] %s" % (os.getpid(), str(msg))) + def getProxyOption(self, opt): return self.proxyOptions[opt] @@ -91,7 +97,9 @@ class RemoteEventHandler(object): after no more events are immediately available. (non-blocking) Returns the number of events processed. """ + self.debugMsg('processRequests:') if self.exited: + self.debugMsg(' processRequests: exited already; raise ClosedError.') raise ClosedError() numProcessed = 0 @@ -100,37 +108,64 @@ class RemoteEventHandler(object): self.handleRequest() numProcessed += 1 except ClosedError: + self.debugMsg(' processRequests: got ClosedError from handleRequest; setting exited=True.') self.exited = True raise - except IOError as err: - if err.errno == 4: ## interrupted system call; try again - continue - else: - 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()) + self.debugMsg(' processRequests: finished %d requests' % numProcessed) return numProcessed def handleRequest(self): """Handle a single request from the remote process. Blocks until a request is available.""" result = None - try: - cmd, reqId, nByteMsgs, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails - except (EOFError, IOError): - ## remote process has shut down; end event loop - raise ClosedError() - #print os.getpid(), "received request:", cmd, reqId + while True: + try: + ## args, kwds are double-pickled to ensure this recv() call never fails + cmd, reqId, nByteMsgs, optStr = self.conn.recv() + break + except EOFError: + self.debugMsg(' handleRequest: got EOFError from recv; raise ClosedError.') + ## remote process has shut down; end event loop + raise ClosedError() + except IOError as err: + if err.errno == 4: ## interrupted system call; try again + self.debugMsg(' handleRequest: got IOError 4 from recv; try again.') + continue + else: + self.debugMsg(' handleRequest: got IOError %d from recv (%s); raise ClosedError.' % (err.errno, err.strerror)) + raise ClosedError() + + self.debugMsg(" handleRequest: received %s %s" % (str(cmd), str(reqId))) ## read byte messages following the main request byteData = [] + if nByteMsgs > 0: + self.debugMsg(" handleRequest: reading %d byte messages" % nByteMsgs) for i in range(nByteMsgs): - try: - byteData.append(self.conn.recv_bytes()) - except (EOFError, IOError): - raise ClosedError() + while True: + try: + byteData.append(self.conn.recv_bytes()) + break + except EOFError: + self.debugMsg(" handleRequest: got EOF while reading byte messages; raise ClosedError.") + raise ClosedError() + except IOError as err: + if err.errno == 4: + self.debugMsg(" handleRequest: got IOError 4 while reading byte messages; try again.") + continue + else: + self.debugMsg(" handleRequest: got IOError while reading byte messages; raise ClosedError.") + raise ClosedError() try: @@ -140,6 +175,7 @@ class RemoteEventHandler(object): ## (this is already a return from a previous request) opts = pickle.loads(optStr) + self.debugMsg(" handleRequest: id=%s opts=%s" % (str(reqId), str(opts))) #print os.getpid(), "received request:", cmd, reqId, opts returnType = opts.get('returnType', 'auto') @@ -213,6 +249,7 @@ class RemoteEventHandler(object): if reqId is not None: if exc is None: + 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']) @@ -225,6 +262,7 @@ class RemoteEventHandler(object): sys.excepthook(*sys.exc_info()) self.replyError(reqId, *sys.exc_info()) else: + self.debugMsg(" handleRequest: returning exception for %d" % reqId) self.replyError(reqId, *exc) elif exc is not None: @@ -368,13 +406,16 @@ class RemoteEventHandler(object): ## 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 diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index 2dd1fe9b..cb36ba62 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -1,4 +1,4 @@ -from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE import pyqtgraph.multiprocess as mp import pyqtgraph as pg from .GraphicsView import GraphicsView @@ -21,13 +21,14 @@ class RemoteGraphicsView(QtGui.QWidget): self._sizeHint = (640,480) ## no clue why this is needed, but it seems to be the default sizeHint for GraphicsView. ## without it, the widget will not compete for space against another GraphicsView. QtGui.QWidget.__init__(self) - self._proc = mp.QtProcess() + self._proc = mp.QtProcess(debug=False) self.pg = self._proc._import('pyqtgraph') self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS) rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') self._view = rpgRemote.Renderer(*args, **kwds) self._view._setProxyOptions(deferGetattr=True) - self.setFocusPolicy(QtCore.Qt.FocusPolicy(self._view.focusPolicy())) + + self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.setMouseTracking(True) self.shm = None @@ -114,6 +115,7 @@ class RemoteGraphicsView(QtGui.QWidget): return self._proc class Renderer(GraphicsView): + ## Created by the remote process to handle render requests sceneRendered = QtCore.Signal(object) @@ -175,7 +177,12 @@ class Renderer(GraphicsView): address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0)) ## render the scene directly to shared memory - self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32) + if USE_PYSIDE: + ch = ctypes.c_char.from_buffer(self.shm, 0) + #ch = ctypes.c_char_p(address) + self.img = QtGui.QImage(ch, self.width(), self.height(), QtGui.QImage.Format_ARGB32) + else: + self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32) self.img.fill(0xffffffff) p = QtGui.QPainter(self.img) self.render(p, self.viewRect(), self.rect()) From 63c3b36a03bb91b3186b34584bbb2785aa015cac Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Feb 2013 13:00:50 -0500 Subject: [PATCH 21/22] added a few examples --- examples/LogPlotTest.py | 42 ++++++++++++++ examples/SimplePlot.py | 12 ++++ examples/multiplePlotSpeedTest.py | 93 +++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 examples/LogPlotTest.py create mode 100644 examples/SimplePlot.py create mode 100644 examples/multiplePlotSpeedTest.py diff --git a/examples/LogPlotTest.py b/examples/LogPlotTest.py new file mode 100644 index 00000000..a5b07520 --- /dev/null +++ b/examples/LogPlotTest.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +## This example demonstrates many of the 2D plotting capabilities +## in pyqtgraph. All of the plots may be panned/scaled by dragging with +## the left/right mouse buttons. Right click on any plot to show a context menu. + + +import initExample ## Add path to library (just for examples; you do not need this) + + +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg + +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) + +win = pg.GraphicsWindow(title="Basic plotting examples") +win.resize(1000,600) + + + +p5 = win.addPlot(title="Scatter plot, axis labels, log scale") +x = np.random.normal(size=1000) * 1e-5 +y = x*1000 + 0.005 * np.random.normal(size=1000) +y -= y.min()-1.0 +mask = x > 1e-15 +x = x[mask] +y = y[mask] +p5.plot(x, y, pen=None, symbol='t', symbolPen=None, symbolSize=10, symbolBrush=(100, 100, 255, 50)) +p5.setLabel('left', "Y Axis", units='A') +p5.setLabel('bottom', "Y Axis", units='s') +p5.setLogMode(x=True, y=False) + + +## 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/SimplePlot.py b/examples/SimplePlot.py new file mode 100644 index 00000000..ec40cf16 --- /dev/null +++ b/examples/SimplePlot.py @@ -0,0 +1,12 @@ +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 +pg.plot(np.random.normal(size=100000), title="Simplest possible plotting example") + +## 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/examples/multiplePlotSpeedTest.py b/examples/multiplePlotSpeedTest.py new file mode 100644 index 00000000..bc54bb51 --- /dev/null +++ b/examples/multiplePlotSpeedTest.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +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 + +app = pg.mkQApp() +plt = pg.PlotWidget() + +app.processEvents() + +## Putting this at the beginning or end does not have much effect +plt.show() + +## The auto-range is recomputed after each item is added, +## so disabling it before plotting helps +plt.enableAutoRange(False, False) + +def plot(): + start = pg.ptime.time() + n = 15 + pts = 100 + x = np.linspace(0, 0.8, pts) + y = np.random.random(size=pts)*0.8 + for i in xrange(n): + for j in xrange(n): + ## calling PlotWidget.plot() generates a PlotDataItem, which + ## has a bit more overhead than PlotCurveItem, which is all + ## we need here. This overhead adds up quickly and makes a big + ## difference in speed. + + #plt.plot(x=x+i, y=y+j) + plt.addItem(pg.PlotCurveItem(x=x+i, y=y+j)) + + #path = pg.arrayToQPath(x+i, y+j) + #item = QtGui.QGraphicsPathItem(path) + #item.setPen(pg.mkPen('w')) + #plt.addItem(item) + + dt = pg.ptime.time() - start + print "Create plots took: %0.3fms" % (dt*1000) + +## Plot and clear 5 times, printing the time it took +for i in range(5): + plt.clear() + plot() + app.processEvents() + plt.autoRange() + + + + + +def fastPlot(): + ## Different approach: generate a single item with all data points. + ## This runs about 20x faster. + start = pg.ptime.time() + n = 15 + pts = 100 + x = np.linspace(0, 0.8, pts) + y = np.random.random(size=pts)*0.8 + xdata = np.empty((n, n, pts)) + xdata[:] = x.reshape(1,1,pts) + np.arange(n).reshape(n,1,1) + ydata = np.empty((n, n, pts)) + ydata[:] = y.reshape(1,1,pts) + np.arange(n).reshape(1,n,1) + conn = np.ones((n*n,pts)) + conn[:,-1] = False # make sure plots are disconnected + path = pg.arrayToQPath(xdata.flatten(), ydata.flatten(), conn.flatten()) + item = QtGui.QGraphicsPathItem(path) + item.setPen(pg.mkPen('w')) + plt.addItem(item) + + dt = pg.ptime.time() - start + print "Create plots took: %0.3fms" % (dt*1000) + + +## Plot and clear 5 times, printing the time it took +if hasattr(pg, 'arrayToQPath'): + for i in range(5): + plt.clear() + fastPlot() + app.processEvents() +else: + print "Skipping fast tests--arrayToQPath function is missing." + +plt.autoRange() + +## 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 815746895dd2b7e54d8733f133a9b5be092ea89e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Feb 2013 17:11:47 -0500 Subject: [PATCH 22/22] Fixed GLSurfacePlot bug --- examples/GLSurfacePlot.py | 2 +- pyqtgraph/opengl/items/GLSurfacePlotItem.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/GLSurfacePlot.py b/examples/GLSurfacePlot.py index c901d51e..d2151c46 100644 --- a/examples/GLSurfacePlot.py +++ b/examples/GLSurfacePlot.py @@ -62,7 +62,7 @@ w.addItem(p3) ## Animated example ## compute surface vertex data -cols = 100 +cols = 90 rows = 100 x = np.linspace(-8, 8, cols+1).reshape(cols+1,1) y = np.linspace(-8, 8, rows+1).reshape(1,rows+1) diff --git a/pyqtgraph/opengl/items/GLSurfacePlotItem.py b/pyqtgraph/opengl/items/GLSurfacePlotItem.py index 69080fad..46c54fc2 100644 --- a/pyqtgraph/opengl/items/GLSurfacePlotItem.py +++ b/pyqtgraph/opengl/items/GLSurfacePlotItem.py @@ -127,8 +127,8 @@ class GLSurfacePlotItem(GLMeshItem): def generateFaces(self): - cols = self._z.shape[0]-1 - rows = self._z.shape[1]-1 + cols = self._z.shape[1]-1 + rows = self._z.shape[0]-1 faces = np.empty((cols*rows*2, 3), dtype=np.uint) rowtemplate1 = np.arange(cols).reshape(cols, 1) + np.array([[0, 1, cols+1]]) rowtemplate2 = np.arange(cols).reshape(cols, 1) + np.array([[cols+1, 1, cols+2]])