From a59f4c206a0185ae74c26fc13e3e697a91cec05d Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 25 Feb 2013 14:03:33 -0500 Subject: [PATCH 001/121] Fixed example testing on windows --- examples/__main__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index f885920f97..bdf105237b 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -235,9 +235,13 @@ def testFile(name, f, exe, lib, graphicsSystem=None): """ % (import1, graphicsSystem, import2) - process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) - process.stdin.write(code.encode('UTF-8')) - #process.stdin.close() + if sys.platform.startswith('win'): + process = subprocess.Popen([exe], stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + process.stdin.write(code.encode('UTF-8')) + process.stdin.close() + else: + process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + process.stdin.write(code.encode('UTF-8')) output = '' fail = False while True: From 83812ad5b8ce1234451112645e1a01ef171d6d4f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Feb 2013 21:54:56 -0500 Subject: [PATCH 002/121] Bugfixes: - AxisItem did not update grid line length when plot stretches - Workaround for PySide/QByteArray memory leak --- pyqtgraph/functions.py | 9 +++++++-- pyqtgraph/graphicsItems/AxisItem.py | 10 +++++++++- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 ++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 62f69cb1b4..84a5c57342 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1109,10 +1109,15 @@ def arrayToQPath(x, y, connect='all'): 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 + + ## Avoiding this method because QByteArray(str) leaks memory in PySide + #buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here + + path.strn = arr.data[12:lastInd+4] # make sure data doesn't run away + buf = QtCore.QByteArray.fromRawData(path.strn) #prof.mark('create buffer') ds = QtCore.QDataStream(buf) - #prof.mark('create datastream') + ds >> path #prof.mark('load') diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 9ef647635e..9d1684bd2e 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -309,10 +309,18 @@ def linkToView(self, view): oldView.sigXRangeChanged.disconnect(self.linkedViewChanged) view.sigXRangeChanged.connect(self.linkedViewChanged) - def linkedViewChanged(self, view, newRange): + if oldView is not None: + oldView.sigResized.disconnect(self.linkedViewChanged) + view.sigResized.connect(self.linkedViewChanged) + + def linkedViewChanged(self, view, newRange=None): if self.orientation in ['right', 'left'] and view.yInverted(): + if newRange is None: + newRange = view.viewRange()[1] self.setRange(*newRange[::-1]) else: + if newRange is None: + newRange = view.viewRange()[0] self.setRange(*newRange) def boundingRect(self): diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 87b687bd34..cf2040071e 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -50,6 +50,7 @@ class ViewBox(GraphicsWidget): #sigActionPositionChanged = QtCore.Signal(object) sigStateChanged = QtCore.Signal(object) sigTransformChanged = QtCore.Signal(object) + sigResized = QtCore.Signal(object) ## mouse modes PanMode = 3 @@ -304,6 +305,7 @@ def resizeEvent(self, ev): #self._itemBoundsCache.clear() #self.linkedXChanged() #self.linkedYChanged() + self.sigResized.emit(self) def viewRange(self): """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]""" From 0642f38657e7518c13f3b68ed6b7446a939b977e Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 27 Feb 2013 16:42:43 -0500 Subject: [PATCH 003/121] Flowcharts get cubic spline connectors --- pyqtgraph/flowchart/Terminal.py | 46 +++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/flowchart/Terminal.py b/pyqtgraph/flowchart/Terminal.py index 623d1a2871..3066223d6e 100644 --- a/pyqtgraph/flowchart/Terminal.py +++ b/pyqtgraph/flowchart/Terminal.py @@ -521,6 +521,8 @@ def __init__(self, source, target=None): self.target = target self.length = 0 self.hovered = False + self.path = None + self.shapePath = None #self.line = QtGui.QGraphicsLineItem(self) self.source.getViewBox().addItem(self) self.updateLine() @@ -544,13 +546,18 @@ def updateLine(self): else: return self.prepareGeometryChange() - self.resetTransform() - ang = (stop-start).angle(Point(0, 1)) - if ang is None: - ang = 0 - self.rotate(ang) - self.setPos(start) - self.length = (start-stop).length() + + self.path = QtGui.QPainterPath() + self.path.moveTo(start) + self.path.cubicTo(Point(stop.x(), start.y()), Point(start.x(), stop.y()), Point(stop.x(), stop.y())) + self.shapePath = None + #self.resetTransform() + #ang = (stop-start).angle(Point(0, 1)) + #if ang is None: + #ang = 0 + #self.rotate(ang) + #self.setPos(start) + #self.length = (start-stop).length() self.update() #self.line.setLine(start.x(), start.y(), stop.x(), stop.y()) @@ -582,12 +589,23 @@ def hoverEvent(self, ev): def boundingRect(self): - #return self.line.boundingRect() - px = self.pixelWidth() - return QtCore.QRectF(-5*px, 0, 10*px, self.length) + return self.shape().boundingRect() + ##return self.line.boundingRect() + #px = self.pixelWidth() + #return QtCore.QRectF(-5*px, 0, 10*px, self.length) + def viewRangeChanged(self): + self.shapePath = None + self.prepareGeometryChange() - #def shape(self): - #return self.line.shape() + def shape(self): + if self.shapePath is None: + if self.path is None: + return QtGui.QPainterPath() + stroker = QtGui.QPainterPathStroker() + px = self.pixelWidth() + stroker.setWidth(px*8) + self.shapePath = stroker.createStroke(self.path) + return self.shapePath def paint(self, p, *args): if self.isSelected(): @@ -598,4 +616,6 @@ def paint(self, p, *args): else: p.setPen(fn.mkPen(100, 100, 250, width=1)) - p.drawLine(0, 0, 0, self.length) + #p.drawLine(0, 0, 0, self.length) + + p.drawPath(self.path) From 2980f8335c1745564e89259a026e83167bb33e96 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 4 Mar 2013 19:43:51 -0500 Subject: [PATCH 004/121] bugfix: ignore inf and nan when auto-ranging added experimental opengl line-drawing code --- pyqtgraph/__init__.py | 1 + pyqtgraph/graphicsItems/PlotCurveItem.py | 77 ++++++++++++++++++++-- pyqtgraph/graphicsItems/ScatterPlotItem.py | 3 + pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 4 +- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index d3aefa83ce..ed9e335746 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -54,6 +54,7 @@ '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 + 'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code) } diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 35a38ae731..c5a8ec3ffd 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -1,4 +1,10 @@ from pyqtgraph.Qt import QtGui, QtCore +try: + from pyqtgraph.Qt import QtOpenGL + HAVE_OPENGL = True +except: + HAVE_OPENGL = False + from scipy.fftpack import fft import numpy as np import scipy.stats @@ -370,12 +376,11 @@ def paint(self, p, opt, widget): prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) if self.xData is None: return - #if self.opts['spectrumMode']: - #if self.specPath is None: - - #self.specPath = self.generatePath(*self.getData()) - #path = self.specPath - #else: + + if HAVE_OPENGL and pg.getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget): + self.paintGL(p, opt, widget) + return + x = None y = None if self.path is None: @@ -385,7 +390,6 @@ def paint(self, p, opt, widget): self.path = self.generatePath(x,y) self.fillPath = None - path = self.path prof.mark('generate path') @@ -440,6 +444,65 @@ def paint(self, p, opt, widget): #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) #p.drawRect(self.boundingRect()) + def paintGL(self, p, opt, widget): + p.beginNativePainting() + import OpenGL.GL as gl + + ## set clipping viewport + view = self.getViewBox() + if view is not None: + rect = view.mapRectToItem(self, view.boundingRect()) + #gl.glViewport(int(rect.x()), int(rect.y()), int(rect.width()), int(rect.height())) + + #gl.glTranslate(-rect.x(), -rect.y(), 0) + + gl.glEnable(gl.GL_STENCIL_TEST) + gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE) # disable drawing to frame buffer + gl.glDepthMask(gl.GL_FALSE) # disable drawing to depth buffer + gl.glStencilFunc(gl.GL_NEVER, 1, 0xFF) + gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP) + + ## draw stencil pattern + gl.glStencilMask(0xFF); + gl.glClear(gl.GL_STENCIL_BUFFER_BIT) + gl.glBegin(gl.GL_TRIANGLES) + gl.glVertex2f(rect.x(), rect.y()) + gl.glVertex2f(rect.x()+rect.width(), rect.y()) + gl.glVertex2f(rect.x(), rect.y()+rect.height()) + gl.glVertex2f(rect.x()+rect.width(), rect.y()+rect.height()) + gl.glVertex2f(rect.x()+rect.width(), rect.y()) + gl.glVertex2f(rect.x(), rect.y()+rect.height()) + gl.glEnd() + + gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE) + gl.glDepthMask(gl.GL_TRUE) + gl.glStencilMask(0x00) + gl.glStencilFunc(gl.GL_EQUAL, 1, 0xFF) + + try: + x, y = self.getData() + pos = np.empty((len(x), 2)) + pos[:,0] = x + pos[:,1] = y + gl.glEnableClientState(gl.GL_VERTEX_ARRAY) + try: + gl.glVertexPointerf(pos) + pen = fn.mkPen(self.opts['pen']) + color = pen.color() + gl.glColor4f(color.red()/255., color.green()/255., color.blue()/255., color.alpha()/255.) + width = pen.width() + if pen.isCosmetic() and width < 1: + width = 1 + gl.glPointSize(width) + gl.glEnable(gl.GL_LINE_SMOOTH) + gl.glEnable(gl.GL_BLEND) + gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) + gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST); + gl.glDrawArrays(gl.GL_LINE_STRIP, 0, pos.size / pos.shape[-1]) + finally: + gl.glDisableClientState(gl.GL_VERTEX_ARRAY) + finally: + p.endNativePainting() def clear(self): self.xData = None ## raw values diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index a69131ef25..84c0547873 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -683,6 +683,9 @@ def generateFragments(self): rec = self.data[i] pos = QtCore.QPointF(pts[0,i], pts[1,i]) x,y,w,h = rec['fragCoords'] + if abs(w) > 10000 or abs(h) > 10000: + print self.data + raise Exception("fragment corrupt") rect = QtCore.QRectF(y, x, h, w) self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect)) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index cf2040071e..654a12c97e 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1048,10 +1048,10 @@ def childrenBounds(self, frac=None, orthoRange=(None,None), items=None): 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): + if xr is None or xr == (None, None) or np.isnan(xr).any() or np.isinf(xr).any(): useX = False xr = (0,0) - if yr is None or yr == (None, None): + if yr is None or yr == (None, None) or np.isnan(yr).any() or np.isinf(yr).any(): useY = False yr = (0,0) From 5254d29b6a97d5dcf3d6d899d17968d5c33b704b Mon Sep 17 00:00:00 2001 From: Brianna Laugher Date: Tue, 5 Mar 2013 13:58:42 +1100 Subject: [PATCH 005/121] Pylint cleanups - remove commented out code, fix formatting etc --- pyqtgraph/widgets/TableWidget.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index dc4f875b73..4c6a77cecf 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -6,7 +6,7 @@ try: import metaarray HAVE_METAARRAY = True -except: +except ImportError: HAVE_METAARRAY = False __all__ = ['TableWidget'] @@ -60,26 +60,19 @@ def appendData(self, data): first = next(it0) except StopIteration: return - #if type(first) == type(np.float64(1)): - # return fn1, header1 = self.iteratorFn(first) if fn1 is None: self.clear() return - #print fn0, header0 - #print fn1, header1 firstVals = [x for x in fn1(first)] self.setColumnCount(len(firstVals)) - #print header0, header1 if not self.verticalHeadersSet and header0 is not None: - #print "set header 0:", header0 self.setRowCount(len(header0)) self.setVerticalHeaderLabels(header0) self.verticalHeadersSet = True if not self.horizontalHeadersSet and header1 is not None: - #print "set header 1:", header1 self.setHorizontalHeaderLabels(header1) self.horizontalHeadersSet = True @@ -110,13 +103,16 @@ def iteratorFn(self, data): elif data is None: return (None,None) else: - raise Exception("Don't know how to iterate over data type: %s" % str(type(data))) + msg = "Don't know how to iterate over data type: {!s}".format(type(data)) + raise TypeError(msg) def iterFirstAxis(self, data): for i in range(data.shape[0]): yield data[i] - def iterate(self, data): ## for numpy.void, which can be iterated but mysteriously has no __iter__ (??) + def iterate(self, data): + # for numpy.void, which can be iterated but mysteriously + # has no __iter__ (??) for x in data: yield x @@ -124,14 +120,13 @@ def appendRow(self, data): self.appendData([data]) def addRow(self, vals): - #print "add row:", vals row = self.rowCount() - self.setRowCount(row+1) + self.setRowCount(row + 1) self.setRow(row, vals) def setRow(self, row, vals): - if row > self.rowCount()-1: - self.setRowCount(row+1) + if row > self.rowCount() - 1: + self.setRowCount(row + 1) for col in range(self.columnCount()): val = vals[col] if isinstance(val, float) or isinstance(val, np.floating): @@ -140,7 +135,6 @@ def setRow(self, row, vals): s = str(val) item = QtGui.QTableWidgetItem(s) item.value = val - #print "add item to row %d:"%row, item, item.value self.items.append(item) self.setItem(row, col, item) @@ -148,8 +142,10 @@ def serialize(self, useSelection=False): """Convert entire table (or just selected area) into tab-separated text values""" if useSelection: selection = self.selectedRanges()[0] - rows = list(range(selection.topRow(), selection.bottomRow()+1)) - columns = list(range(selection.leftColumn(), selection.rightColumn()+1)) + rows = list(range(selection.topRow(), + selection.bottomRow() + 1)) + columns = list(range(selection.leftColumn(), + selection.rightColumn() + 1)) else: rows = list(range(self.rowCount())) columns = list(range(self.columnCount())) From cba720730dae98201a1e2e61924b2f2e5bd6fb7f Mon Sep 17 00:00:00 2001 From: Brianna Laugher Date: Tue, 5 Mar 2013 14:02:55 +1100 Subject: [PATCH 006/121] Some extra bits - add sizeHint, make not editable, make columns sortable --- pyqtgraph/widgets/TableWidget.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 4c6a77cecf..3fa02d599c 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -26,6 +26,7 @@ def __init__(self, *args): QtGui.QTableWidget.__init__(self, *args) self.setVerticalScrollMode(self.ScrollPerPixel) self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection) + self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) self.clear() self.contextMenu = QtGui.QMenu() self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel) @@ -44,6 +45,8 @@ def clear(self): def setData(self, data): self.clear() self.appendData(data) + self.setSortingEnabled(True) + self.resizeColumnsToContents() def appendData(self, data): """Types allowed: @@ -135,9 +138,23 @@ def setRow(self, row, vals): s = str(val) item = QtGui.QTableWidgetItem(s) item.value = val + # by default this is enabled, selectable & editable, but + # we don't want editable + item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled) self.items.append(item) self.setItem(row, col, item) - + + def sizeHint(self): + # based on http://stackoverflow.com/a/7195443/54056 + width = sum(self.columnWidth(i) for i in range(self.columnCount())) + width += self.verticalHeader().sizeHint().width() + width += self.verticalScrollBar().sizeHint().width() + width += self.frameWidth() * 2 + height = sum(self.rowHeight(i) for i in range(self.rowCount())) + height += self.verticalHeader().sizeHint().height() + height += self.horizontalScrollBar().sizeHint().height() + return QtCore.QSize(width, height) + def serialize(self, useSelection=False): """Convert entire table (or just selected area) into tab-separated text values""" if useSelection: From 262d4bf53fd749fe3e9aac4a776e031c39140494 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 4 Mar 2013 23:29:22 -0500 Subject: [PATCH 007/121] bugfix: examples working in PyQt 4.9.6 (workaround for API change) --- examples/__main__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/__main__.py b/examples/__main__.py index bdf105237b..57caa7e00e 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -96,6 +96,7 @@ def __init__(self): self.codeBtn.hide() global examples + self.itemCache = [] self.populateTree(self.ui.exampleTree.invisibleRootItem(), examples) self.ui.exampleTree.expandAll() @@ -122,6 +123,9 @@ def pysideToggled(self, b): def populateTree(self, root, examples): for key, val in examples.items(): item = QtGui.QTreeWidgetItem([key]) + self.itemCache.append(item) # PyQt 4.9.6 no longer keeps references to these wrappers, + # so we need to make an explicit reference or else the .file + # attribute will disappear. if isinstance(val, basestring): item.file = val else: From e4314f883d62f70c35bf2745816dfbf8cb445966 Mon Sep 17 00:00:00 2001 From: Brianna Laugher Date: Tue, 5 Mar 2013 16:29:07 +1100 Subject: [PATCH 008/121] Move setSortingEnabled to the widget init rather than after setting the data, otherwise weird sorting happens --- pyqtgraph/widgets/TableWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 3fa02d599c..5b49b86f06 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -27,6 +27,7 @@ def __init__(self, *args): self.setVerticalScrollMode(self.ScrollPerPixel) self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection) self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + self.setSortingEnabled(True) self.clear() self.contextMenu = QtGui.QMenu() self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel) @@ -45,7 +46,6 @@ def clear(self): def setData(self, data): self.clear() self.appendData(data) - self.setSortingEnabled(True) self.resizeColumnsToContents() def appendData(self, data): From db5c303fad2d959b8e370eac830cec8e66a2530f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Mar 2013 06:27:24 -0500 Subject: [PATCH 009/121] TableWidget updates: - Made numerically sortable - Added setEditable method - Added example --- examples/TableWidget.py | 34 ++++++++++++++++ examples/__main__.py | 2 +- pyqtgraph/widgets/TableWidget.py | 70 ++++++++++++++++++++++---------- 3 files changed, 84 insertions(+), 22 deletions(-) create mode 100644 examples/TableWidget.py diff --git a/examples/TableWidget.py b/examples/TableWidget.py new file mode 100644 index 0000000000..cfeac399bb --- /dev/null +++ b/examples/TableWidget.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +""" +Simple demonstration of TableWidget, which is an extension of QTableWidget +that automatically displays a variety of tabluar data formats. +""" +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 = QtGui.QApplication([]) + +w = pg.TableWidget() +w.show() +w.resize(500,500) +w.setWindowTitle('pyqtgraph example: TableWidget') + + +data = np.array([ + (1, 1.6, 'x'), + (3, 5.4, 'y'), + (8, 12.5, 'z'), + (443, 1e-12, 'w'), + ], dtype=[('Column 1', int), ('Column 2', float), ('Column 3', object)]) + +w.setData(data) + + +## 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/__main__.py b/examples/__main__.py index 57caa7e00e..c46d7065c4 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -64,7 +64,7 @@ ('TreeWidget', 'TreeWidget.py'), ('DataTreeWidget', 'DataTreeWidget.py'), ('GradientWidget', 'GradientWidget.py'), - #('TableWidget', '../widgets/TableWidget.py'), + ('TableWidget', 'TableWidget.py'), ('ColorButton', 'ColorButton.py'), #('CheckTable', '../widgets/CheckTable.py'), #('VerticalLabel', '../widgets/VerticalLabel.py'), diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 5b49b86f06..8ffe729181 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -12,23 +12,20 @@ __all__ = ['TableWidget'] class TableWidget(QtGui.QTableWidget): """Extends QTableWidget with some useful functions for automatic data handling - and copy / export context menu. Can automatically format and display: - - - numpy arrays - - numpy record arrays - - metaarrays - - list-of-lists [[1,2,3], [4,5,6]] - - dict-of-lists {'x': [1,2,3], 'y': [4,5,6]} - - list-of-dicts [{'x': 1, 'y': 4}, {'x': 2, 'y': 5}, ...] + and copy / export context menu. Can automatically format and display a variety + of data types (see :func:`setData() ` for more + information. """ - def __init__(self, *args): + def __init__(self, *args, **kwds): QtGui.QTableWidget.__init__(self, *args) self.setVerticalScrollMode(self.ScrollPerPixel) self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection) self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) self.setSortingEnabled(True) self.clear() + editable = kwds.get('editable', False) + self.setEditable(editable) self.contextMenu = QtGui.QMenu() self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel) self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll) @@ -36,6 +33,7 @@ def __init__(self, *args): self.contextMenu.addAction('Save All').triggered.connect(self.saveAll) def clear(self): + """Clear all contents from the table.""" QtGui.QTableWidget.clear(self) self.verticalHeadersSet = False self.horizontalHeadersSet = False @@ -44,6 +42,16 @@ def clear(self): self.setColumnCount(0) def setData(self, data): + """Set the data displayed in the table. + Allowed formats are: + + * numpy arrays + * numpy record arrays + * metaarrays + * list-of-lists [[1,2,3], [4,5,6]] + * dict-of-lists {'x': [1,2,3], 'y': [4,5,6]} + * list-of-dicts [{'x': 1, 'y': 4}, {'x': 2, 'y': 5}, ...] + """ self.clear() self.appendData(data) self.resizeColumnsToContents() @@ -84,10 +92,15 @@ def appendData(self, data): for row in it0: self.setRow(i, [x for x in fn1(row)]) i += 1 + + def setEditable(self, editable=True): + self.editable = editable + for item in self.items: + item.setEditable(editable) def iteratorFn(self, data): - """Return 1) a function that will provide an iterator for data and 2) a list of header strings""" - if isinstance(data, list): + ## Return 1) a function that will provide an iterator for data and 2) a list of header strings + if isinstance(data, list) or isinstance(data, tuple): return lambda d: d.__iter__(), None elif isinstance(data, dict): return lambda d: iter(d.values()), list(map(str, data.keys())) @@ -130,17 +143,10 @@ def addRow(self, vals): def setRow(self, row, vals): if row > self.rowCount() - 1: self.setRowCount(row + 1) - for col in range(self.columnCount()): + for col in range(len(vals)): val = vals[col] - if isinstance(val, float) or isinstance(val, np.floating): - s = "%0.3g" % val - else: - s = str(val) - item = QtGui.QTableWidgetItem(s) - item.value = val - # by default this is enabled, selectable & editable, but - # we don't want editable - item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled) + item = TableWidgetItem(val) + item.setEditable(self.editable) self.items.append(item) self.setItem(row, col, item) @@ -228,6 +234,28 @@ def keyPressEvent(self, ev): else: ev.ignore() +class TableWidgetItem(QtGui.QTableWidgetItem): + def __init__(self, val): + if isinstance(val, float) or isinstance(val, np.floating): + s = "%0.3g" % val + else: + s = str(val) + QtGui.QTableWidgetItem.__init__(self, s) + self.value = val + flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + self.setFlags(flags) + + def setEditable(self, editable): + if editable: + self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable) + else: + self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable) + + def __lt__(self, other): + if hasattr(other, 'value'): + return self.value < other.value + else: + return self.text() < other.text() if __name__ == '__main__': From 2a27687fb2d8900196e40b62889a48fcf24ecc89 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 7 Mar 2013 15:29:56 -0500 Subject: [PATCH 010/121] merged updates from acq4 --- doc/source/functions.rst | 2 +- pyqtgraph/__init__.py | 44 +++++++++++++++++++++ pyqtgraph/console/Console.py | 4 +- pyqtgraph/flowchart/Terminal.py | 46 ++++++++++++++++------ pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 4 +- pyqtgraph/widgets/ColorMapWidget.py | 10 ++++- pyqtgraph/widgets/DataFilterWidget.py | 10 +++-- 7 files changed, 97 insertions(+), 23 deletions(-) diff --git a/doc/source/functions.rst b/doc/source/functions.rst index 966fd926c4..556c5be04d 100644 --- a/doc/source/functions.rst +++ b/doc/source/functions.rst @@ -97,6 +97,6 @@ Miscellaneous Functions .. autofunction:: pyqtgraph.systemInfo - +.. autofunction:: pyqtgraph.exit diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index d3aefa83ce..71880fbd90 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -54,6 +54,7 @@ '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 + 'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide } @@ -190,9 +191,20 @@ def importAll(path, globals, locals, excludes=()): from .colormap import * from .ptime import time +############################################################## +## PyQt and PySide both are prone to crashing on exit. +## There are two general approaches to dealing with this: +## 1. Install atexit handlers that assist in tearing down to avoid crashes. +## This helps, but is never perfect. +## 2. Terminate the process before python starts tearing down +## This is potentially dangerous +## Attempts to work around exit crashes: import atexit def cleanup(): + if not getConfigOption('exitCleanup'): + return + ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore. ## Workaround for Qt exit crash: @@ -212,6 +224,38 @@ def cleanup(): atexit.register(cleanup) +## Optional function for exiting immediately (with some manual teardown) +def exit(): + """ + Causes python to exit without garbage-collecting any objects, and thus avoids + calling object destructor methods. This is a sledgehammer workaround for + a variety of bugs in PyQt and Pyside that cause crashes on exit. + + This function does the following in an attempt to 'safely' terminate + the process: + + * Invoke atexit callbacks + * Close all open file handles + * os._exit() + + Note: there is some potential for causing damage with this function if you + are using objects that _require_ their destructors to be called (for example, + to properly terminate log files, disconnect from devices, etc). Situations + like this are probably quite rare, but use at your own risk. + """ + + ## first disable our own cleanup function; won't be needing it. + setConfigOptions(exitCleanup=False) + + ## invoke atexit callbacks + atexit._run_exitfuncs() + + ## close file handles + os.closerange(3, 4096) ## just guessing on the maximum descriptor count.. + + os._exit(os.EX_OK) + + ## Convenience functions for command-line use diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 6fbe44a76f..982c242424 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -169,7 +169,7 @@ def execSingle(self, cmd): def execMulti(self, nextLine): - self.stdout.write(nextLine+"\n") + #self.stdout.write(nextLine+"\n") if nextLine.strip() != '': self.multiline += "\n" + nextLine return @@ -372,4 +372,4 @@ def checkException(self, excType, exc, tb): return False return True - \ No newline at end of file + diff --git a/pyqtgraph/flowchart/Terminal.py b/pyqtgraph/flowchart/Terminal.py index 623d1a2871..3066223d6e 100644 --- a/pyqtgraph/flowchart/Terminal.py +++ b/pyqtgraph/flowchart/Terminal.py @@ -521,6 +521,8 @@ def __init__(self, source, target=None): self.target = target self.length = 0 self.hovered = False + self.path = None + self.shapePath = None #self.line = QtGui.QGraphicsLineItem(self) self.source.getViewBox().addItem(self) self.updateLine() @@ -544,13 +546,18 @@ def updateLine(self): else: return self.prepareGeometryChange() - self.resetTransform() - ang = (stop-start).angle(Point(0, 1)) - if ang is None: - ang = 0 - self.rotate(ang) - self.setPos(start) - self.length = (start-stop).length() + + self.path = QtGui.QPainterPath() + self.path.moveTo(start) + self.path.cubicTo(Point(stop.x(), start.y()), Point(start.x(), stop.y()), Point(stop.x(), stop.y())) + self.shapePath = None + #self.resetTransform() + #ang = (stop-start).angle(Point(0, 1)) + #if ang is None: + #ang = 0 + #self.rotate(ang) + #self.setPos(start) + #self.length = (start-stop).length() self.update() #self.line.setLine(start.x(), start.y(), stop.x(), stop.y()) @@ -582,12 +589,23 @@ def hoverEvent(self, ev): def boundingRect(self): - #return self.line.boundingRect() - px = self.pixelWidth() - return QtCore.QRectF(-5*px, 0, 10*px, self.length) + return self.shape().boundingRect() + ##return self.line.boundingRect() + #px = self.pixelWidth() + #return QtCore.QRectF(-5*px, 0, 10*px, self.length) + def viewRangeChanged(self): + self.shapePath = None + self.prepareGeometryChange() - #def shape(self): - #return self.line.shape() + def shape(self): + if self.shapePath is None: + if self.path is None: + return QtGui.QPainterPath() + stroker = QtGui.QPainterPathStroker() + px = self.pixelWidth() + stroker.setWidth(px*8) + self.shapePath = stroker.createStroke(self.path) + return self.shapePath def paint(self, p, *args): if self.isSelected(): @@ -598,4 +616,6 @@ def paint(self, p, *args): else: p.setPen(fn.mkPen(100, 100, 250, width=1)) - p.drawLine(0, 0, 0, self.length) + #p.drawLine(0, 0, 0, self.length) + + p.drawPath(self.path) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index b562132cfa..21d74efd9b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1046,10 +1046,10 @@ def childrenBounds(self, frac=None, orthoRange=(None,None), items=None): 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): + if xr is None or (xr[0] is None and xr[1] is None): useX = False xr = (0,0) - if yr is None or yr == (None, None): + if yr is None or (yr[0] is None and yr[1] is None): useY = False yr = (0,0) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index 619d639a78..c82ecc1594 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -169,6 +169,13 @@ def __init__(self, name, opts): self.fieldName = name vals = opts.get('values', []) childs = [{'name': v, 'type': 'color'} for v in vals] + + childs = [] + for v in vals: + ch = ptree.Parameter.create(name=str(v), type='color') + ch.maskValue = v + childs.append(ch) + ptree.types.GroupParameter.__init__(self, name=name, autoIncrementName=True, removable=True, renamable=True, children=[ @@ -191,8 +198,7 @@ def map(self, data): colors[:] = default for v in self.param('Values'): - n = v.name() - mask = data == n + mask = data == v.maskValue c = np.array(fn.colorTuple(v.value())) / 255. colors[mask] = c #scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index a2e1a7b898..65796a15f8 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -92,14 +92,18 @@ def __init__(self, name, opts): 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 + return (vals >= self['Min']) & (vals < self['Max']) ## 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] + childs = [] + for v in vals: + ch = ptree.Parameter.create(name=str(v), type='bool', value=True) + ch.maskValue = v + childs.append(ch) ptree.types.SimpleParameter.__init__(self, name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True, children=childs) @@ -110,6 +114,6 @@ def generateMask(self, data): for c in self: if c.value() is True: continue - key = c.name() + key = c.maskValue mask &= vals != key return mask From 2f510de2caafdd536c8c8b6329d022202a7374b0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Mar 2013 17:17:39 -0400 Subject: [PATCH 011/121] Added PolyLineROI.getArrayRegion --- examples/ROIExamples.py | 2 +- pyqtgraph/graphicsItems/ImageItem.py | 4 ++- pyqtgraph/graphicsItems/ROI.py | 42 ++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py index 56d6b13c7b..a67e279d09 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -56,7 +56,7 @@ rois.append(pg.EllipseROI([60, 10], [30, 20], pen=(3,9))) rois.append(pg.CircleROI([80, 50], [20, 20], pen=(4,9))) #rois.append(pg.LineSegmentROI([[110, 50], [20, 20]], pen=(5,9))) -#rois.append(pg.PolyLineROI([[110, 60], [20, 30], [50, 10]], pen=(6,9))) +rois.append(pg.PolyLineROI([[80, 60], [90, 30], [60, 40]], pen=(6,9), closed=True)) def update(roi): img1b.setImage(roi.getArrayRegion(arr, img1a), levels=(0, arr.max())) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 123612b833..fad88bee75 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -249,7 +249,7 @@ def updateImage(self, *args, **kargs): def render(self): prof = debug.Profiler('ImageItem.render', disabled=True) - if self.image is None: + if self.image is None or self.image.size == 0: return if isinstance(self.lut, collections.Callable): lut = self.lut(self.image) @@ -269,6 +269,8 @@ def paint(self, p, *args): return if self.qimage is None: self.render() + if self.qimage is None: + return prof.mark('render QImage') if self.paintMode is not None: p.setCompositionMode(self.paintMode) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 4da8fa4a69..9cdc8c2978 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -802,7 +802,11 @@ def getArraySlice(self, data, img, axes=(0,1), returnSlice=True): Also returns the transform which maps the ROI into data coordinates. If returnSlice is set to False, the function returns a pair of tuples with the values that would have - been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))""" + been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop)) + + If the slice can not be computed (usually because the scene/transforms are not properly + constructed yet), then the method returns None. + """ #print "getArraySlice" ## Determine shape of array along ROI axes @@ -810,8 +814,11 @@ def getArraySlice(self, data, img, axes=(0,1), returnSlice=True): #print " dshape", dShape ## Determine transform that maps ROI bounding box to image coordinates - tr = self.sceneTransform() * fn.invertQTransform(img.sceneTransform()) - + try: + tr = self.sceneTransform() * fn.invertQTransform(img.sceneTransform()) + except np.linalg.linalg.LinAlgError: + return None + ## Modify transform to scale from image coords to data coords #m = QtGui.QTransform() tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) @@ -1737,11 +1744,34 @@ def boundingRect(self): def shape(self): p = QtGui.QPainterPath() + if len(self.handles) == 0: + return p p.moveTo(self.handles[0]['item'].pos()) for i in range(len(self.handles)): p.lineTo(self.handles[i]['item'].pos()) p.lineTo(self.handles[0]['item'].pos()) - return p + return p + + def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): + sl = self.getArraySlice(data, img, axes=(0,1)) + if sl is None: + return None + sliced = data[sl[0]] + im = QtGui.QImage(sliced.shape[axes[0]], sliced.shape[axes[1]], QtGui.QImage.Format_ARGB32) + im.fill(0x0) + p = QtGui.QPainter(im) + p.setPen(fn.mkPen(None)) + p.setBrush(fn.mkBrush('w')) + p.setTransform(self.itemTransform(img)[0]) + bounds = self.mapRectToItem(img, self.boundingRect()) + p.translate(-bounds.left(), -bounds.top()) + p.drawPath(self.shape()) + p.end() + mask = fn.imageToArray(im)[:,:,0].astype(float) / 255. + shape = [1] * data.ndim + shape[axes[0]] = sliced.shape[axes[0]] + shape[axes[1]] = sliced.shape[axes[1]] + return sliced * mask class LineSegmentROI(ROI): @@ -1845,8 +1875,8 @@ def boundingRect(self): #for h in self.handles: #h['pos'] = h['item'].pos()/self.state['size'][0] - def stateChanged(self): - ROI.stateChanged(self) + def stateChanged(self, finish=True): + ROI.stateChanged(self, finish=finish) if len(self.handles) > 1: self.path = QtGui.QPainterPath() h0 = Point(self.handles[0]['item'].pos()).length() From ad20103ccca980dbef23987bfed5406dc66d91a0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 17 Mar 2013 14:26:23 -0400 Subject: [PATCH 012/121] Check for length=0 arrays when using autoVisible --- pyqtgraph/graphicsItems/PlotCurveItem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index c5a8ec3ffd..d707a3475c 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -112,7 +112,10 @@ def dataBounds(self, ax, frac=1.0, orthoRange=None): if orthoRange is not None: mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) d = d[mask] - d2 = d2[mask] + #d2 = d2[mask] + + if len(d) == 0: + return (None, None) ## Get min/max (or percentiles) of the requested data range if frac >= 1.0: From 87f45186d885aa6ece60debfb38996082af40c2c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 17 Mar 2013 15:16:27 -0400 Subject: [PATCH 013/121] bugfix: prevent auto-range disabling when dragging with one mouse axis diabled --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 68 +++++++++++++++++----- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 654a12c97e..b7785a9d70 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -467,12 +467,32 @@ def suggestPadding(self, axis): padding = 0.02 return padding - def scaleBy(self, s, center=None): + def scaleBy(self, s=None, center=None, x=None, y=None): """ Scale by *s* around given center point (or center of view). - *s* may be a Point or tuple (x, y) + *s* may be a Point or tuple (x, y). + + Optionally, x or y may be specified individually. This allows the other + axis to be left unaffected (note that using a scale factor of 1.0 may + cause slight changes due to floating-point error). """ - scale = Point(s) + if s is not None: + scale = Point(s) + else: + scale = [x, y] + + affect = [True, True] + if scale[0] is None and scale[1] is None: + return + elif scale[0] is None: + affect[0] = False + scale[0] = 1.0 + elif scale[1] is None: + affect[1] = False + scale[1] = 1.0 + + scale = Point(scale) + if self.state['aspectLocked'] is not False: scale[0] = self.state['aspectLocked'] * scale[1] @@ -481,21 +501,37 @@ def scaleBy(self, s, center=None): center = Point(vr.center()) else: center = Point(center) + tl = center + (vr.topLeft()-center) * scale br = center + (vr.bottomRight()-center) * scale - self.setRange(QtCore.QRectF(tl, br), padding=0) - def translateBy(self, t): + if not affect[0]: + self.setYRange(tl.y(), br.y(), padding=0) + elif not affect[1]: + self.setXRange(tl.x(), br.x(), padding=0) + else: + self.setRange(QtCore.QRectF(tl, br), padding=0) + + def translateBy(self, t=None, x=None, y=None): """ Translate the view by *t*, which may be a Point or tuple (x, y). - """ - t = Point(t) - #if viewCoords: ## scale from pixels - #o = self.mapToView(Point(0,0)) - #t = self.mapToView(t) - o + Alternately, x or y may be specified independently, leaving the other + axis unchanged (note that using a translation of 0 may still cause + small changes due to floating-point error). + """ vr = self.targetRect() - self.setRange(vr.translated(t), padding=0) + if t is not None: + t = Point(t) + self.setRange(vr.translated(t), padding=0) + elif x is not None: + x1, x2 = vr.left()+x, vr.right()+x + self.setXRange(x1, x2, padding=0) + elif y is not None: + y1, y2 = vr.top()+y, vr.bottom()+y + self.setYRange(y1, y2, padding=0) + + def enableAutoRange(self, axis=None, enable=True): """ @@ -935,7 +971,10 @@ def mouseDragEvent(self, ev, axis=None): else: tr = dif*mask tr = self.mapToView(tr) - self.mapToView(Point(0,0)) - self.translateBy(tr) + x = tr.x() if mask[0] == 1 else None + y = tr.y() if mask[1] == 1 else None + + self.translateBy(x=x, y=y) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) elif ev.button() & QtCore.Qt.RightButton: #print "vb.rightDrag" @@ -950,8 +989,11 @@ def mouseDragEvent(self, ev, axis=None): tr = self.childGroup.transform() tr = fn.invertQTransform(tr) + x = s[0] if mask[0] == 1 else None + y = s[1] if mask[1] == 1 else None + center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton))) - self.scaleBy(s, center) + self.scaleBy(x=x, y=y, center=center) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) def keyPressEvent(self, ev): From 4716a841175cdf0ee22c346cfd8ca660f26add7d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 19 Mar 2013 11:49:10 -0400 Subject: [PATCH 014/121] AxisItem bugfix: corrected x-linked view update behavior Added MultiplePlotAxes example --- examples/MultiplePlotAxes.py | 67 +++++++++++++++++++++++++++++ pyqtgraph/graphicsItems/AxisItem.py | 13 +++--- 2 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 examples/MultiplePlotAxes.py diff --git a/examples/MultiplePlotAxes.py b/examples/MultiplePlotAxes.py new file mode 100644 index 0000000000..75e0c68041 --- /dev/null +++ b/examples/MultiplePlotAxes.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +""" +Demonstrates a way to put multiple axes around a single plot. + +(This will eventually become a built-in feature of PlotItem) + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +pg.mkQApp() + +pw = pg.PlotWidget() +pw.show() +pw.setWindowTitle('pyqtgraph example: MultiplePlotAxes') +p1 = pw.plotItem +p1.setLabels(left='axis 1') + +## create a new ViewBox, link the right axis to its coordinate system +p2 = pg.ViewBox() +p1.showAxis('right') +p1.scene().addItem(p2) +p1.getAxis('right').linkToView(p2) +p2.setXLink(p1) +p1.getAxis('right').setLabel('axis2', color='#0000ff') + +## create third ViewBox. +## this time we need to create a new axis as well. +p3 = pg.ViewBox() +ax3 = pg.AxisItem('right') +p1.layout.addItem(ax3, 2, 3) +p1.scene().addItem(p3) +ax3.linkToView(p3) +p3.setXLink(p1) +ax3.setZValue(-10000) +ax3.setLabel('axis 3', color='#ff0000') + + +## Handle view resizing +def updateViews(): + ## view has resized; update auxiliary views to match + global p1, p2, p3 + p2.setGeometry(p1.vb.sceneBoundingRect()) + p3.setGeometry(p1.vb.sceneBoundingRect()) + + ## need to re-update linked axes since this was called + ## incorrectly while views had different shapes. + ## (probably this should be handled in ViewBox.resizeEvent) + p2.linkedViewChanged(p1.vb, p2.XAxis) + p3.linkedViewChanged(p1.vb, p3.XAxis) + +updateViews() +p1.vb.sigResized.connect(updateViews) + + +p1.plot([1,2,4,8,16,32]) +p2.addItem(pg.PlotCurveItem([10,20,40,80,40,20], pen='b')) +p3.addItem(pg.PlotCurveItem([3200,1600,800,400,200,100], pen='r')) + +## 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/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 9d1684bd2e..7081f0baad 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -314,10 +314,13 @@ def linkToView(self, view): view.sigResized.connect(self.linkedViewChanged) def linkedViewChanged(self, view, newRange=None): - if self.orientation in ['right', 'left'] and view.yInverted(): + if self.orientation in ['right', 'left']: if newRange is None: newRange = view.viewRange()[1] - self.setRange(*newRange[::-1]) + if view.yInverted(): + self.setRange(*newRange[::-1]) + else: + self.setRange(*newRange) else: if newRange is None: newRange = view.viewRange()[0] @@ -330,18 +333,12 @@ def boundingRect(self): ## 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.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 = rect.adjusted(min(0,self.tickLength), -15, 0, 15) elif self.orientation == 'top': - #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 = rect.adjusted(-15, min(0,self.tickLength), 15, 0) return rect else: From cefb4f9828f9240e7fd4be30567d9350844d0312 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 19 Mar 2013 16:04:46 -0400 Subject: [PATCH 015/121] merged updates from acq4 --- examples/ScatterPlotWidget.py | 38 ++++++++++++++++++ pyqtgraph/flowchart/Terminal.py | 45 +++++++++++++++------- pyqtgraph/graphicsItems/PlotDataItem.py | 17 ++++++-- pyqtgraph/graphicsItems/ScatterPlotItem.py | 5 +-- pyqtgraph/widgets/DataFilterWidget.py | 15 ++++++-- pyqtgraph/widgets/ScatterPlotWidget.py | 20 ++++++++-- 6 files changed, 112 insertions(+), 28 deletions(-) create mode 100644 examples/ScatterPlotWidget.py diff --git a/examples/ScatterPlotWidget.py b/examples/ScatterPlotWidget.py new file mode 100644 index 0000000000..e766e4567e --- /dev/null +++ b/examples/ScatterPlotWidget.py @@ -0,0 +1,38 @@ +# -*- 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 + +pg.mkQApp() + +spw = pg.ScatterPlotWidget() +spw.show() + +data = np.array([ + (1, 1, 3, 4, 'x'), + (2, 3, 3, 7, 'y'), + (3, 2, 5, 2, 'z'), + (4, 4, 6, 9, 'z'), + (5, 3, 6, 7, 'x'), + (6, 5, 2, 6, 'y'), + (7, 5, 7, 2, 'z'), + ], dtype=[('col1', float), ('col2', float), ('col3', int), ('col4', int), ('col5', 'S10')]) + +spw.setFields([ + ('col1', {'units': 'm'}), + ('col2', {'units': 'm'}), + ('col3', {}), + ('col4', {}), + ('col5', {'mode': 'enum', 'values': ['x', 'y', 'z']}), + ]) + +spw.setData(data) + + +## 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/flowchart/Terminal.py b/pyqtgraph/flowchart/Terminal.py index 3066223d6e..45805cd87e 100644 --- a/pyqtgraph/flowchart/Terminal.py +++ b/pyqtgraph/flowchart/Terminal.py @@ -523,6 +523,15 @@ def __init__(self, source, target=None): self.hovered = False self.path = None self.shapePath = None + self.style = { + 'shape': 'line', + 'color': (100, 100, 250), + 'width': 1.0, + 'hoverColor': (150, 150, 250), + 'hoverWidth': 1.0, + 'selectedColor': (200, 200, 0), + 'selectedWidth': 3.0, + } #self.line = QtGui.QGraphicsLineItem(self) self.source.getViewBox().addItem(self) self.updateLine() @@ -537,6 +546,13 @@ def setTarget(self, target): self.target = target self.updateLine() + def setStyle(self, **kwds): + self.style.update(kwds) + if 'shape' in kwds: + self.updateLine() + else: + self.update() + def updateLine(self): start = Point(self.source.connectPoint()) if isinstance(self.target, TerminalGraphicsItem): @@ -547,19 +563,20 @@ def updateLine(self): return self.prepareGeometryChange() - self.path = QtGui.QPainterPath() - self.path.moveTo(start) - self.path.cubicTo(Point(stop.x(), start.y()), Point(start.x(), stop.y()), Point(stop.x(), stop.y())) + self.path = self.generatePath(start, stop) self.shapePath = None - #self.resetTransform() - #ang = (stop-start).angle(Point(0, 1)) - #if ang is None: - #ang = 0 - #self.rotate(ang) - #self.setPos(start) - #self.length = (start-stop).length() self.update() - #self.line.setLine(start.x(), start.y(), stop.x(), stop.y()) + + def generatePath(self, start, stop): + path = QtGui.QPainterPath() + path.moveTo(start) + if self.style['shape'] == 'line': + path.lineTo(stop) + elif self.style['shape'] == 'cubic': + path.cubicTo(Point(stop.x(), start.y()), Point(start.x(), stop.y()), Point(stop.x(), stop.y())) + else: + raise Exception('Invalid shape "%s"; options are "line" or "cubic"' % self.style['shape']) + return path def keyPressEvent(self, ev): if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: @@ -609,12 +626,12 @@ def shape(self): def paint(self, p, *args): if self.isSelected(): - p.setPen(fn.mkPen(200, 200, 0, width=3)) + p.setPen(fn.mkPen(self.style['selectedColor'], width=self.style['selectedWidth'])) else: if self.hovered: - p.setPen(fn.mkPen(150, 150, 250, width=1)) + p.setPen(fn.mkPen(self.style['hoverColor'], width=self.style['hoverWidth'])) else: - p.setPen(fn.mkPen(100, 100, 250, width=1)) + p.setPen(fn.mkPen(self.style['color'], width=self.style['width'])) #p.drawLine(0, 0, 0, self.length) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index c0d5f2f39e..76b743592b 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -84,13 +84,24 @@ def __init__(self, *args, **kargs): **Optimization keyword arguments:** - ========== ===================================================================== + ============ ===================================================================== antialias (bool) By default, antialiasing is disabled to improve performance. Note that in some cases (in particluar, when pxMode=True), points will be rendered antialiased even if this is set to False. + decimate (int) Sub-sample data by selecting every nth sample before plotting + onlyVisible (bool) If True, only plot data that is visible within the X range of + the containing ViewBox. This can improve performance when plotting + very large data sets where only a fraction of the data is visible + at any time. + autoResample (bool) If True, resample the data before plotting to avoid plotting + multiple line segments per pixel. This can improve performance when + viewing very high-density data, but increases the initial overhead + and memory usage. + sampleRate (float) The sample rate of the data along the X axis (for data with + a fixed sample rate). Providing this value improves performance of + the *onlyVisible* and *autoResample* options. identical *deprecated* - decimate (int) sub-sample data by selecting every nth sample before plotting - ========== ===================================================================== + ============ ===================================================================== **Meta-info keyword arguments:** diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 84c0547873..8fdbe0f917 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -677,15 +677,12 @@ def generateFragments(self): pts[1] = self.data['y'] pts = fn.transformCoordinates(tr, pts) self.fragments = [] - pts = np.clip(pts, -2**31, 2**31) ## prevent Qt segmentation fault. + pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. ## Still won't be able to render correctly, though. for i in xrange(len(self.data)): rec = self.data[i] pos = QtCore.QPointF(pts[0,i], pts[1,i]) x,y,w,h = rec['fragCoords'] - if abs(w) > 10000 or abs(h) > 10000: - print self.data - raise Exception("fragment corrupt") rect = QtCore.QRectF(y, x, h, w) self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect)) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index 65796a15f8..93c5f24fbc 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -104,6 +104,10 @@ def __init__(self, name, opts): ch = ptree.Parameter.create(name=str(v), type='bool', value=True) ch.maskValue = v childs.append(ch) + ch = ptree.Parameter.create(name='(other)', type='bool', value=True) + ch.maskValue = '__other__' + childs.append(ch) + ptree.types.SimpleParameter.__init__(self, name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True, children=childs) @@ -111,9 +115,14 @@ def __init__(self, name, opts): def generateMask(self, data): vals = data[self.fieldName] mask = np.ones(len(data), dtype=bool) + otherMask = np.ones(len(data), dtype=bool) for c in self: - if c.value() is True: - continue key = c.maskValue - mask &= vals != key + if key == '__other__': + m = ~otherMask + else: + m = vals != key + otherMask &= m + if c.value() is False: + mask &= m return mask diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index 2e1c191888..5760fac6e4 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -48,13 +48,15 @@ def __init__(self, parent=None): self.addWidget(self.plot) self.data = None + self.mouseOverField = None + self.scatterPlot = 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): + def setFields(self, fields, mouseOverField=None): """ Set the list of field names/units to be processed. @@ -62,6 +64,7 @@ def setFields(self, fields): :func:`ColorMapWidget.setFields ` """ self.fields = OrderedDict(fields) + self.mouseOverField = mouseOverField self.fieldList.clear() for f,opts in fields: item = QtGui.QListWidgetItem(f) @@ -158,7 +161,7 @@ def updatePlot(self): 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) + xy[i] = np.array([vals.index(x) if x in vals else len(vals) for x in xy[i]], dtype=float) axis.setTicks([list(enumerate(vals))]) else: axis.setTicks(None) # reset to automatic ticking @@ -179,7 +182,16 @@ def updatePlot(self): else: y = y[mask] - - self.plot.plot(x, y, **style) + if self.scatterPlot is not None: + try: + self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked) + except: + pass + self.scatterPlot = self.plot.plot(x, y, data=data[mask], **style) + self.scatterPlot.sigPointsClicked.connect(self.plotClicked) + def plotClicked(self, plot, points): + pass + + From e656366fabc1b42dd328afd1bfac4c90e5337ede Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 19 Mar 2013 20:54:05 -0400 Subject: [PATCH 016/121] fixed panning bug introduced in inp:274 --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index b7785a9d70..8769ed92a5 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -524,12 +524,13 @@ def translateBy(self, t=None, x=None, y=None): if t is not None: t = Point(t) self.setRange(vr.translated(t), padding=0) - elif x is not None: - x1, x2 = vr.left()+x, vr.right()+x - self.setXRange(x1, x2, padding=0) - elif y is not None: - y1, y2 = vr.top()+y, vr.bottom()+y - self.setYRange(y1, y2, padding=0) + else: + if x is not None: + x1, x2 = vr.left()+x, vr.right()+x + self.setXRange(x1, x2, padding=0) + if y is not None: + y1, y2 = vr.top()+y, vr.bottom()+y + self.setYRange(y1, y2, padding=0) From ff59924ee00b845324697da0234d12847662601e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 19 Mar 2013 21:22:23 -0400 Subject: [PATCH 017/121] fixed mouse scaling issue introduced in inp a few commits ago added panning plot example --- examples/PanningPlot.py | 37 ++++++++++++++++++++++ pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 7 ++-- 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 examples/PanningPlot.py diff --git a/examples/PanningPlot.py b/examples/PanningPlot.py new file mode 100644 index 0000000000..165240b2ae --- /dev/null +++ b/examples/PanningPlot.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" +Shows use of PlotWidget to display panning data + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +win = pg.GraphicsWindow() +win.setWindowTitle('pyqtgraph example: PanningPlot') + +plt = win.addPlot() +#plt.setAutoVisibleOnly(y=True) +curve = plt.plot() + +data = [] +count = 0 +def update(): + global data, curve, count + data.append(np.random.normal(size=10) + np.sin(count * 0.1) * 5) + if len(data) > 100: + data.pop(0) + curve.setData(np.hstack(data)) + count += 1 + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(50) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 8769ed92a5..3bbb9fe83e 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -950,7 +950,8 @@ def mouseDragEvent(self, ev, axis=None): dif = dif * -1 ## Ignore axes if mouse is disabled - mask = np.array(self.state['mouseEnabled'], dtype=np.float) + mouseEnabled = np.array(self.state['mouseEnabled'], dtype=np.float) + mask = mouseEnabled.copy() if axis is not None: mask[1-axis] = 0.0 @@ -990,8 +991,8 @@ def mouseDragEvent(self, ev, axis=None): tr = self.childGroup.transform() tr = fn.invertQTransform(tr) - x = s[0] if mask[0] == 1 else None - y = s[1] if mask[1] == 1 else None + x = s[0] if mouseEnabled[0] == 1 else None + y = s[1] if mouseEnabled[1] == 1 else None center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton))) self.scaleBy(x=x, y=y, center=center) From a50f74a1fcbc3f1ea076dfa711179b16d16d2ec3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 22 Mar 2013 15:52:44 -0400 Subject: [PATCH 018/121] bugfix: https://bugs.launchpad.net/pyqtgraph/+bug/1157857 --- pyqtgraph/graphicsItems/AxisItem.py | 2 +- pyqtgraph/multiprocess/processes.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 7081f0baad..c4e0138c6b 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -380,7 +380,7 @@ def tickSpacing(self, minVal, maxVal, size): This method is called whenever the axis needs to be redrawn and is a good method to override in subclasses that require control over tick locations. - The return value must be a list of three tuples:: + The return value must be a list of tuples, one for each set of ticks:: [ (major tick spacing, offset), diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 93a109ed87..2b345e8b3c 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -79,7 +79,11 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de 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) + + ## note: we need all three streams to have their own PIPE due to this bug: + ## http://bugs.python.org/issue3905 + self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=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 send pid to child because windows does not have getppid From 7fce0ce5cba39a55e7125f1cc5f56b0ecd21299c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Mar 2013 13:35:29 -0400 Subject: [PATCH 019/121] Allow GraphicsView.setCentralItem(None) --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 ++ pyqtgraph/widgets/GraphicsView.py | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 3bbb9fe83e..338cdde4e1 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1322,6 +1322,8 @@ def quit(): k.destroyed.disconnect() except RuntimeError: ## signal is already disconnected. pass + except TypeError: ## view has already been deleted (?) + pass def locate(self, item, timeout=3.0, children=False): """ diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index dd49ab7dc7..6ddfe93033 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -181,8 +181,9 @@ def setCentralWidget(self, item): if self.centralWidget is not None: self.scene().removeItem(self.centralWidget) self.centralWidget = item - self.sceneObj.addItem(item) - self.resizeEvent(None) + if item is not None: + self.sceneObj.addItem(item) + self.resizeEvent(None) def addItem(self, *args): return self.scene().addItem(*args) @@ -272,7 +273,8 @@ def setRange(self, newRect=None, padding=0.05, lockAspect=None, propagate=True, scaleChanged = True self.range = newRect #print "New Range:", self.range - self.centralWidget.setGeometry(self.range) + if self.centralWidget is not None: + self.centralWidget.setGeometry(self.range) self.updateMatrix(propagate) if scaleChanged: self.sigScaleChanged.emit(self) From 8828892e55e810a17cd33fe726f953591cf539bd Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 26 Mar 2013 13:46:26 -0400 Subject: [PATCH 020/121] merged many changes from acq4 --- examples/GraphItem.py | 3 + examples/Plotting.py | 3 +- examples/ScaleBar.py | 31 ++++ examples/ScatterPlotWidget.py | 18 ++- examples/SimplePlot.py | 7 +- examples/beeswarm.py | 38 +++++ pyqtgraph/exporters/SVGExporter.py | 30 +++- pyqtgraph/functions.py | 56 ++++--- pyqtgraph/graphicsItems/BarGraphItem.py | 149 ++++++++++++++++++ pyqtgraph/graphicsItems/GraphItem.py | 2 + pyqtgraph/graphicsItems/GraphicsItem.py | 14 ++ pyqtgraph/graphicsItems/GraphicsObject.py | 2 +- .../graphicsItems/GraphicsWidgetAnchor.py | 4 +- pyqtgraph/graphicsItems/LabelItem.py | 4 +- pyqtgraph/graphicsItems/PlotCurveItem.py | 1 - pyqtgraph/graphicsItems/ScaleBar.py | 128 ++++++++++----- pyqtgraph/graphicsItems/ScatterPlotItem.py | 1 + pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 17 +- pyqtgraph/widgets/ColorMapWidget.py | 11 +- pyqtgraph/widgets/DataFilterWidget.py | 46 ++++-- pyqtgraph/widgets/ScatterPlotWidget.py | 90 ++++++----- 21 files changed, 527 insertions(+), 128 deletions(-) create mode 100644 examples/ScaleBar.py create mode 100644 examples/beeswarm.py create mode 100644 pyqtgraph/graphicsItems/BarGraphItem.py diff --git a/examples/GraphItem.py b/examples/GraphItem.py index effa6b0b3f..c6362295d0 100644 --- a/examples/GraphItem.py +++ b/examples/GraphItem.py @@ -10,6 +10,9 @@ from pyqtgraph.Qt import QtCore, QtGui import numpy as np +# Enable antialiasing for prettier plots +pg.setConfigOptions(antialias=True) + w = pg.GraphicsWindow() w.setWindowTitle('pyqtgraph example: GraphItem') v = w.addViewBox() diff --git a/examples/Plotting.py b/examples/Plotting.py index 6a3a1d11bf..6578fb2b9d 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -21,7 +21,8 @@ win.resize(1000,600) win.setWindowTitle('pyqtgraph example: Plotting') - +# Enable antialiasing for prettier plots +pg.setConfigOptions(antialias=True) p1 = win.addPlot(title="Basic array plotting", y=np.random.normal(size=100)) diff --git a/examples/ScaleBar.py b/examples/ScaleBar.py new file mode 100644 index 0000000000..5f9675e4fb --- /dev/null +++ b/examples/ScaleBar.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" +Demonstrates ScaleBar +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +pg.mkQApp() +win = pg.GraphicsWindow() +win.setWindowTitle('pyqtgraph example: ScaleBar') + +vb = win.addViewBox() +vb.setAspectLocked() + +img = pg.ImageItem() +img.setImage(np.random.normal(size=(100,100))) +img.scale(0.01, 0.01) +vb.addItem(img) + +scale = pg.ScaleBar(size=0.1) +scale.setParentItem(vb) +scale.anchor((1, 1), (1, 1), offset=(-20, -20)) + +## 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/ScatterPlotWidget.py b/examples/ScatterPlotWidget.py index e766e4567e..563667bd98 100644 --- a/examples/ScatterPlotWidget.py +++ b/examples/ScatterPlotWidget.py @@ -16,8 +16,22 @@ (3, 2, 5, 2, 'z'), (4, 4, 6, 9, 'z'), (5, 3, 6, 7, 'x'), - (6, 5, 2, 6, 'y'), - (7, 5, 7, 2, 'z'), + (6, 5, 4, 6, 'x'), + (7, 5, 8, 2, 'z'), + (8, 1, 2, 4, 'x'), + (9, 2, 3, 7, 'z'), + (0, 6, 0, 2, 'z'), + (1, 3, 1, 2, 'z'), + (2, 5, 4, 6, 'y'), + (3, 4, 8, 1, 'y'), + (4, 7, 6, 8, 'z'), + (5, 8, 7, 4, 'y'), + (6, 1, 2, 3, 'y'), + (7, 5, 3, 9, 'z'), + (8, 9, 3, 1, 'x'), + (9, 2, 6, 2, 'z'), + (0, 3, 4, 6, 'x'), + (1, 5, 9, 3, 'y'), ], dtype=[('col1', float), ('col2', float), ('col3', int), ('col4', int), ('col5', 'S10')]) spw.setFields([ diff --git a/examples/SimplePlot.py b/examples/SimplePlot.py index ec40cf16ef..f572743a4d 100644 --- a/examples/SimplePlot.py +++ b/examples/SimplePlot.py @@ -3,9 +3,12 @@ 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") - +plt = pg.plot(np.random.normal(size=100), title="Simplest possible plotting example") +plt.getAxis('bottom').setTicks([[(x*20, str(x*20)) for x in range(6)]]) ## Start Qt event loop unless running in interactive mode or using pyside. +ex = pg.exporters.SVGExporter.SVGExporter(plt.plotItem.scene()) +ex.export('/home/luke/tmp/test.svg') + if __name__ == '__main__': import sys if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'): diff --git a/examples/beeswarm.py b/examples/beeswarm.py new file mode 100644 index 0000000000..48ee4236b4 --- /dev/null +++ b/examples/beeswarm.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" +Example beeswarm / bar chart +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +win = pg.plot() +win.setWindowTitle('pyqtgraph example: beeswarm') + +data = np.random.normal(size=(4,20)) +data[0] += 5 +data[1] += 7 +data[2] += 5 +data[3] = 10 + data[3] * 2 + +## Make bar graph +#bar = pg.BarGraphItem(x=range(4), height=data.mean(axis=1), width=0.5, brush=0.4) +#win.addItem(bar) + +## add scatter plots on top +for i in range(4): + xvals = pg.pseudoScatter(data[i], spacing=0.4, bidir=True) * 0.2 + win.plot(x=xvals+i, y=data[i], pen=None, symbol='o', symbolBrush=pg.intColor(i,6,maxValue=128)) + +## Make error bars +err = pg.ErrorBarItem(x=np.arange(4), y=data.mean(axis=1), height=data.std(axis=1), beam=0.5, pen={'color':'w', 'width':2}) +win.addItem(err) + + +## 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/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index b284db89a1..672897ab3b 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -304,7 +304,36 @@ def _generateItemSvg(item, nodes=None, root=None): def correctCoordinates(node, item): ## Remove transformation matrices from tags by applying matrix to coordinates inside. + ## Each item is represented by a single top-level group with one or more groups inside. + ## Each inner group contains one or more drawing primitives, possibly of different types. groups = node.getElementsByTagName('g') + + ## Since we leave text unchanged, groups which combine text and non-text primitives must be split apart. + ## (if at some point we start correcting text transforms as well, then it should be safe to remove this) + groups2 = [] + for grp in groups: + subGroups = [grp.cloneNode(deep=False)] + textGroup = None + for ch in grp.childNodes[:]: + if isinstance(ch, xml.Element): + if textGroup is None: + textGroup = ch.tagName == 'text' + if ch.tagName == 'text': + if textGroup is False: + subGroups.append(grp.cloneNode(deep=False)) + textGroup = True + else: + if textGroup is True: + subGroups.append(grp.cloneNode(deep=False)) + textGroup = False + subGroups[-1].appendChild(ch) + groups2.extend(subGroups) + for sg in subGroups: + node.insertBefore(sg, grp) + node.removeChild(grp) + groups = groups2 + + for grp in groups: matrix = grp.getAttribute('transform') match = re.match(r'matrix\((.*)\)', matrix) @@ -374,7 +403,6 @@ def correctCoordinates(node, item): if removeTransform: grp.removeAttribute('transform') - def itemTransform(item, root): ## Return the transformation mapping item to root diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 84a5c57342..5f820a9a38 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1930,9 +1930,9 @@ def invertQTransform(tr): return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1]) -def pseudoScatter(data, spacing=None, shuffle=True): +def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): """ - Used for examining the distribution of values in a set. + Used for examining the distribution of values in a set. Produces scattering as in beeswarm or column scatter plots. Given a list of x-values, construct a set of y-values such that an x,y scatter-plot will not have overlapping points (it will look similar to a histogram). @@ -1959,23 +1959,41 @@ def pseudoScatter(data, spacing=None, shuffle=True): xmask = dx < s2 # exclude anything too far away if xmask.sum() > 0: - dx = dx[xmask] - dy = (s2 - dx)**0.5 - limits = np.empty((2,len(dy))) # ranges of y-values to exclude - limits[0] = y0[xmask] - dy - limits[1] = y0[xmask] + dy - - while True: - # ignore anything below this y-value - mask = limits[1] >= y - limits = limits[:,mask] - - # are we inside an excluded region? - mask = (limits[0] < y) & (limits[1] > y) - if mask.sum() == 0: - break - y = limits[:,mask].max() - + if bidir: + dirs = [-1, 1] + else: + dirs = [1] + yopts = [] + for direction in dirs: + y = 0 + dx2 = dx[xmask] + dy = (s2 - dx2)**0.5 + limits = np.empty((2,len(dy))) # ranges of y-values to exclude + limits[0] = y0[xmask] - dy + limits[1] = y0[xmask] + dy + while True: + # ignore anything below this y-value + if direction > 0: + mask = limits[1] >= y + else: + mask = limits[0] <= y + + limits2 = limits[:,mask] + + # are we inside an excluded region? + mask = (limits2[0] < y) & (limits2[1] > y) + if mask.sum() == 0: + break + + if direction > 0: + y = limits2[:,mask].max() + else: + y = limits2[:,mask].min() + yopts.append(y) + if bidir: + y = yopts[0] if -yopts[0] < yopts[1] else yopts[1] + else: + y = yopts[0] yvals[i] = y return yvals[np.argsort(inds)] ## un-shuffle values before returning diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py new file mode 100644 index 0000000000..0527e9f116 --- /dev/null +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -0,0 +1,149 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +from .GraphicsObject import GraphicsObject +import numpy as np + +__all__ = ['BarGraphItem'] + +class BarGraphItem(GraphicsObject): + def __init__(self, **opts): + """ + Valid keyword options are: + x, x0, x1, y, y0, y1, width, height, pen, brush + + x specifies the x-position of the center of the bar. + x0, x1 specify left and right edges of the bar, respectively. + width specifies distance from x0 to x1. + You may specify any combination: + + x, width + x0, width + x1, width + x0, x1 + + Likewise y, y0, y1, and height. + If only height is specified, then y0 will be set to 0 + + Example uses: + + BarGraphItem(x=range(5), height=[1,5,2,4,3], width=0.5) + + + """ + GraphicsObject.__init__(self) + self.opts = dict( + x=None, + y=None, + x0=None, + y0=None, + x1=None, + y1=None, + height=None, + width=None, + pen=None, + brush=None, + pens=None, + brushes=None, + ) + self.setOpts(**opts) + + def setOpts(self, **opts): + self.opts.update(opts) + self.picture = None + self.update() + self.informViewBoundsChanged() + + def drawPicture(self): + self.picture = QtGui.QPicture() + p = QtGui.QPainter(self.picture) + + pen = self.opts['pen'] + pens = self.opts['pens'] + + if pen is None and pens is None: + pen = pg.getConfigOption('foreground') + + brush = self.opts['brush'] + brushes = self.opts['brushes'] + if brush is None and brushes is None: + brush = (128, 128, 128) + + def asarray(x): + if x is None or np.isscalar(x) or isinstance(x, np.ndarray): + return x + return np.array(x) + + + x = asarray(self.opts.get('x')) + x0 = asarray(self.opts.get('x0')) + x1 = asarray(self.opts.get('x1')) + width = asarray(self.opts.get('width')) + + if x0 is None: + if width is None: + raise Exception('must specify either x0 or width') + if x1 is not None: + x0 = x1 - width + elif x is not None: + x0 = x - width/2. + else: + raise Exception('must specify at least one of x, x0, or x1') + if width is None: + if x1 is None: + raise Exception('must specify either x1 or width') + width = x1 - x0 + + y = asarray(self.opts.get('y')) + y0 = asarray(self.opts.get('y0')) + y1 = asarray(self.opts.get('y1')) + height = asarray(self.opts.get('height')) + + if y0 is None: + if height is None: + y0 = 0 + elif y1 is not None: + y0 = y1 - height + elif y is not None: + y0 = y - height/2. + else: + y0 = 0 + if height is None: + if y1 is None: + raise Exception('must specify either y1 or height') + height = y1 - y0 + + p.setPen(pg.mkPen(pen)) + p.setBrush(pg.mkBrush(brush)) + for i in range(len(x0)): + if pens is not None: + p.setPen(pg.mkPen(pens[i])) + if brushes is not None: + p.setBrush(pg.mkBrush(brushes[i])) + + if np.isscalar(y0): + y = y0 + else: + y = y0[i] + if np.isscalar(width): + w = width + else: + w = width[i] + + p.drawRect(QtCore.QRectF(x0[i], y, w, height[i])) + + + p.end() + self.prepareGeometryChange() + + + def paint(self, p, *args): + if self.picture is None: + self.drawPicture() + self.picture.play(p) + + def boundingRect(self): + if self.picture is None: + self.drawPicture() + return QtCore.QRectF(self.picture.boundingRect()) + + \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/GraphItem.py b/pyqtgraph/graphicsItems/GraphItem.py index be6138cec4..79f8804a63 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -103,6 +103,8 @@ def generatePicture(self): def paint(self, p, *args): if self.picture == None: self.generatePicture() + if pg.getConfigOption('antialias') is True: + p.setRenderHint(p.Antialiasing) self.picture.play(p) def boundingRect(self): diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 3a63afa7c3..40ff6bc58c 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -446,6 +446,14 @@ def transformAngle(self, relativeItem=None): #print " --> ", ch2.scene() #self.setChildScene(ch2) + def parentChanged(self): + """Called when the item's parent has changed. + This method handles connecting / disconnecting from ViewBox signals + to make sure viewRangeChanged works properly. It should generally be + extended, not overridden.""" + self._updateView() + + def _updateView(self): ## called to see whether this item has a new view to connect to ## NOTE: This is called from GraphicsObject.itemChange or GraphicsWidget.itemChange. @@ -496,6 +504,12 @@ def _updateView(self): ## inform children that their view might have changed self._replaceView(oldView) + self.viewChanged(view, oldView) + + def viewChanged(self, view, oldView): + """Called when this item's view has changed + (ie, the item has been added to or removed from a ViewBox)""" + pass def _replaceView(self, oldView, item=None): if item is None: diff --git a/pyqtgraph/graphicsItems/GraphicsObject.py b/pyqtgraph/graphicsItems/GraphicsObject.py index 121a67ea15..e4c5cd81cb 100644 --- a/pyqtgraph/graphicsItems/GraphicsObject.py +++ b/pyqtgraph/graphicsItems/GraphicsObject.py @@ -19,7 +19,7 @@ def __init__(self, *args): def itemChange(self, change, value): ret = QtGui.QGraphicsObject.itemChange(self, change, value) if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]: - self._updateView() + self.parentChanged() if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: self.informViewBoundsChanged() diff --git a/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py b/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py index 9770b66198..3174e6e042 100644 --- a/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py +++ b/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py @@ -5,7 +5,9 @@ class GraphicsWidgetAnchor(object): """ Class used to allow GraphicsWidgets to anchor to a specific position on their - parent. + parent. The item will be automatically repositioned if the parent is resized. + This is used, for example, to anchor a LegendItem to a corner of its parent + PlotItem. """ diff --git a/pyqtgraph/graphicsItems/LabelItem.py b/pyqtgraph/graphicsItems/LabelItem.py index 17301fb3ad..6101c4bc2b 100644 --- a/pyqtgraph/graphicsItems/LabelItem.py +++ b/pyqtgraph/graphicsItems/LabelItem.py @@ -2,11 +2,12 @@ import pyqtgraph.functions as fn import pyqtgraph as pg from .GraphicsWidget import GraphicsWidget +from .GraphicsWidgetAnchor import GraphicsWidgetAnchor __all__ = ['LabelItem'] -class LabelItem(GraphicsWidget): +class LabelItem(GraphicsWidget, GraphicsWidgetAnchor): """ GraphicsWidget displaying text. Used mainly as axis labels, titles, etc. @@ -17,6 +18,7 @@ class LabelItem(GraphicsWidget): def __init__(self, text=' ', parent=None, angle=0, **args): GraphicsWidget.__init__(self, parent) + GraphicsWidgetAnchor.__init__(self) self.item = QtGui.QGraphicsTextItem(self) self.opts = { 'color': None, diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index d707a3475c..fc8fe4c281 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -402,7 +402,6 @@ def paint(self, p, opt, widget): aa = self.opts['antialias'] p.setRenderHint(p.Antialiasing, aa) - if self.opts['brush'] is not None and self.opts['fillLevel'] is not None: if self.fillPath is None: diff --git a/pyqtgraph/graphicsItems/ScaleBar.py b/pyqtgraph/graphicsItems/ScaleBar.py index 961f07d7df..768f69789c 100644 --- a/pyqtgraph/graphicsItems/ScaleBar.py +++ b/pyqtgraph/graphicsItems/ScaleBar.py @@ -1,50 +1,104 @@ from pyqtgraph.Qt import QtGui, QtCore -from .UIGraphicsItem import * +from .GraphicsObject import * +from .GraphicsWidgetAnchor import * +from .TextItem import TextItem import numpy as np import pyqtgraph.functions as fn +import pyqtgraph as pg __all__ = ['ScaleBar'] -class ScaleBar(UIGraphicsItem): + +class ScaleBar(GraphicsObject, GraphicsWidgetAnchor): """ - Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view. + Displays a rectangular bar to indicate the relative scale of objects on the view. """ - def __init__(self, size, width=5, color=(100, 100, 255)): - UIGraphicsItem.__init__(self) + def __init__(self, size, width=5, brush=None, pen=None, suffix='m'): + GraphicsObject.__init__(self) + GraphicsWidgetAnchor.__init__(self) + self.setFlag(self.ItemHasNoContents) self.setAcceptedMouseButtons(QtCore.Qt.NoButton) - self.brush = fn.mkBrush(color) - self.pen = fn.mkPen((0,0,0)) + if brush is None: + brush = pg.getConfigOption('foreground') + self.brush = fn.mkBrush(brush) + self.pen = fn.mkPen(pen) self._width = width self.size = size - def paint(self, p, opt, widget): - UIGraphicsItem.paint(self, p, opt, widget) - - rect = self.boundingRect() - unit = self.pixelSize() - y = rect.top() + (rect.bottom()-rect.top()) * 0.02 - y1 = y + unit[1]*self._width - x = rect.right() + (rect.left()-rect.right()) * 0.02 - x1 = x - self.size - - p.setPen(self.pen) - p.setBrush(self.brush) - rect = QtCore.QRectF( - QtCore.QPointF(x1, y1), - QtCore.QPointF(x, y) - ) - p.translate(x1, y1) - p.scale(rect.width(), rect.height()) - p.drawRect(0, 0, 1, 1) - - alpha = np.clip(((self.size/unit[0]) - 40.) * 255. / 80., 0, 255) - p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha))) - for i in range(1, 10): - #x2 = x + (x1-x) * 0.1 * i - x2 = 0.1 * i - p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1)) - - - def setSize(self, s): - self.size = s + self.bar = QtGui.QGraphicsRectItem() + self.bar.setPen(self.pen) + self.bar.setBrush(self.brush) + self.bar.setParentItem(self) + + self.text = TextItem(text=fn.siFormat(size, suffix=suffix), anchor=(0.5,1)) + self.text.setParentItem(self) + + def parentChanged(self): + view = self.parentItem() + if view is None: + return + view.sigRangeChanged.connect(self.updateBar) + self.updateBar() + + + def updateBar(self): + view = self.parentItem() + if view is None: + return + p1 = view.mapFromViewToItem(self, QtCore.QPointF(0,0)) + p2 = view.mapFromViewToItem(self, QtCore.QPointF(self.size,0)) + w = (p2-p1).x() + self.bar.setRect(QtCore.QRectF(-w, 0, w, self._width)) + self.text.setPos(-w/2., 0) + + def boundingRect(self): + return QtCore.QRectF() + + + + + +#class ScaleBar(UIGraphicsItem): + #""" + #Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view. + #""" + #def __init__(self, size, width=5, color=(100, 100, 255)): + #UIGraphicsItem.__init__(self) + #self.setAcceptedMouseButtons(QtCore.Qt.NoButton) + + #self.brush = fn.mkBrush(color) + #self.pen = fn.mkPen((0,0,0)) + #self._width = width + #self.size = size + + #def paint(self, p, opt, widget): + #UIGraphicsItem.paint(self, p, opt, widget) + + #rect = self.boundingRect() + #unit = self.pixelSize() + #y = rect.top() + (rect.bottom()-rect.top()) * 0.02 + #y1 = y + unit[1]*self._width + #x = rect.right() + (rect.left()-rect.right()) * 0.02 + #x1 = x - self.size + + #p.setPen(self.pen) + #p.setBrush(self.brush) + #rect = QtCore.QRectF( + #QtCore.QPointF(x1, y1), + #QtCore.QPointF(x, y) + #) + #p.translate(x1, y1) + #p.scale(rect.width(), rect.height()) + #p.drawRect(0, 0, 1, 1) + + #alpha = np.clip(((self.size/unit[0]) - 40.) * 255. / 80., 0, 255) + #p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha))) + #for i in range(1, 10): + ##x2 = x + (x1-x) * 0.1 * i + #x2 = 0.1 * i + #p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1)) + + + #def setSize(self, s): + #self.size = s diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 8fdbe0f917..29bfeaac23 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -740,6 +740,7 @@ def paint(self, p, *args): drawSymbol(p2, *self.getSpotOpts(rec, scale)) p2.end() + p.setRenderHint(p.Antialiasing, aa) self.picture.play(p) def points(self): diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 3516c9f6ed..8769ed92a5 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -524,12 +524,13 @@ def translateBy(self, t=None, x=None, y=None): if t is not None: t = Point(t) self.setRange(vr.translated(t), padding=0) - elif x is not None: - x1, x2 = vr.left()+x, vr.right()+x - self.setXRange(x1, x2, padding=0) - elif y is not None: - y1, y2 = vr.top()+y, vr.bottom()+y - self.setYRange(y1, y2, padding=0) + else: + if x is not None: + x1, x2 = vr.left()+x, vr.right()+x + self.setXRange(x1, x2, padding=0) + if y is not None: + y1, y2 = vr.top()+y, vr.bottom()+y + self.setYRange(y1, y2, padding=0) @@ -1090,10 +1091,10 @@ def childrenBounds(self, frac=None, orthoRange=(None,None), items=None): 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[0] is None and xr[1] is None) or np.isnan(xr).any() or np.isinf(xr).any(): + if xr is None or xr == (None, None) or np.isnan(xr).any() or np.isinf(xr).any(): useX = False xr = (0,0) - if yr is None or (yr[0] is None and yr[1] is None) or np.isnan(yr).any() or np.isinf(yr).any(): + if yr is None or yr == (None, None) or np.isnan(yr).any() or np.isinf(yr).any(): useY = False yr = (0,0) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index c82ecc1594..26539d7ec4 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -72,7 +72,8 @@ def setFields(self, fields): (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'. + color when mode=='enum'. Optionally may specify a dict + instead {value: name}. ============== ============================================================ """ self.fields = OrderedDict(fields) @@ -168,12 +169,14 @@ class EnumColorMapItem(ptree.types.GroupParameter): def __init__(self, name, opts): self.fieldName = name vals = opts.get('values', []) + if isinstance(vals, list): + vals = OrderedDict([(v,str(v)) for v in vals]) childs = [{'name': v, 'type': 'color'} for v in vals] childs = [] - for v in vals: - ch = ptree.Parameter.create(name=str(v), type='color') - ch.maskValue = v + for val,vname in vals.items(): + ch = ptree.Parameter.create(name=vname, type='color') + ch.maskValue = val childs.append(ch) ptree.types.GroupParameter.__init__(self, diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index 93c5f24fbc..c94f6c685d 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -2,6 +2,7 @@ import pyqtgraph.parametertree as ptree import numpy as np from pyqtgraph.pgcollections import OrderedDict +import pyqtgraph as pg __all__ = ['DataFilterWidget'] @@ -22,6 +23,7 @@ def __init__(self): self.setFields = self.params.setFields self.filterData = self.params.filterData + self.describe = self.params.describe def filterChanged(self): self.sigFilterChanged.emit(self) @@ -70,18 +72,28 @@ def generateMask(self, data): for fp in self: if fp.value() is False: continue - mask &= fp.generateMask(data) + mask &= fp.generateMask(data, mask.copy()) #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 + + def describe(self): + """Return a list of strings describing the currently enabled filters.""" + desc = [] + for fp in self: + if fp.value() is False: + continue + desc.append(fp.describe()) + return desc class RangeFilterItem(ptree.types.SimpleParameter): def __init__(self, name, opts): self.fieldName = name units = opts.get('units', '') + self.units = units ptree.types.SimpleParameter.__init__(self, name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True, children=[ @@ -90,19 +102,24 @@ def __init__(self, name, opts): dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True), ]) - def generateMask(self, data): - vals = data[self.fieldName] - return (vals >= self['Min']) & (vals < self['Max']) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections + def generateMask(self, data, mask): + vals = data[self.fieldName][mask] + mask[mask] = (vals >= self['Min']) & (vals < self['Max']) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections + return mask + def describe(self): + return "%s < %s < %s" % (pg.siFormat(self['Min'], suffix=self.units), self.fieldName, pg.siFormat(self['Max'], suffix=self.units)) class EnumFilterItem(ptree.types.SimpleParameter): def __init__(self, name, opts): self.fieldName = name vals = opts.get('values', []) childs = [] - for v in vals: - ch = ptree.Parameter.create(name=str(v), type='bool', value=True) - ch.maskValue = v + if isinstance(vals, list): + vals = OrderedDict([(v,str(v)) for v in vals]) + for val,vname in vals.items(): + ch = ptree.Parameter.create(name=vname, type='bool', value=True) + ch.maskValue = val childs.append(ch) ch = ptree.Parameter.create(name='(other)', type='bool', value=True) ch.maskValue = '__other__' @@ -112,10 +129,10 @@ def __init__(self, name, opts): 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) - otherMask = np.ones(len(data), dtype=bool) + def generateMask(self, data, startMask): + vals = data[self.fieldName][startMask] + mask = np.ones(len(vals), dtype=bool) + otherMask = np.ones(len(vals), dtype=bool) for c in self: key = c.maskValue if key == '__other__': @@ -125,4 +142,9 @@ def generateMask(self, data): otherMask &= m if c.value() is False: mask &= m - return mask + startMask[startMask] = mask + return startMask + + def describe(self): + vals = [ch.name() for ch in self if ch.value() is True] + return "%s: %s" % (self.fieldName, ', '.join(vals)) \ No newline at end of file diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index 5760fac6e4..fe785e0458 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -6,6 +6,7 @@ import pyqtgraph.functions as fn import numpy as np from pyqtgraph.pgcollections import OrderedDict +import pyqtgraph as pg __all__ = ['ScatterPlotWidget'] @@ -47,6 +48,12 @@ def __init__(self, parent=None): self.ctrlPanel.addWidget(self.ptree) self.addWidget(self.plot) + bg = pg.mkColor(pg.getConfigOption('background')) + bg.setAlpha(150) + self.filterText = pg.TextItem(border=pg.getConfigOption('foreground'), color=bg) + self.filterText.setPos(60,20) + self.filterText.setParentItem(self.plot.plotItem) + self.data = None self.mouseOverField = None self.scatterPlot = None @@ -97,6 +104,13 @@ def fieldSelectionChanged(self): def filterChanged(self, f): self.filtered = None self.updatePlot() + desc = self.filter.describe() + if len(desc) == 0: + self.filterText.setVisible(False) + else: + self.filterText.setText('\n'.join(desc)) + self.filterText.setVisible(True) + def updatePlot(self): self.plot.clear() @@ -125,69 +139,69 @@ def updatePlot(self): 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 + #x = data[sel[0]] + #y = None + xy = [data[sel[0]], 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] + xy = [data[sel[0]], data[sel[1]]] + #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 ## convert enum-type fields to float, set axis labels - xy = [x,y] + enum = [False, False] 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'): + if xy[i] is not None and (self.fields[sel[i]].get('mode', None) == 'enum' or 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 len(vals) for x in xy[i]], dtype=float) axis.setTicks([list(enumerate(vals))]) + enum[i] = True 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] + mask = np.ones(len(xy[0]), dtype=bool) + if xy[0].dtype.kind == 'f': + mask &= ~np.isnan(xy[0]) + if xy[1] is not None and xy[1].dtype.kind == 'f': + mask &= ~np.isnan(xy[1]) + + xy[0] = xy[0][mask] style['symbolBrush'] = colors[mask] ## Scatter y-values for a histogram-like appearance - if y is None: - y = fn.pseudoScatter(x) + if xy[1] is None: + ## column scatter plot + xy[1] = fn.pseudoScatter(xy[0]) else: - y = y[mask] - + ## beeswarm plots + xy[1] = xy[1][mask] + for ax in [0,1]: + if not enum[ax]: + continue + for i in range(int(xy[ax].max())+1): + keymask = xy[ax] == i + scatter = pg.pseudoScatter(xy[1-ax][keymask], bidir=True) + scatter *= 0.2 / np.abs(scatter).max() + xy[ax][keymask] += scatter + if self.scatterPlot is not None: try: self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked) except: pass - self.scatterPlot = self.plot.plot(x, y, data=data[mask], **style) + self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data[mask], **style) self.scatterPlot.sigPointsClicked.connect(self.plotClicked) From 829503f3d291b67ee49caffcfdaf75539a75c0c6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 27 Mar 2013 20:24:01 -0400 Subject: [PATCH 021/121] AxisItem updates: - better handling of tick text / label area - ability to truncate axis lines at the last tick --- pyqtgraph/graphicsItems/AxisItem.py | 213 ++++++++++++++----- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 1 + 2 files changed, 161 insertions(+), 53 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index c4e0138c6b..bf3c874371 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -39,19 +39,19 @@ def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLen if orientation not in ['left', 'right', 'top', 'bottom']: raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") if orientation in ['left', 'right']: - #self.setMinimumWidth(25) - #self.setSizePolicy(QtGui.QSizePolicy( - #QtGui.QSizePolicy.Minimum, - #QtGui.QSizePolicy.Expanding - #)) self.label.rotate(-90) - #else: - #self.setMinimumHeight(50) - #self.setSizePolicy(QtGui.QSizePolicy( - #QtGui.QSizePolicy.Expanding, - #QtGui.QSizePolicy.Minimum - #)) - #self.drawLabel = False + + self.style = { + 'tickTextOffset': 3, ## spacing between text and axis + 'tickTextWidth': 30, ## space reserved for tick text + 'tickTextHeight': 18, + 'autoExpandTextSpace': True, ## automatically expand text space if needed + 'tickFont': None, + 'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick + } + + self.textWidth = 30 ## Keeps track of maximum width / height of tick text + self.textHeight = 18 self.labelText = '' self.labelUnits = '' @@ -60,7 +60,6 @@ def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLen 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 @@ -184,7 +183,7 @@ def setLabel(self, text=None, units=None, unitPrefix=None, **args): if len(args) > 0: self.labelStyle = args self.label.setHtml(self.labelString()) - self.resizeEvent() + self._adjustSize() self.picture = None self.update() @@ -203,14 +202,43 @@ def labelString(self): style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle]) return asUnicode("%s") % (style, s) - + + def _updateMaxTextSize(self, x): + ## Informs that the maximum tick size orthogonal to the axis has + ## changed; we use this to decide whether the item needs to be resized + ## to accomodate. + if self.orientation in ['left', 'right']: + mx = max(self.textWidth, x) + if mx > self.textWidth: + self.textWidth = mx + if self.style['autoExpandTextSpace'] is True: + self.setWidth() + #return True ## size has changed + else: + mx = max(self.textHeight, x) + if mx > self.textHeight: + self.textHeight = mx + if self.style['autoExpandTextSpace'] is True: + self.setHeight() + #return True ## size has changed + + def _adjustSize(self): + if self.orientation in ['left', 'right']: + self.setWidth() + else: + self.setHeight() + def setHeight(self, h=None): """Set the height of this axis reserved for ticks and tick labels. The height of the axis label is automatically added.""" if h is None: - h = self.textHeight + max(0, self.tickLength) + if self.style['autoExpandTextSpace'] is True: + h = self.textHeight + else: + h = self.style['tickTextHeight'] + h += max(0, self.tickLength) + self.style['tickTextOffset'] if self.label.isVisible(): - h += self.textHeight + h += self.label.boundingRect().height() * 0.8 self.setMaximumHeight(h) self.setMinimumHeight(h) self.picture = None @@ -220,11 +248,16 @@ def setWidth(self, w=None): """Set the width of this axis reserved for ticks and tick labels. The width of the axis label is automatically added.""" if w is None: - w = max(0, self.tickLength) + 40 + if self.style['autoExpandTextSpace'] is True: + w = self.textWidth + else: + w = self.style['tickTextWidth'] + w += max(0, self.tickLength) + self.style['tickTextOffset'] if self.label.isVisible(): - w += self.textHeight + w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate self.setMaximumWidth(w) self.setMinimumWidth(w) + self.picture = None def pen(self): if self._pen is None: @@ -346,12 +379,14 @@ def boundingRect(self): def paint(self, p, opt, widget): if self.picture is None: - self.picture = QtGui.QPicture() - painter = QtGui.QPainter(self.picture) try: - self.drawPicture(painter) + picture = QtGui.QPicture() + painter = QtGui.QPainter(picture) + specs = self.generateDrawSpecs(painter) + self.drawPicture(painter, *specs) finally: painter.end() + self.picture = picture #p.setRenderHint(p.Antialiasing, False) ## Sometimes we get a segfault here ??? #p.setRenderHint(p.TextAntialiasing, True) self.picture.play(p) @@ -540,12 +575,13 @@ def tickStrings(self, values, scale, spacing): def logTickStrings(self, values, scale, spacing): return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)] - def drawPicture(self, p): - - p.setRenderHint(p.Antialiasing, False) - p.setRenderHint(p.TextAntialiasing, True) - - prof = debug.Profiler("AxisItem.paint", disabled=True) + def generateDrawSpecs(self, p): + """ + Calls tickValues() and tickStrings to determine where and how ticks should + be drawn, then generates from this a set of drawing commands to be + interpreted by drawPicture(). + """ + prof = debug.Profiler("AxisItem.generateDrawSpecs", disabled=True) #bounds = self.boundingRect() bounds = self.mapRectFromParent(self.geometry()) @@ -582,11 +618,6 @@ def drawPicture(self, p): axis = 1 #print tickStart, tickStop, span - ## draw long line along axis - p.setPen(self.pen()) - p.drawLine(*span) - p.translate(0.5,0) ## resolves some damn pixel ambiguity - ## determine size of this item in pixels points = list(map(self.mapToDevice, span)) if None in points: @@ -633,7 +664,7 @@ def drawPicture(self, p): ## draw ticks ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching) ## draw three different intervals, long ticks first - + tickSpecs = [] for i in range(len(tickLevels)): tickPositions.append([]) ticks = tickLevels[i][1] @@ -663,15 +694,38 @@ def drawPicture(self, p): color = tickPen.color() color.setAlpha(lineAlpha) tickPen.setColor(color) - p.setPen(tickPen) - p.drawLine(Point(p1), Point(p2)) - prof.mark('draw ticks') + tickSpecs.append((tickPen, Point(p1), Point(p2))) + prof.mark('compute ticks') - ## Draw text until there is no more room (or no more text) - if self.tickFont is not None: - p.setFont(self.tickFont) + ## This is where the long axis line should be drawn + + if self.style['stopAxisAtTick'][0] is True: + stop = max(span[0].y(), min(map(min, tickPositions))) + if axis == 0: + span[0].setY(stop) + else: + span[0].setX(stop) + if self.style['stopAxisAtTick'][1] is True: + stop = min(span[1].y(), max(map(max, tickPositions))) + if axis == 0: + span[1].setY(stop) + else: + span[1].setX(stop) + axisSpec = (self.pen(), span[0], span[1]) + + + + textOffset = self.style['tickTextOffset'] ## spacing between axis and text + #if self.style['autoExpandTextSpace'] is True: + #textWidth = self.textWidth + #textHeight = self.textHeight + #else: + #textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text + #textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text + textRects = [] + textSpecs = [] ## list of draw for i in range(len(tickLevels)): ## Get the list of strings to display for this level if tickStrings is None: @@ -688,18 +742,34 @@ def drawPicture(self, p): if tickPositions[i][j] is None: strings[j] = 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]) + ## Measure density of text; decide whether to draw this level + rects = [] + for s in strings: + if s is None: + rects.append(None) + else: + br = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s)) + ## boundingRect is usually just a bit too large + ## (but this probably depends on per-font metrics?) + br.setHeight(br.height() * 0.8) + + rects.append(br) + textRects.append(rects[-1]) + if i > 0: ## always draw top level ## measure all text, make sure there's enough room if axis == 0: textSize = np.sum([r.height() for r in textRects]) + textSize2 = np.max([r.width() for r in textRects]) else: textSize = np.sum([r.width() for r in textRects]) + textSize2 = np.max([r.height() for r in textRects]) ## If the strings are too crowded, stop drawing text now textFillRatio = float(textSize) / lengthInPixels if textFillRatio > 0.7: break + #spacing, values = tickLevels[best] #strings = self.tickStrings(values, self.scale, spacing) for j in range(len(strings)): @@ -708,24 +778,61 @@ def drawPicture(self, p): continue vstr = str(vstr) x = tickPositions[i][j] - textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) + #textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) + textRect = rects[j] height = textRect.height() - self.textHeight = height + width = textRect.width() + #self.textHeight = height + offset = max(0,self.tickLength) + textOffset if self.orientation == 'left': - textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop-100, x-(height/2), 99-max(0,self.tickLength), height) + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height) elif self.orientation == 'right': - textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop+max(0,self.tickLength)+1, x-(height/2), 100-max(0,self.tickLength), height) + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop+offset, x-(height/2), width, height) elif self.orientation == 'top': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom - rect = QtCore.QRectF(x-100, tickStop-max(0,self.tickLength)-height, 200, height) + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom + rect = QtCore.QRectF(x-width/2., tickStop-offset-height, width, height) elif self.orientation == 'bottom': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop - rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height) + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop + rect = QtCore.QRectF(x-width/2., tickStop+offset, width, height) - p.setPen(self.pen()) - p.drawText(rect, textFlags, vstr) + #p.setPen(self.pen()) + #p.drawText(rect, textFlags, vstr) + textSpecs.append((rect, textFlags, vstr)) + prof.mark('compute text') + + ## update max text size if needed. + self._updateMaxTextSize(textSize2) + + return (axisSpec, tickSpecs, textSpecs) + + def drawPicture(self, p, axisSpec, tickSpecs, textSpecs): + prof = debug.Profiler("AxisItem.drawPicture", disabled=True) + + p.setRenderHint(p.Antialiasing, False) + p.setRenderHint(p.TextAntialiasing, True) + + ## draw long line along axis + pen, p1, p2 = axisSpec + p.setPen(pen) + p.drawLine(p1, p2) + p.translate(0.5,0) ## resolves some damn pixel ambiguity + + ## draw ticks + for pen, p1, p2 in tickSpecs: + p.setPen(pen) + p.drawLine(p1, p2) + prof.mark('draw ticks') + + ## Draw all text + if self.tickFont is not None: + p.setFont(self.tickFont) + p.setPen(self.pen()) + for rect, flags, text in textSpecs: + p.drawText(rect, flags, text) + #p.drawRect(rect) + prof.mark('draw text') prof.finish() diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 3100087a7b..c226b9c43b 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -1079,6 +1079,7 @@ def setLabel(self, axis, text=None, units=None, unitPrefix=None, **args): ============= ================================================================= """ self.getAxis(axis).setLabel(text=text, units=units, **args) + self.showAxis(axis) def setLabels(self, **kwds): """ From ee89b291dcbbcdec9429f64b2de4eeecedcde75b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Mar 2013 12:34:17 -0400 Subject: [PATCH 022/121] Axis line can optionally stop at the last tick --- pyqtgraph/graphicsItems/AxisItem.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index bf3c874371..e31030dfb6 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -383,7 +383,8 @@ def paint(self, p, opt, widget): picture = QtGui.QPicture() painter = QtGui.QPainter(picture) specs = self.generateDrawSpecs(painter) - self.drawPicture(painter, *specs) + if specs is not None: + self.drawPicture(painter, *specs) finally: painter.end() self.picture = picture @@ -646,12 +647,16 @@ def generateDrawSpecs(self, p): ## determine mapping between tick values and local coordinates dif = self.range[1] - self.range[0] - if axis == 0: - xScale = -bounds.height() / dif - offset = self.range[0] * xScale - bounds.height() + if dif == 0: + xscale = 1 + offset = 0 else: - xScale = bounds.width() / dif - offset = self.range[0] * xScale + if axis == 0: + xScale = -bounds.height() / dif + offset = self.range[0] * xScale - bounds.height() + else: + xScale = bounds.width() / dif + offset = self.range[0] * xScale xRange = [x * xScale - offset for x in self.range] xMin = min(xRange) From 5bb5c7487cb536c616cddc60cf5849308e0b4bac Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 30 Mar 2013 22:25:46 -0400 Subject: [PATCH 023/121] Prevent updating ViewBox matrix in setRange when no changes have been made to range --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 338cdde4e1..0a625d48bc 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -87,6 +87,7 @@ def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, self.addedItems = [] #self.gView = view #self.showGrid = showGrid + self.matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred self.state = { @@ -406,8 +407,11 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru self.sigStateChanged.emit(self) - if update: + if update and (any(changed) or self.matrixNeedsUpdate): self.updateMatrix(changed) + + if not update and any(changed): + self.matrixNeedsUpdate = True for ax, range in changes.items(): link = self.linkedView(ax) @@ -1246,6 +1250,7 @@ def updateMatrix(self, changed=None): self.sigRangeChanged.emit(self, self.state['viewRange']) self.sigTransformChanged.emit(self) ## segfaults here: 1 + self.matrixNeedsUpdate = False def paint(self, p, opt, widget): if self.border is not None: From 70ec3589950948f0e2b7f3f5503dfe382092b8b7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 30 Mar 2013 22:26:32 -0400 Subject: [PATCH 024/121] Fix: make HistogramLUTWidget obey default background color --- pyqtgraph/widgets/HistogramLUTWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/HistogramLUTWidget.py b/pyqtgraph/widgets/HistogramLUTWidget.py index bc0415956a..cbe8eb61ba 100644 --- a/pyqtgraph/widgets/HistogramLUTWidget.py +++ b/pyqtgraph/widgets/HistogramLUTWidget.py @@ -13,7 +13,7 @@ class HistogramLUTWidget(GraphicsView): def __init__(self, parent=None, *args, **kargs): - background = kargs.get('background', 'k') + background = kargs.get('background', 'default') GraphicsView.__init__(self, parent, useOpenGL=False, background=background) self.item = HistogramLUTItem(*args, **kargs) self.setCentralItem(self.item) From 09bc17bdb556d240ae458eb25a6d9f19e40f6039 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 30 Mar 2013 22:39:11 -0400 Subject: [PATCH 025/121] Fixed GLLinePlotItem line width option Added antialiasing to GL line items --- examples/GLLinePlotItem.py | 2 +- pyqtgraph/opengl/items/GLAxisItem.py | 10 +++++++--- pyqtgraph/opengl/items/GLGridItem.py | 15 +++++++++------ pyqtgraph/opengl/items/GLLinePlotItem.py | 14 +++++++++++--- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/examples/GLLinePlotItem.py b/examples/GLLinePlotItem.py index ab2fd75b9e..1de07cff5c 100644 --- a/examples/GLLinePlotItem.py +++ b/examples/GLLinePlotItem.py @@ -40,7 +40,7 @@ def fn(x, y): d = (x**2 + yi**2)**0.5 z = 10 * np.cos(d) / (d+1) pts = np.vstack([x,yi,z]).transpose() - plt = gl.GLLinePlotItem(pos=pts, color=pg.glColor((i,n*1.3))) + plt = gl.GLLinePlotItem(pos=pts, color=pg.glColor((i,n*1.3)), width=(i+1)/10., antialias=True) w.addItem(plt) diff --git a/pyqtgraph/opengl/items/GLAxisItem.py b/pyqtgraph/opengl/items/GLAxisItem.py index 1586d70a63..9dbcd443d6 100644 --- a/pyqtgraph/opengl/items/GLAxisItem.py +++ b/pyqtgraph/opengl/items/GLAxisItem.py @@ -12,10 +12,11 @@ class GLAxisItem(GLGraphicsItem): """ - def __init__(self, size=None): + def __init__(self, size=None, antialias=True): GLGraphicsItem.__init__(self) if size is None: size = QtGui.QVector3D(1,1,1) + self.antialias = antialias self.setSize(size=size) def setSize(self, x=None, y=None, z=None, size=None): @@ -39,8 +40,11 @@ def paint(self): glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glEnable( GL_BLEND ) glEnable( GL_ALPHA_TEST ) - glEnable( GL_POINT_SMOOTH ) - #glDisable( GL_DEPTH_TEST ) + + if self.antialias: + glEnable(GL_LINE_SMOOTH) + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + glBegin( GL_LINES ) x,y,z = self.size() diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index 630b2aba67..01a2b178ab 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -11,9 +11,10 @@ class GLGridItem(GLGraphicsItem): Displays a wire-grame grid. """ - def __init__(self, size=None, color=None, glOptions='translucent'): + def __init__(self, size=None, color=None, antialias=True, glOptions='translucent'): GLGraphicsItem.__init__(self) self.setGLOptions(glOptions) + self.antialias = antialias if size is None: size = QtGui.QVector3D(1,1,1) self.setSize(size=size) @@ -36,11 +37,13 @@ def size(self): def paint(self): self.setupGLState() - #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - #glEnable( GL_BLEND ) - #glEnable( GL_ALPHA_TEST ) - glEnable( GL_POINT_SMOOTH ) - #glDisable( GL_DEPTH_TEST ) + + if self.antialias: + glEnable(GL_LINE_SMOOTH) + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + glBegin( GL_LINES ) x,y,z = self.size() diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py index ef747d1711..9ef34cab26 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -32,13 +32,14 @@ def setData(self, **kwds): color tuple of floats (0.0-1.0) specifying a color for the entire item. width float specifying line width + antialias enables smooth line drawing ==================== ================================================== """ - args = ['pos', 'color', 'width', 'connected'] + args = ['pos', 'color', 'width', 'connected', 'antialias'] for k in kwds.keys(): if k not in args: raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args))) - + self.antialias = False for arg in args: if arg in kwds: setattr(self, arg, kwds[arg]) @@ -72,8 +73,15 @@ def paint(self): try: glVertexPointerf(self.pos) glColor4f(*self.color) + glLineWidth(self.width) + #glPointSize(self.width) - glPointSize(self.width) + if self.antialias: + glEnable(GL_LINE_SMOOTH) + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + glDrawArrays(GL_LINE_STRIP, 0, self.pos.size / self.pos.shape[-1]) finally: glDisableClientState(GL_VERTEX_ARRAY) From fde4267ccc420cb3a7f41c7804d3c8812f808e2b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 7 Apr 2013 09:16:21 -0400 Subject: [PATCH 026/121] Corrected use of setGLOptions for image, axis, and box --- examples/GLImageItem.py | 5 ++++- pyqtgraph/opengl/GLGraphicsItem.py | 11 +++++++---- pyqtgraph/opengl/items/GLAxisItem.py | 10 ++++++---- pyqtgraph/opengl/items/GLBoxItem.py | 17 ++++++++++------- pyqtgraph/opengl/items/GLImageItem.py | 15 +++++++++------ 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/examples/GLImageItem.py b/examples/GLImageItem.py index 8b52ac09c9..dfdaad0c22 100644 --- a/examples/GLImageItem.py +++ b/examples/GLImageItem.py @@ -25,11 +25,14 @@ data = ndi.gaussian_filter(np.random.normal(size=shape), (4,4,4)) data += ndi.gaussian_filter(np.random.normal(size=shape), (15,15,15))*15 -## slice out three planes, convert to ARGB for OpenGL texture +## slice out three planes, convert to RGBA for OpenGL texture levels = (-0.08, 0.08) tex1 = pg.makeRGBA(data[shape[0]/2], levels=levels)[0] # yz plane tex2 = pg.makeRGBA(data[:,shape[1]/2], levels=levels)[0] # xz plane tex3 = pg.makeRGBA(data[:,:,shape[2]/2], levels=levels)[0] # xy plane +#tex1[:,:,3] = 128 +#tex2[:,:,3] = 128 +#tex3[:,:,3] = 128 ## Create three image items from textures, add to view v1 = gl.GLImageItem(tex1) diff --git a/pyqtgraph/opengl/GLGraphicsItem.py b/pyqtgraph/opengl/GLGraphicsItem.py index 9babec3a7e..f73b0a7a97 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -116,11 +116,11 @@ def setDepthValue(self, value): Items with negative depth values are drawn before their parent. (This is analogous to QGraphicsItem.zValue) The depthValue does NOT affect the position of the item or the values it imparts to the GL depth buffer. - '""" + """ self.__depthValue = value def depthValue(self): - """Return the depth value of this item. See setDepthValue for mode information.""" + """Return the depth value of this item. See setDepthValue for more information.""" return self.__depthValue def setTransform(self, tr): @@ -134,9 +134,12 @@ def resetTransform(self): def applyTransform(self, tr, local): """ Multiply this object's transform by *tr*. - If local is True, then *tr* is multiplied on the right of the current transform: + If local is True, then *tr* is multiplied on the right of the current transform:: + newTransform = transform * tr - If local is False, then *tr* is instead multiplied on the left: + + If local is False, then *tr* is instead multiplied on the left:: + newTransform = tr * transform """ if local: diff --git a/pyqtgraph/opengl/items/GLAxisItem.py b/pyqtgraph/opengl/items/GLAxisItem.py index 9dbcd443d6..860ac497b1 100644 --- a/pyqtgraph/opengl/items/GLAxisItem.py +++ b/pyqtgraph/opengl/items/GLAxisItem.py @@ -12,12 +12,13 @@ class GLAxisItem(GLGraphicsItem): """ - def __init__(self, size=None, antialias=True): + def __init__(self, size=None, antialias=True, glOptions='translucent'): GLGraphicsItem.__init__(self) if size is None: size = QtGui.QVector3D(1,1,1) self.antialias = antialias self.setSize(size=size) + self.setGLOptions(glOptions) def setSize(self, x=None, y=None, z=None, size=None): """ @@ -37,9 +38,10 @@ def size(self): def paint(self): - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glEnable( GL_BLEND ) - glEnable( GL_ALPHA_TEST ) + #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + #glEnable( GL_BLEND ) + #glEnable( GL_ALPHA_TEST ) + self.setupGLState() if self.antialias: glEnable(GL_LINE_SMOOTH) diff --git a/pyqtgraph/opengl/items/GLBoxItem.py b/pyqtgraph/opengl/items/GLBoxItem.py index af888e9106..bc25afd122 100644 --- a/pyqtgraph/opengl/items/GLBoxItem.py +++ b/pyqtgraph/opengl/items/GLBoxItem.py @@ -11,7 +11,7 @@ class GLBoxItem(GLGraphicsItem): Displays a wire-frame box. """ - def __init__(self, size=None, color=None): + def __init__(self, size=None, color=None, glOptions='translucent'): GLGraphicsItem.__init__(self) if size is None: size = QtGui.QVector3D(1,1,1) @@ -19,6 +19,7 @@ def __init__(self, size=None, color=None): if color is None: color = (255,255,255,80) self.setColor(color) + self.setGLOptions(glOptions) def setSize(self, x=None, y=None, z=None, size=None): """ @@ -43,12 +44,14 @@ def color(self): return self.__color def paint(self): - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glEnable( GL_BLEND ) - glEnable( GL_ALPHA_TEST ) - #glAlphaFunc( GL_ALWAYS,0.5 ) - glEnable( GL_POINT_SMOOTH ) - glDisable( GL_DEPTH_TEST ) + #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + #glEnable( GL_BLEND ) + #glEnable( GL_ALPHA_TEST ) + ##glAlphaFunc( GL_ALWAYS,0.5 ) + #glEnable( GL_POINT_SMOOTH ) + #glDisable( GL_DEPTH_TEST ) + self.setupGLState() + glBegin( GL_LINES ) glColor4f(*self.color().glColor()) diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py index b292a7b70f..aca68f3d7e 100644 --- a/pyqtgraph/opengl/items/GLImageItem.py +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -13,7 +13,7 @@ class GLImageItem(GLGraphicsItem): """ - def __init__(self, data, smooth=False): + def __init__(self, data, smooth=False, glOptions='translucent'): """ ============== ======================================================================================= @@ -27,6 +27,7 @@ def __init__(self, data, smooth=False): self.smooth = smooth self.data = data GLGraphicsItem.__init__(self) + self.setGLOptions(glOptions) def initializeGL(self): glEnable(GL_TEXTURE_2D) @@ -66,11 +67,13 @@ def paint(self): glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.texture) - glEnable(GL_DEPTH_TEST) - #glDisable(GL_CULL_FACE) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glEnable( GL_BLEND ) - glEnable( GL_ALPHA_TEST ) + self.setupGLState() + + #glEnable(GL_DEPTH_TEST) + ##glDisable(GL_CULL_FACE) + #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + #glEnable( GL_BLEND ) + #glEnable( GL_ALPHA_TEST ) glColor4f(1,1,1,1) glBegin(GL_QUADS) From daaf48183050f80d503fc6d6a898399d74cbcdf3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 7 Apr 2013 09:30:49 -0400 Subject: [PATCH 027/121] fixed glGraphicsItem documentation --- doc/source/3dgraphics/glgraphicsitem.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/3dgraphics/glgraphicsitem.rst b/doc/source/3dgraphics/glgraphicsitem.rst index 4ff3d1752d..eac70f5171 100644 --- a/doc/source/3dgraphics/glgraphicsitem.rst +++ b/doc/source/3dgraphics/glgraphicsitem.rst @@ -1,8 +1,8 @@ GLGraphicsItem ============== -.. autoclass:: pyqtgraph.opengl.GLGraphicsItem +.. autoclass:: pyqtgraph.opengl.GLGraphicsItem.GLGraphicsItem :members: - .. automethod:: pyqtgraph.GLGraphicsItem.__init__ + .. automethod:: pyqtgraph.opengl.GLGraphicsItem.GLGraphicsItem.__init__ From 1a0b5921dfda59ee227856e3e49996fbde14d0da Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 7 Apr 2013 16:18:58 -0400 Subject: [PATCH 028/121] remotegraphicsview fix for PyQt 4.10 --- examples/RemoteGraphicsView.py | 12 +++++++++--- pyqtgraph/widgets/RemoteGraphicsView.py | 8 +++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/RemoteGraphicsView.py b/examples/RemoteGraphicsView.py index 5b4e7ef4e7..a5d869c94d 100644 --- a/examples/RemoteGraphicsView.py +++ b/examples/RemoteGraphicsView.py @@ -1,20 +1,26 @@ # -*- coding: utf-8 -*- """ -Very simple example demonstrating RemoteGraphicsView +Very simple example demonstrating RemoteGraphicsView. + +This allows graphics to be rendered in a child process and displayed in the +parent, which can improve CPU usage on multi-core processors. """ import initExample ## Add path to library (just for examples; you do not need this) + from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg from pyqtgraph.widgets.RemoteGraphicsView import RemoteGraphicsView app = pg.mkQApp() -v = RemoteGraphicsView() +## Create the widget +v = RemoteGraphicsView(debug=False) v.show() v.setWindowTitle('pyqtgraph example: RemoteGraphicsView') ## v.pg is a proxy to the remote process' pyqtgraph module. All attribute ## requests and function calls made with this object are forwarded to the -## remote process and executed there. +## remote process and executed there. See pyqtgraph.multiprocess.remoteproxy +## for more inormation. plt = v.pg.PlotItem() v.setCentralItem(plt) plt.plot([1,4,2,3,6,2,3,4,2,3], pen='g') diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index cb36ba6214..d1a21e9727 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -1,4 +1,6 @@ from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +if not USE_PYSIDE: + import sip import pyqtgraph.multiprocess as mp import pyqtgraph as pg from .GraphicsView import GraphicsView @@ -21,7 +23,7 @@ def __init__(self, parent=None, *args, **kwds): 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(debug=False) + self._proc = mp.QtProcess(debug=kwds.pop('debug', False)) self.pg = self._proc._import('pyqtgraph') self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS) rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') @@ -174,7 +176,6 @@ def renderView(self): 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 if USE_PYSIDE: @@ -182,7 +183,8 @@ def renderView(self): #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) + address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0)) + self.img = QtGui.QImage(sip.voidptr(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 e0e1123d338984b98c7a18b769b4fd26adc7b031 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 29 Apr 2013 08:13:28 -0400 Subject: [PATCH 029/121] fixed import statements python3 compatibility PolyLineROI.getArrayRegion correctly applies mask to N-dimensional data fixed multiprocess for python2.6 compatibility --- examples/__init__.py | 2 +- examples/__main__.py | 12 +++++++++--- pyqtgraph/__init__.py | 3 ++- pyqtgraph/graphicsItems/ROI.py | 2 +- pyqtgraph/multiprocess/parallelizer.py | 2 +- pyqtgraph/multiprocess/remoteproxy.py | 2 +- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/examples/__init__.py b/examples/__init__.py index 23b7cd58ff..76a71e14c4 100644 --- a/examples/__init__.py +++ b/examples/__init__.py @@ -1 +1 @@ -from __main__ import run +from .__main__ import run diff --git a/examples/__main__.py b/examples/__main__.py index c46d7065c4..e7b89716b2 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -1,12 +1,18 @@ import sys, os, subprocess, time -import initExample +try: + from . import initExample +except ValueError: + sys.excepthook(*sys.exc_info()) + print("examples/ can not be executed as a script; please run 'python -m examples' instead.") + sys.exit(1) + from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE if USE_PYSIDE: - from exampleLoaderTemplate_pyside import Ui_Form + from .exampleLoaderTemplate_pyside import Ui_Form else: - from exampleLoaderTemplate_pyqt import Ui_Form + from .exampleLoaderTemplate_pyqt import Ui_Form import os, sys from pyqtgraph.pgcollections import OrderedDict diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 67eb712e54..d83e0ec001 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -154,7 +154,8 @@ def importModules(path, globals, locals, excludes=()): try: if len(path) > 0: modName = path + '.' + modName - mod = __import__(modName, globals, locals, fromlist=['*']) + #mod = __import__(modName, globals, locals, fromlist=['*']) + mod = __import__(modName, globals, locals, ['*'], 1) mods[modName] = mod except: import traceback diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 9cdc8c2978..97669fe015 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1771,7 +1771,7 @@ def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds shape = [1] * data.ndim shape[axes[0]] = sliced.shape[axes[0]] shape[axes[1]] = sliced.shape[axes[1]] - return sliced * mask + return sliced * mask.reshape(shape) class LineSegmentROI(ROI): diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index 9925a57317..e96692e2e7 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -129,7 +129,7 @@ def runParallel(self): self.childs.append(proc) ## Keep track of the progress of each worker independently. - self.progress = {ch.childPid: [] for ch in self.childs} + self.progress = dict([(ch.childPid, []) for ch in self.childs]) ## for each child process, self.progress[pid] is a list ## of task indexes. The last index is the task currently being ## processed; all others are finished. diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 6cd65f6ee7..974e1e95c4 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -803,7 +803,7 @@ def _getProxyOption(self, opt): return val def _getProxyOptions(self): - return {k: self._getProxyOption(k) for k in self._proxyOptions} + return dict([(k, self._getProxyOption(k)) for k in self._proxyOptions]) def __reduce__(self): return (unpickleObjectProxy, (self._processId, self._proxyId, self._typeStr, self._attributes)) From 00e865f56c008d33e831c24b6417d971c56c6559 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 5 May 2013 10:54:47 -0400 Subject: [PATCH 030/121] minor fix in AxisItem --- pyqtgraph/graphicsItems/AxisItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index e31030dfb6..d8c49390ec 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -731,6 +731,7 @@ def generateDrawSpecs(self, p): textRects = [] textSpecs = [] ## list of draw + textSize2 = 0 for i in range(len(tickLevels)): ## Get the list of strings to display for this level if tickStrings is None: From 671e624f177f12f43da0971098b53c7f48bb6592 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 9 May 2013 23:02:14 -0400 Subject: [PATCH 031/121] Fixes: AxisItem correctly handles scaling with values that are not power of 10 Can remove items from legend updated plotItem setLogMode to allow unspecified axes --- examples/__main__.py | 1 + pyqtgraph/graphicsItems/AxisItem.py | 20 +++++++++++-- pyqtgraph/graphicsItems/LegendItem.py | 30 ++++++++++++++++++++ pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 12 ++++---- 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index e7b89716b2..2ecc810df0 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -3,6 +3,7 @@ try: from . import initExample except ValueError: + #__package__ = os.path.split(os.path.dirname(__file__))[-1] sys.excepthook(*sys.exc_info()) print("examples/ can not be executed as a script; please run 'python -m examples' instead.") sys.exit(1) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index d8c49390ec..846f48ac21 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -281,7 +281,7 @@ def setPen(self, pen): def setScale(self, scale=None): """ Set the value scaling for this axis. Values on the axis are multiplied - by this scale factor before being displayed as text. By default, + by this scale factor before being displayed as text. By default (scale=None), this scaling value is automatically determined based on the visible range and the axis units are updated to reflect the chosen scale factor. @@ -301,6 +301,7 @@ def setScale(self, scale=None): self.setLabel(unitPrefix=prefix) else: scale = 1.0 + self.autoScale = True else: self.setLabel(unitPrefix='') self.autoScale = False @@ -499,6 +500,10 @@ def tickValues(self, minVal, maxVal, size): """ minVal, maxVal = sorted((minVal, maxVal)) + + minVal *= self.scale + maxVal *= self.scale + #size *= self.scale ticks = [] tickLevels = self.tickSpacing(minVal, maxVal, size) @@ -511,16 +516,25 @@ def tickValues(self, minVal, maxVal, size): ## determine number of ticks num = int((maxVal-start) / spacing) + 1 - values = np.arange(num) * spacing + start + values = (np.arange(num) * spacing + start) / self.scale ## remove any ticks that were present in higher levels ## we assume here that if the difference between a tick value and a previously seen tick value ## is less than spacing/100, then they are 'equal' and we can ignore the new tick. values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) ) allValues = np.concatenate([allValues, values]) - ticks.append((spacing, values)) + ticks.append((spacing/self.scale, values)) if self.logMode: return self.logTickValues(minVal, maxVal, size, ticks) + + + #nticks = [] + #for t in ticks: + #nvals = [] + #for v in t[1]: + #nvals.append(v/self.scale) + #nticks.append((t[0]/self.scale,nvals)) + #ticks = nticks return ticks diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index c41feb9537..3f4d5fa1b7 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -73,6 +73,36 @@ def addItem(self, item, name): self.layout.addItem(sample, row, 0) self.layout.addItem(label, row, 1) self.updateSize() + + # + # + # Ulrich + def removeItem(self, name): + """ + Removes one item from the legend. + + =========== ======================================================== + Arguments + title The title displayed for this item. + =========== ======================================================== + """ + # cycle for a match + for sample, label in self.items: + print label.text, name + if label.text == name: # hit + self.items.remove( (sample, label) ) # remove from itemlist + self.layout.removeItem(sample) # remove from layout + sample.close() # remove from drawing + self.layout.removeItem(label) + label.close() + self.updateSize() # redraq box + + # hcirlU + # + # + + + def updateSize(self): if self.size is not None: diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index c226b9c43b..52a1429b52 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -295,19 +295,21 @@ def getViewBox(self): - def setLogMode(self, x, y): + def setLogMode(self, x=None, y=None): """ - Set log scaling for x and y axes. + Set log scaling for x and/or y axes. This informs PlotDataItems to transform logarithmically and switches the axes to use log ticking. Note that *no other items* in the scene will be affected by - this; there is no generic way to redisplay a GraphicsItem + this; there is (currently) no generic way to redisplay a GraphicsItem with log coordinates. """ - self.ctrl.logXCheck.setChecked(x) - self.ctrl.logYCheck.setChecked(y) + if x is not None: + self.ctrl.logXCheck.setChecked(x) + if y is not None: + self.ctrl.logYCheck.setChecked(y) def showGrid(self, x=None, y=None, alpha=None): """ From 09b16baed13b41e71e94f25d2c24dae04bc491f1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 13 May 2013 08:51:59 -0400 Subject: [PATCH 032/121] python3 fixes imageview fix --- pyqtgraph/imageview/ImageView.py | 7 ++++--- pyqtgraph/parametertree/ParameterItem.py | 6 ++++++ pyqtgraph/widgets/SpinBox.py | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index f0c13a6098..cb72241abb 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -209,7 +209,7 @@ def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, *pos* Change the position of the displayed image *scale* Change the scale of the displayed image - *transform* Set the transform of the dispalyed image. This option overrides *pos* + *transform* Set the transform of the displayed image. This option overrides *pos* and *scale*. ============== ======================================================================= """ @@ -271,8 +271,9 @@ def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, if levels is None and autoLevels: self.autoLevels() if levels is not None: ## this does nothing since getProcessedImage sets these values again. - self.levelMax = levels[1] - self.levelMin = levels[0] + #self.levelMax = levels[1] + #self.levelMin = levels[0] + self.setLevels(*levels) if self.ui.roiBtn.isChecked(): self.roiChanged() diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index 376e900d5a..46499fd365 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -157,3 +157,9 @@ def requestRemove(self): ## since destroying the menu in mid-action will cause a crash. QtCore.QTimer.singleShot(0, self.param.remove) + ## for python 3 support, we need to redefine hash and eq methods. + def __hash__(self): + return id(self) + + def __eq__(self, x): + return x is self diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 71695f4a64..57e4f1ede1 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -313,7 +313,7 @@ def stepBy(self, n): s = [D(-1), D(1)][n >= 0] ## determine sign of step val = self.val - for i in range(abs(n)): + for i in range(int(abs(n))): if self.opts['log']: raise Exception("Log mode no longer supported.") From 720c5c0242e3b769674c6804f4a8a64bd932f1f8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 13 May 2013 14:46:53 -0400 Subject: [PATCH 033/121] Fixed handling of non-native dtypes when optimizing with weave --- pyqtgraph/functions.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 5f820a9a38..6c52e7752f 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -619,7 +619,15 @@ def rescaleData(data, scale, offset, dtype=None): if not USE_WEAVE: raise Exception('Weave is disabled; falling back to slower version.') - newData = np.empty((data.size,), dtype=dtype) + ## require native dtype when using weave + if not data.dtype.isnative(): + data = data.astype(data.dtype.newbyteorder('=')) + if not dtype.isnative(): + weaveDtype = dtype.newbyteorder('=') + else: + weaveDtype = dtype + + newData = np.empty((data.size,), dtype=weaveDtype) flat = np.ascontiguousarray(data).reshape(data.size) size = data.size @@ -631,6 +639,8 @@ def rescaleData(data, scale, offset, dtype=None): } """ scipy.weave.inline(code, ['flat', 'newData', 'size', 'offset', 'scale'], compiler='gcc') + if dtype != weaveDtype: + newData = newData.astype(dtype) data = newData.reshape(data.shape) except: if USE_WEAVE: @@ -839,7 +849,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): if minVal == maxVal: maxVal += 1e-16 data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int) - prof.mark('2') @@ -849,7 +858,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): else: if data.dtype is not np.ubyte: data = np.clip(data, 0, 255).astype(np.ubyte) - prof.mark('3') From a55d58024d4b49c8787dcf5c8e3d6b4f2c02cae2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 22 May 2013 14:09:56 -0400 Subject: [PATCH 034/121] Added Dock.close() Fixed bugs in functions weave usage Documented ROI signals Fixed 3D view updating after every scene change --- pyqtgraph/dockarea/Dock.py | 7 +++++++ pyqtgraph/functions.py | 31 +++++++++++++++--------------- pyqtgraph/graphicsItems/ROI.py | 15 +++++++++++++++ pyqtgraph/opengl/GLGraphicsItem.py | 2 +- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 19ebc76ebc..414980ac71 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -209,6 +209,13 @@ def containerChanged(self, c): self.setOrientation(force=True) + def close(self): + """Remove this dock from the DockArea it lives inside.""" + self.setParent(None) + self.label.setParent(None) + self._container.apoptose() + self._container = None + def __repr__(self): return "" % (self.name(), self.stretch()) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 6c52e7752f..836ae43356 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -23,7 +23,7 @@ from .Qt import QtGui, QtCore, USE_PYSIDE -from pyqtgraph import getConfigOption +import pyqtgraph as pg import numpy as np import decimal, re import ctypes @@ -32,12 +32,12 @@ try: import scipy.ndimage HAVE_SCIPY = True - WEAVE_DEBUG = getConfigOption('weaveDebug') - try: - import scipy.weave - USE_WEAVE = getConfigOption('useWeave') - except: - USE_WEAVE = False + WEAVE_DEBUG = pg.getConfigOption('weaveDebug') + if pg.getConfigOption('useWeave'): + try: + import scipy.weave + except ImportError: + pg.setConfigOptions(useWeave=False) except ImportError: HAVE_SCIPY = False @@ -611,18 +611,19 @@ def rescaleData(data, scale, offset, dtype=None): Uses scipy.weave (if available) to improve performance. """ - global USE_WEAVE if dtype is None: dtype = data.dtype + else: + dtype = np.dtype(dtype) try: - if not USE_WEAVE: + if not pg.getConfigOption('useWeave'): raise Exception('Weave is disabled; falling back to slower version.') ## require native dtype when using weave - if not data.dtype.isnative(): + if not data.dtype.isnative: data = data.astype(data.dtype.newbyteorder('=')) - if not dtype.isnative(): + if not dtype.isnative: weaveDtype = dtype.newbyteorder('=') else: weaveDtype = dtype @@ -643,10 +644,10 @@ def rescaleData(data, scale, offset, dtype=None): newData = newData.astype(dtype) data = newData.reshape(data.shape) except: - if USE_WEAVE: - if WEAVE_DEBUG: + if pg.getConfigOption('useWeave'): + if pg.getConfigOption('weaveDebug'): debug.printExc("Error; disabling weave.") - USE_WEAVE = False + pg.setConfigOption('useWeave', False) #p = np.poly1d([scale, -offset*scale]) #data = p(data).astype(dtype) @@ -663,8 +664,6 @@ def applyLookupTable(data, lut): Uses scipy.weave to improve performance if it is available. Note: color gradient lookup tables can be generated using GradientWidget. """ - global USE_WEAVE - if data.dtype.kind not in ('i', 'u'): data = data.astype(int) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 97669fe015..bdfc850822 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -38,6 +38,21 @@ def rectStr(r): class ROI(GraphicsObject): """Generic region-of-interest widget. Can be used for implementing many types of selection box with rotate/translate/scale handles. + + Signals + ----------------------- ---------------------------------------------------- + sigRegionChangeFinished Emitted when the user stops dragging the ROI (or + one of its handles) or if the ROI is changed + programatically. + sigRegionChangeStarted Emitted when the user starts dragging the ROI (or + one of its handles). + sigRegionChanged Emitted any time the position of the ROI changes, + including while it is being dragged by the user. + sigHoverEvent Emitted when the mouse hovers over the ROI. + sigClicked Emitted when the user clicks on the ROI + sigRemoveRequested Emitted when the user selects 'remove' from the + ROI's context menu (if available). + ----------------------- ---------------------------------------------------- """ sigRegionChangeFinished = QtCore.Signal(object) diff --git a/pyqtgraph/opengl/GLGraphicsItem.py b/pyqtgraph/opengl/GLGraphicsItem.py index f73b0a7a97..59bc4449b2 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -240,7 +240,7 @@ def update(self): v = self.view() if v is None: return - v.updateGL() + v.update() def mapToParent(self, point): tr = self.transform() From 91ac29bf23dbde2b00a941676afad559786de737 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 22 May 2013 14:27:19 -0400 Subject: [PATCH 035/121] Added basic symbol support to LegendItem --- pyqtgraph/graphicsItems/LegendItem.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 3f4d5fa1b7..e2484ecfd1 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -4,6 +4,7 @@ from .. import functions as fn from ..Point import Point from .GraphicsWidgetAnchor import GraphicsWidgetAnchor +import pyqtgraph as pg __all__ = ['LegendItem'] class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): @@ -136,6 +137,7 @@ def boundingRect(self): return QtCore.QRectF(0, 0, 20, 20) def paint(self, p, *args): + #p.setRenderHint(p.Antialiasing) # only if the data is antialiased. opts = self.item.opts if opts.get('fillLevel',None) is not None and opts.get('fillBrush',None) is not None: @@ -146,6 +148,13 @@ def paint(self, p, *args): p.setPen(fn.mkPen(opts['pen'])) p.drawLine(2, 18, 18, 2) + symbol = opts.get('symbol', None) + if symbol is not None: + p.translate(10,10) + pen = pg.mkPen(opts['symbolPen']) + brush = pg.mkBrush(opts['symbolBrush']) + path = pg.graphicsItems.ScatterPlotItem.drawSymbol(p, symbol, opts['symbolSize'], pen, brush) + From ee0825d677977cfdee39729fec9112bc1fe92b7b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 22 May 2013 14:35:14 -0400 Subject: [PATCH 036/121] Allow custom ItemSamples in LegendItem. --- pyqtgraph/graphicsItems/LegendItem.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index e2484ecfd1..a2fc0e049b 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -63,21 +63,23 @@ def addItem(self, item, name): =========== ======================================================== Arguments item A PlotDataItem from which the line and point style - of the item will be determined + of the item will be determined or an instance of + ItemSample (or a subclass), allowing the item display + to be customized. title The title to display for this item. Simple HTML allowed. =========== ======================================================== """ label = LabelItem(name) - sample = ItemSample(item) + if isinstance(item, ItemSample): + sample = item + else: + sample = ItemSample(item) row = len(self.items) self.items.append((sample, label)) self.layout.addItem(sample, row, 0) self.layout.addItem(label, row, 1) self.updateSize() - # - # - # Ulrich def removeItem(self, name): """ Removes one item from the legend. @@ -87,6 +89,7 @@ def removeItem(self, name): title The title displayed for this item. =========== ======================================================== """ + # Thanks, Ulrich! # cycle for a match for sample, label in self.items: print label.text, name @@ -98,12 +101,6 @@ def removeItem(self, name): label.close() self.updateSize() # redraq box - # hcirlU - # - # - - - def updateSize(self): if self.size is not None: From 7a7288b6b3af8ea9511a57d7e5ccbb4d17106b9a Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 28 May 2013 15:31:10 -0400 Subject: [PATCH 037/121] Fixed documentation for 'uver/under' in DockArea Configure matplotlib to use PySide in MatplotlibWidget --- README.txt | 8 +++++++- pyqtgraph/dockarea/DockArea.py | 6 +++--- pyqtgraph/widgets/MatplotlibWidget.py | 6 +++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.txt b/README.txt index b51b9aa313..d209ef0100 100644 --- a/README.txt +++ b/README.txt @@ -2,10 +2,16 @@ PyQtGraph - A pure-Python graphics library for PyQt/PySide Copyright 2012 Luke Campagnola, University of North Carolina at Chapel Hill http://www.pyqtgraph.org -Authors: +Maintainer: Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') + +Contributors: Megan Kratz + Paul Manis Ingo Breßler + Christian Gavin + Michael Cristopher Hogg + Ulrich Leutner Requirements: PyQt 4.7+ or PySide diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index 752cf3b6f2..882b29a3ff 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -40,11 +40,11 @@ def addDock(self, dock=None, position='bottom', relativeTo=None, **kwds): Arguments: dock The new Dock object to add. If None, then a new Dock will be created. - position 'bottom', 'top', 'left', 'right', 'over', or 'under' + position 'bottom', 'top', 'left', 'right', 'above', or 'below' relativeTo If relativeTo is None, then the new Dock is added to fill an entire edge of the window. If relativeTo is another Dock, then the new Dock is placed adjacent to it (or in a tabbed - configuration for 'over' and 'under'). + configuration for 'above' and 'below'). =========== ================================================================= All extra keyword arguments are passed to Dock.__init__() if *dock* is @@ -316,4 +316,4 @@ def dropEvent(self, *args): DockDrop.dropEvent(self, *args) - \ No newline at end of file + diff --git a/pyqtgraph/widgets/MatplotlibWidget.py b/pyqtgraph/widgets/MatplotlibWidget.py index 25e058f96c..6a22c973ab 100644 --- a/pyqtgraph/widgets/MatplotlibWidget.py +++ b/pyqtgraph/widgets/MatplotlibWidget.py @@ -1,5 +1,9 @@ -from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE import matplotlib + +if USE_PYSIDE: + matplotlib.rcParams['backend.qt4']='PySide' + from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar from matplotlib.figure import Figure From ba31b3d7ba22b498807845ff14668afa450de7be Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 28 May 2013 18:47:33 -0400 Subject: [PATCH 038/121] Legends can be dragged by user --- .../graphicsItems/GraphicsWidgetAnchor.py | 45 +++++++++++++++++++ pyqtgraph/graphicsItems/LegendItem.py | 12 +++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py b/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py index 3174e6e042..251bc0c8ee 100644 --- a/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py +++ b/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py @@ -47,7 +47,52 @@ def anchor(self, itemPos, parentPos, offset=(0,0)): self.__parentAnchor = parentPos self.__offset = offset self.__geometryChanged() + + + def autoAnchor(self, pos, relative=True): + """ + Set the position of this item relative to its parent by automatically + choosing appropriate anchor settings. + + If relative is True, one corner of the item will be anchored to + the appropriate location on the parent with no offset. The anchored + corner will be whichever is closest to the parent's boundary. + + If relative is False, one corner of the item will be anchored to the same + corner of the parent, with an absolute offset to achieve the correct + position. + """ + pos = Point(pos) + br = self.mapRectToParent(self.boundingRect()).translated(pos - self.pos()) + pbr = self.parentItem().boundingRect() + anchorPos = [0,0] + parentPos = Point() + itemPos = Point() + if abs(br.left() - pbr.left()) < abs(br.right() - pbr.right()): + anchorPos[0] = 0 + parentPos[0] = pbr.left() + itemPos[0] = br.left() + else: + anchorPos[0] = 1 + parentPos[0] = pbr.right() + itemPos[0] = br.right() + + if abs(br.top() - pbr.top()) < abs(br.bottom() - pbr.bottom()): + anchorPos[1] = 0 + parentPos[1] = pbr.top() + itemPos[1] = br.top() + else: + anchorPos[1] = 1 + parentPos[1] = pbr.bottom() + itemPos[1] = br.bottom() + if relative: + relPos = [(itemPos[0]-pbr.left()) / pbr.width(), (itemPos[1]-pbr.top()) / pbr.height()] + self.anchor(anchorPos, relPos) + else: + offset = itemPos - parentPos + self.anchor(anchorPos, anchorPos, offset) + def __geometryChanged(self): if self.__parent is None: return diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index a2fc0e049b..6c42fb4c4b 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -101,7 +101,6 @@ def removeItem(self, name): label.close() self.updateSize() # redraq box - def updateSize(self): if self.size is not None: return @@ -115,15 +114,22 @@ def updateSize(self): #print(width, height) #print width, height self.setGeometry(0, 0, width+25, height) - + def boundingRect(self): return QtCore.QRectF(0, 0, self.width(), self.height()) - + def paint(self, p, *args): p.setPen(fn.mkPen(255,255,255,100)) p.setBrush(fn.mkBrush(100,100,100,50)) p.drawRect(self.boundingRect()) + + def hoverEvent(self, ev): + ev.acceptDrags(QtCore.Qt.LeftButton) + def mouseDragEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton: + dpos = ev.pos() - ev.lastPos() + self.autoAnchor(self.pos() + dpos) class ItemSample(GraphicsWidget): def __init__(self, item): From 96a5f9290d88f9e84ac5745050cdbfdcafe2c9a6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 29 May 2013 08:16:34 -0400 Subject: [PATCH 039/121] Fixed ItemSample handling of ScatterPlotItem --- pyqtgraph/graphicsItems/LegendItem.py | 21 ++++++++++++++++----- pyqtgraph/opengl/GLViewWidget.py | 1 + 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index a2fc0e049b..1fc886629f 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -126,6 +126,11 @@ def paint(self, p, *args): class ItemSample(GraphicsWidget): + """ Class responsible for drawing a single item in a LegendItem (sans label). + + This may be subclassed to draw custom graphics in a Legend. + """ + ## Todo: make this more generic; let each item decide how it should be represented. def __init__(self, item): GraphicsWidget.__init__(self) self.item = item @@ -142,15 +147,21 @@ def paint(self, p, *args): p.setPen(fn.mkPen(None)) p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)])) - p.setPen(fn.mkPen(opts['pen'])) - p.drawLine(2, 18, 18, 2) + if not isinstance(self.item, pg.ScatterPlotItem): + p.setPen(fn.mkPen(opts['pen'])) + p.drawLine(2, 18, 18, 2) symbol = opts.get('symbol', None) if symbol is not None: + if isinstance(self.item, pg.PlotDataItem): + opts = self.item.scatter.opts + + pen = pg.mkPen(opts['pen']) + brush = pg.mkBrush(opts['brush']) + size = opts['size'] + p.translate(10,10) - pen = pg.mkPen(opts['symbolPen']) - brush = pg.mkBrush(opts['symbolBrush']) - path = pg.graphicsItems.ScatterPlotItem.drawSymbol(p, symbol, opts['symbolSize'], pen, brush) + path = pg.graphicsItems.ScatterPlotItem.drawSymbol(p, symbol, size, pen, brush) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index d1c1d090ef..40bd853e38 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -168,6 +168,7 @@ def cameraPosition(self): def orbit(self, azim, elev): """Orbits the camera around the center position. *azim* and *elev* are given in degrees.""" self.opts['azimuth'] += azim + #self.opts['elevation'] += elev self.opts['elevation'] = np.clip(self.opts['elevation'] + elev, -90, 90) self.update() From ba56899a365949d77d28327468628b1aa4dacb23 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 29 May 2013 14:33:14 -0400 Subject: [PATCH 040/121] Added basic wireframe mesh drawing --- examples/GLMeshItem.py | 10 ++++ pyqtgraph/opengl/MeshData.py | 28 ++++++++- pyqtgraph/opengl/items/GLMeshItem.py | 85 ++++++++++++++++++++-------- 3 files changed, 98 insertions(+), 25 deletions(-) diff --git a/examples/GLMeshItem.py b/examples/GLMeshItem.py index 9056fbd615..5ef8eb51aa 100644 --- a/examples/GLMeshItem.py +++ b/examples/GLMeshItem.py @@ -83,6 +83,16 @@ w.addItem(m3) +# Example 4: +# wireframe + +md = gl.MeshData.sphere(rows=4, cols=8) +m4 = gl.GLMeshItem(meshdata=md, smooth=False, drawFaces=False, drawEdges=True, edgeColor=(1,1,1,1)) +m4.translate(0,10,0) +w.addItem(m4) + + + diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index 170074b9c8..12a9b83b5e 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -44,7 +44,7 @@ def __init__(self, vertexes=None, faces=None, edges=None, vertexColors=None, fac ## mappings between vertexes, faces, and edges self._faces = None # Nx3 array of indexes into self._vertexes specifying three vertexes for each face - self._edges = None + self._edges = None # Nx2 array of indexes into self._vertexes specifying two vertexes per edge self._vertexFaces = None ## maps vertex ID to a list of face IDs (inverse mapping of _faces) self._vertexEdges = None ## maps vertex ID to a list of edge IDs (inverse mapping of _edges) @@ -143,12 +143,19 @@ def __init__(self, vertexes=None, faces=None, edges=None, vertexColors=None, fac def faces(self): """Return an array (Nf, 3) of vertex indexes, three per triangular face in the mesh.""" return self._faces + + def edges(self): + """Return an array (Nf, 3) of vertex indexes, two per edge in the mesh.""" + if self._edges is None: + self._computeEdges() + return self._edges def setFaces(self, faces): """Set the (Nf, 3) array of faces. Each rown in the array contains three indexes into the vertex array, specifying the three corners of a triangular face.""" self._faces = faces + self._edges = None self._vertexFaces = None self._vertexesIndexedByFaces = None self.resetNormals() @@ -418,6 +425,25 @@ def vertexFaces(self): #""" #pass + def _computeEdges(self): + ## generate self._edges from self._faces + #print self._faces + nf = len(self._faces) + edges = np.empty(nf*3, dtype=[('i', np.uint, 2)]) + edges['i'][0:nf] = self._faces[:,:2] + edges['i'][nf:2*nf] = self._faces[:,1:3] + edges['i'][-nf:,0] = self._faces[:,2] + edges['i'][-nf:,1] = self._faces[:,0] + + # sort per-edge + mask = edges['i'][:,0] > edges['i'][:,1] + edges['i'][mask] = edges['i'][mask][:,::-1] + + # remove duplicate entries + self._edges = np.unique(edges)['i'] + #print self._edges + + def save(self): """Serialize this mesh to a string appropriate for disk storage""" import pickle diff --git a/pyqtgraph/opengl/items/GLMeshItem.py b/pyqtgraph/opengl/items/GLMeshItem.py index 4222c96b2c..66d5436141 100644 --- a/pyqtgraph/opengl/items/GLMeshItem.py +++ b/pyqtgraph/opengl/items/GLMeshItem.py @@ -22,9 +22,15 @@ def __init__(self, **kwds): Arguments meshdata MeshData object from which to determine geometry for this item. - color Default color used if no vertex or face colors are - specified. - shader Name of shader program to use (None for no shader) + color Default face color used if no vertex or face colors + are specified. + edgeColor Default edge color to use if no edge colors are + specified in the mesh data. + drawEdges If True, a wireframe mesh will be drawn. + (default=False) + drawFaces If True, mesh faces are drawn. (default=True) + shader Name of shader program to use when drawing faces. + (None for no shader) smooth If True, normal vectors are computed for each vertex and interpolated within each face. computeNormals If False, then computation of normal vectors is @@ -35,6 +41,9 @@ def __init__(self, **kwds): self.opts = { 'meshdata': None, 'color': (1., 1., 1., 1.), + 'drawEdges': False, + 'drawFaces': True, + 'edgeColor': (0.5, 0.5, 0.5, 1.0), 'shader': None, 'smooth': True, 'computeNormals': True, @@ -100,6 +109,8 @@ def meshDataChanged(self): self.faces = None self.normals = None self.colors = None + self.edges = None + self.edgeColors = None self.update() def parseMeshData(self): @@ -137,6 +148,9 @@ def parseMeshData(self): elif md.hasFaceColor(): self.colors = md.faceColors(indexed='faces') + if self.opts['drawEdges']: + self.edges = md.edges() + self.edgeVerts = md.vertexes() return def paint(self): @@ -144,19 +158,52 @@ def paint(self): self.parseMeshData() - with self.shader(): - verts = self.vertexes - norms = self.normals - color = self.colors - faces = self.faces - if verts is None: - return + if self.opts['drawFaces']: + with self.shader(): + verts = self.vertexes + norms = self.normals + color = self.colors + faces = self.faces + if verts is None: + return + glEnableClientState(GL_VERTEX_ARRAY) + try: + glVertexPointerf(verts) + + if self.colors is None: + color = self.opts['color'] + if isinstance(color, QtGui.QColor): + glColor4f(*pg.glColor(color)) + else: + glColor4f(*color) + else: + glEnableClientState(GL_COLOR_ARRAY) + glColorPointerf(color) + + + if norms is not None: + glEnableClientState(GL_NORMAL_ARRAY) + glNormalPointerf(norms) + + if faces is None: + glDrawArrays(GL_TRIANGLES, 0, np.product(verts.shape[:-1])) + else: + faces = faces.astype(np.uint).flatten() + glDrawElements(GL_TRIANGLES, faces.shape[0], GL_UNSIGNED_INT, faces) + finally: + glDisableClientState(GL_NORMAL_ARRAY) + glDisableClientState(GL_VERTEX_ARRAY) + glDisableClientState(GL_COLOR_ARRAY) + + if self.opts['drawEdges']: + verts = self.edgeVerts + edges = self.edges glEnableClientState(GL_VERTEX_ARRAY) try: glVertexPointerf(verts) - if self.colors is None: - color = self.opts['color'] + if self.edgeColors is None: + color = self.opts['edgeColor'] if isinstance(color, QtGui.QColor): glColor4f(*pg.glColor(color)) else: @@ -164,19 +211,9 @@ def paint(self): else: glEnableClientState(GL_COLOR_ARRAY) glColorPointerf(color) - - - if norms is not None: - glEnableClientState(GL_NORMAL_ARRAY) - glNormalPointerf(norms) - - if faces is None: - glDrawArrays(GL_TRIANGLES, 0, np.product(verts.shape[:-1])) - else: - faces = faces.astype(np.uint).flatten() - glDrawElements(GL_TRIANGLES, faces.shape[0], GL_UNSIGNED_INT, faces) + edges = edges.flatten() + glDrawElements(GL_LINES, edges.shape[0], GL_UNSIGNED_INT, edges) finally: - glDisableClientState(GL_NORMAL_ARRAY) glDisableClientState(GL_VERTEX_ARRAY) glDisableClientState(GL_COLOR_ARRAY) From 59bbe0127e95bc757972454615003f1beb79750b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 30 May 2013 09:33:09 -0400 Subject: [PATCH 041/121] ImageView cleanups - fixed auto-levelling when normalization options change - added autoHistogramRange argument to setImage --- pyqtgraph/imageview/ImageView.py | 127 ++++++++----------------------- 1 file changed, 33 insertions(+), 94 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index cb72241abb..77f344196a 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -90,14 +90,6 @@ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *ar self.ignoreTimeLine = False - #if 'linux' in sys.platform.lower(): ## Stupid GL bug in linux. - # self.ui.graphicsView.setViewport(QtGui.QWidget()) - - #self.ui.graphicsView.enableMouse(True) - #self.ui.graphicsView.autoPixelRange = False - #self.ui.graphicsView.setAspectLocked(True) - #self.ui.graphicsView.invertY() - #self.ui.graphicsView.enableMouse() if view is None: self.view = ViewBox() else: @@ -106,13 +98,6 @@ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *ar self.view.setAspectLocked(True) self.view.invertY() - #self.ticks = [t[0] for t in self.ui.gradientWidget.listTicks()] - #self.ticks[0].colorChangeAllowed = False - #self.ticks[1].colorChangeAllowed = False - #self.ui.gradientWidget.allowAdd = False - #self.ui.gradientWidget.setTickColor(self.ticks[1], QtGui.QColor(255,255,255)) - #self.ui.gradientWidget.setOrientation('right') - if imageItem is None: self.imageItem = ImageItem() else: @@ -133,7 +118,6 @@ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *ar self.normRoi.setZValue(20) self.view.addItem(self.normRoi) self.normRoi.hide() - #self.ui.roiPlot.hide() self.roiCurve = self.ui.roiPlot.plot() self.timeLine = InfiniteLine(0, movable=True) self.timeLine.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0, 200))) @@ -147,13 +131,6 @@ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *ar self.playRate = 0 self.lastPlayTime = 0 - #self.normLines = [] - #for i in [0,1]: - #l = InfiniteLine(self.ui.roiPlot, 0) - #l.setPen(QtGui.QPen(QtGui.QColor(0, 100, 200, 200))) - #self.ui.roiPlot.addItem(l) - #self.normLines.append(l) - #l.hide() self.normRgn = LinearRegionItem() self.normRgn.setZValue(0) self.ui.roiPlot.addItem(self.normRgn) @@ -168,7 +145,6 @@ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *ar setattr(self, fn, getattr(self.ui.histogram, fn)) self.timeLine.sigPositionChanged.connect(self.timeLineChanged) - #self.ui.gradientWidget.sigGradientChanged.connect(self.updateImage) self.ui.roiBtn.clicked.connect(self.roiClicked) self.roi.sigRegionChanged.connect(self.roiChanged) self.ui.normBtn.toggled.connect(self.normToggled) @@ -187,31 +163,32 @@ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *ar self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown] - self.roiClicked() ## initialize roi plot to correct shape / visibility - def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None): + def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True): """ Set the image to be displayed in the widget. - ============== ======================================================================= + ================== ======================================================================= **Arguments:** - *img* (numpy array) the image to be displayed. - *xvals* (numpy array) 1D array of z-axis values corresponding to the third axis - in a 3D image. For video, this array should contain the time of each frame. - *autoRange* (bool) whether to scale/pan the view to fit the image. - *autoLevels* (bool) whether to update the white/black levels to fit the image. - *levels* (min, max); the white and black level values to use. - *axes* Dictionary indicating the interpretation for each axis. - This is only needed to override the default guess. Format is:: + img (numpy array) the image to be displayed. + xvals (numpy array) 1D array of z-axis values corresponding to the third axis + in a 3D image. For video, this array should contain the time of each frame. + autoRange (bool) whether to scale/pan the view to fit the image. + autoLevels (bool) whether to update the white/black levels to fit the image. + levels (min, max); the white and black level values to use. + axes Dictionary indicating the interpretation for each axis. + This is only needed to override the default guess. Format is:: - {'t':0, 'x':1, 'y':2, 'c':3}; - - *pos* Change the position of the displayed image - *scale* Change the scale of the displayed image - *transform* Set the transform of the displayed image. This option overrides *pos* - and *scale*. - ============== ======================================================================= + {'t':0, 'x':1, 'y':2, 'c':3}; + + pos Change the position of the displayed image + scale Change the scale of the displayed image + transform Set the transform of the displayed image. This option overrides *pos* + and *scale*. + autoHistogramRange If True, the histogram y-range is automatically scaled to fit the + image data. + ================== ======================================================================= """ prof = debug.Profiler('ImageView.setImage', disabled=True) @@ -231,9 +208,7 @@ def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, self.tVals = np.arange(img.shape[0]) else: self.tVals = np.arange(img.shape[0]) - #self.ui.timeSlider.setValue(0) - #self.ui.normStartSlider.setValue(0) - #self.ui.timeSlider.setMaximum(img.shape[0]-1) + prof.mark('1') if axes is None: @@ -265,14 +240,12 @@ def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, prof.mark('3') - + self.currentIndex = 0 - self.updateImage() + self.updateImage(autoHistogramRange=autoHistogramRange) if levels is None and autoLevels: self.autoLevels() if levels is not None: ## this does nothing since getProcessedImage sets these values again. - #self.levelMax = levels[1] - #self.levelMin = levels[0] self.setLevels(*levels) if self.ui.roiBtn.isChecked(): @@ -329,15 +302,9 @@ def play(self, rate): if not self.playTimer.isActive(): self.playTimer.start(16) - - def autoLevels(self): - """Set the min/max levels automatically to match the image data.""" - #image = self.getProcessedImage() + """Set the min/max intensity levels automatically to match the image data.""" self.setLevels(self.levelMin, self.levelMax) - - #self.ui.histogram.imageChanged(autoLevel=True) - def setLevels(self, min, max): """Set the min/max (bright and dark) levels.""" @@ -346,17 +313,16 @@ def setLevels(self, min, max): def autoRange(self): """Auto scale and pan the view around the image.""" image = self.getProcessedImage() - - #self.ui.graphicsView.setRange(QtCore.QRectF(0, 0, image.shape[self.axes['x']], image.shape[self.axes['y']]), padding=0., lockAspect=True) - self.view.autoRange() ##setRange(self.imageItem.viewBoundingRect(), padding=0.) + self.view.autoRange() def getProcessedImage(self): - """Returns the image data after it has been processed by any normalization options in use.""" + """Returns the image data after it has been processed by any normalization options in use. + This method also sets the attributes self.levelMin and self.levelMax + to indicate the range of data in the image.""" if self.imageDisp is None: image = self.normalize(self.image) self.imageDisp = image self.levelMin, self.levelMax = list(map(float, ImageView.quickMinMax(self.imageDisp))) - self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) return self.imageDisp @@ -365,7 +331,6 @@ def close(self): """Closes the widget nicely, making sure to clear the graphics scene and release memory.""" self.ui.roiPlot.close() self.ui.graphicsView.close() - #self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage) self.scene.clear() del self.image del self.imageDisp @@ -468,20 +433,12 @@ def jumpFrames(self, n): def normRadioChanged(self): self.imageDisp = None self.updateImage() + self.autoLevels() self.roiChanged() self.sigProcessingChanged.emit(self) def updateNorm(self): - #for l, sl in zip(self.normLines, [self.ui.normStartSlider, self.ui.normStopSlider]): - #if self.ui.normTimeRangeCheck.isChecked(): - #l.show() - #else: - #l.hide() - - #i, t = self.timeIndex(sl) - #l.setPos(t) - if self.ui.normTimeRangeCheck.isChecked(): #print "show!" self.normRgn.show() @@ -497,6 +454,7 @@ def updateNorm(self): if not self.ui.normOffRadio.isChecked(): self.imageDisp = None self.updateImage() + self.autoLevels() self.roiChanged() self.sigProcessingChanged.emit(self) @@ -634,22 +592,19 @@ def timeLineChanged(self): #self.emit(QtCore.SIGNAL('timeChanged'), ind, time) self.sigTimeChanged.emit(ind, time) - def updateImage(self): + def updateImage(self, autoHistogramRange=True): ## Redraw image on screen if self.image is None: return image = self.getProcessedImage() - #print "update:", image.ndim, image.max(), image.min(), self.blackLevel(), self.whiteLevel() + + if autoHistogramRange: + self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) if self.axes['t'] is None: - #self.ui.timeSlider.hide() self.imageItem.updateImage(image) - #self.ui.roiPlot.hide() - #self.ui.roiBtn.hide() else: - #self.ui.roiBtn.show() self.ui.roiPlot.show() - #self.ui.timeSlider.show() self.imageItem.updateImage(image[self.currentIndex]) @@ -657,38 +612,22 @@ def timeIndex(self, slider): ## Return the time and frame index indicated by a slider if self.image is None: return (0,0) - #v = slider.value() - #vmax = slider.maximum() - #f = float(v) / vmax t = slider.value() - #t = 0.0 - #xv = self.image.xvals('Time') xv = self.tVals if xv is None: ind = int(t) - #ind = int(f * self.image.shape[0]) else: if len(xv) < 2: return (0,0) totTime = xv[-1] + (xv[-1]-xv[-2]) - #t = f * totTime inds = np.argwhere(xv < t) if len(inds) < 1: return (0,t) ind = inds[-1,0] - #print ind return ind, t - #def whiteLevel(self): - #return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[1]) - ##return self.levelMin + (self.levelMax-self.levelMin) * self.ui.whiteSlider.value() / self.ui.whiteSlider.maximum() - - #def blackLevel(self): - #return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[0]) - ##return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value() - def getView(self): """Return the ViewBox (or other compatible object) which displays the ImageItem""" return self.view From aff70070ac83943b517036472a22fb5f01e0d79f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 30 May 2013 12:57:03 -0400 Subject: [PATCH 042/121] started Qt documentation extension --- doc/extensions/qt_doc.py | 143 +++++++++++++++++++++++++++++++++++++++ doc/source/conf.py | 4 +- 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 doc/extensions/qt_doc.py diff --git a/doc/extensions/qt_doc.py b/doc/extensions/qt_doc.py new file mode 100644 index 0000000000..17d1ef32e7 --- /dev/null +++ b/doc/extensions/qt_doc.py @@ -0,0 +1,143 @@ +""" +Extension for building Qt-like documentation. + + - Method lists preceding the actual method documentation + - Inherited members documented separately + - Members inherited from Qt have links to qt-project documentation + - Signal documentation + +""" + + + +def setup(app): + ## Add new configuration options + app.add_config_value('todo_include_todos', False, False) + + ## Nodes are the basic objects representing documentation directives + ## and roles + app.add_node(Todolist) + app.add_node(Todo, + html=(visit_todo_node, depart_todo_node), + latex=(visit_todo_node, depart_todo_node), + text=(visit_todo_node, depart_todo_node)) + + ## New directives like ".. todo:" + app.add_directive('todo', TodoDirective) + app.add_directive('todolist', TodolistDirective) + + ## Connect callbacks to specific hooks in the build process + app.connect('doctree-resolved', process_todo_nodes) + app.connect('env-purge-doc', purge_todos) + + +from docutils import nodes +from sphinx.util.compat import Directive +from sphinx.util.compat import make_admonition + + +# Just a general node +class Todolist(nodes.General, nodes.Element): + pass + +# .. and its directive +class TodolistDirective(Directive): + # all directives have 'run' method that returns a list of nodes + def run(self): + return [Todolist('')] + + + + +# Admonition classes are like notes or warnings +class Todo(nodes.Admonition, nodes.Element): + pass + +def visit_todo_node(self, node): + self.visit_admonition(node) + +def depart_todo_node(self, node): + self.depart_admonition(node) + +class TodoDirective(Directive): + + # this enables content in the directive + has_content = True + + def run(self): + env = self.state.document.settings.env + + # create a new target node for linking to + targetid = "todo-%d" % env.new_serialno('todo') + targetnode = nodes.target('', '', ids=[targetid]) + + # make the admonition node + ad = make_admonition(Todo, self.name, [('Todo')], self.options, + self.content, self.lineno, self.content_offset, + self.block_text, self.state, self.state_machine) + + # store a handle in a global list of all todos + if not hasattr(env, 'todo_all_todos'): + env.todo_all_todos = [] + env.todo_all_todos.append({ + 'docname': env.docname, + 'lineno': self.lineno, + 'todo': ad[0].deepcopy(), + 'target': targetnode, + }) + + # return both the linking target and the node itself + return [targetnode] + ad + + +# env data is persistent across source files so we purge whenever the source file has changed. +def purge_todos(app, env, docname): + if not hasattr(env, 'todo_all_todos'): + return + env.todo_all_todos = [todo for todo in env.todo_all_todos + if todo['docname'] != docname] + + +# called at the end of resolving phase; we will convert temporary nodes +# into finalized nodes +def process_todo_nodes(app, doctree, fromdocname): + if not app.config.todo_include_todos: + for node in doctree.traverse(Todo): + node.parent.remove(node) + + # Replace all todolist nodes with a list of the collected todos. + # Augment each todo with a backlink to the original location. + env = app.builder.env + + for node in doctree.traverse(Todolist): + if not app.config.todo_include_todos: + node.replace_self([]) + continue + + content = [] + + for todo_info in env.todo_all_todos: + para = nodes.paragraph() + filename = env.doc2path(todo_info['docname'], base=None) + description = ( + ('(The original entry is located in %s, line %d and can be found ') % + (filename, todo_info['lineno'])) + para += nodes.Text(description, description) + + # Create a reference + newnode = nodes.reference('', '') + innernode = nodes.emphasis(('here'), ('here')) + newnode['refdocname'] = todo_info['docname'] + newnode['refuri'] = app.builder.get_relative_uri( + fromdocname, todo_info['docname']) + newnode['refuri'] += '#' + todo_info['target']['refid'] + newnode.append(innernode) + para += newnode + para += nodes.Text('.)', '.)') + + # Insert into the todolist + content.append(todo_info['todo']) + content.append(para) + + node.replace_self(content) + diff --git a/doc/source/conf.py b/doc/source/conf.py index 236cb80738..893f79f54d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -18,6 +18,7 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(path, '..', '..')) +sys.path.insert(0, os.path.join(path, '..', 'extensions')) # -- General configuration ----------------------------------------------------- @@ -26,7 +27,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'qt_doc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -215,3 +216,4 @@ ('index', 'pyqtgraph', 'pyqtgraph Documentation', ['Luke Campagnola'], 1) ] + From 9a20d051cbd8cb1d550a7d5a53c6c87bab062dfb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 31 May 2013 10:15:40 -0400 Subject: [PATCH 043/121] Fixed unicode support in export file save --- pyqtgraph/exporters/Exporter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/Exporter.py b/pyqtgraph/exporters/Exporter.py index f5a9308866..43a8c33035 100644 --- a/pyqtgraph/exporters/Exporter.py +++ b/pyqtgraph/exporters/Exporter.py @@ -1,6 +1,7 @@ from pyqtgraph.widgets.FileDialog import FileDialog import pyqtgraph as pg from pyqtgraph.Qt import QtGui, QtCore, QtSvg +from pyqtgraph.python2_3 import asUnicode import os, re LastExportDirectory = None @@ -56,13 +57,13 @@ def fileSaveDialog(self, filter=None, opts=None): return def fileSaveFinished(self, fileName): - fileName = str(fileName) + fileName = asUnicode(fileName) global LastExportDirectory LastExportDirectory = os.path.split(fileName)[0] ## If file name does not match selected extension, append it now ext = os.path.splitext(fileName)[1].lower().lstrip('.') - selectedExt = re.search(r'\*\.(\w+)\b', str(self.fileDialog.selectedNameFilter())) + selectedExt = re.search(r'\*\.(\w+)\b', asUnicode(self.fileDialog.selectedNameFilter())) if selectedExt is not None: selectedExt = selectedExt.groups()[0].lower() if ext != selectedExt: From 3d820400d32da0b5dbc47e1fc19cf458f71b9e2b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 31 May 2013 14:04:04 -0400 Subject: [PATCH 044/121] Added GLLinePlotItem documentation --- doc/source/3dgraphics/gllineplotitem.rst | 8 ++++++++ doc/source/3dgraphics/index.rst | 1 + pyqtgraph/opengl/items/GLLinePlotItem.py | 3 ++- 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 doc/source/3dgraphics/gllineplotitem.rst diff --git a/doc/source/3dgraphics/gllineplotitem.rst b/doc/source/3dgraphics/gllineplotitem.rst new file mode 100644 index 0000000000..490ba2986a --- /dev/null +++ b/doc/source/3dgraphics/gllineplotitem.rst @@ -0,0 +1,8 @@ +GLLinePlotItem +============== + +.. autoclass:: pyqtgraph.opengl.GLLinePlotItem + :members: + + .. automethod:: pyqtgraph.opengl.GLLinePlotItem.__init__ + diff --git a/doc/source/3dgraphics/index.rst b/doc/source/3dgraphics/index.rst index 255f550bcc..d025a4c7a3 100644 --- a/doc/source/3dgraphics/index.rst +++ b/doc/source/3dgraphics/index.rst @@ -20,6 +20,7 @@ Contents: glvolumeitem glimageitem glmeshitem + gllineplotitem glaxisitem glgraphicsitem glscatterplotitem diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py index 9ef34cab26..bb5ce2f6e3 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -11,6 +11,7 @@ class GLLinePlotItem(GLGraphicsItem): """Draws line plots in 3D.""" def __init__(self, **kwds): + """All keyword arguments are passed to setData()""" GLGraphicsItem.__init__(self) glopts = kwds.pop('glOptions', 'additive') self.setGLOptions(glopts) @@ -22,7 +23,7 @@ def __init__(self, **kwds): def setData(self, **kwds): """ Update the data displayed by this item. All arguments are optional; - for example it is allowed to update spot positions while leaving + for example it is allowed to update vertex positions while leaving colors unchanged, etc. ==================== ================================================== From f5435b7798bc46e6585a1b913358af574628b2ce Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 1 Jun 2013 07:54:55 -0400 Subject: [PATCH 045/121] Fixed ScatterPlotItem.renderSymbol device argument --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 29bfeaac23..bec6a318e9 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -53,25 +53,17 @@ def renderSymbol(symbol, size, pen, brush, device=None): the symbol will be rendered into the device specified (See QPainter documentation for more information). """ - ## see if this pixmap is already cached - #global SymbolPixmapCache - #key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color())) - #if key in SymbolPixmapCache: - #return SymbolPixmapCache[key] - ## Render a spot with the given parameters to a pixmap 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) + if device is None: + device = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32) + device.fill(0) + p = QtGui.QPainter(device) p.setRenderHint(p.Antialiasing) - p.translate(image.width()*0.5, image.height()*0.5) + p.translate(device.width()*0.5, device.height()*0.5) drawSymbol(p, symbol, size, pen, brush) p.end() - return image - #pixmap = QtGui.QPixmap(image) - #SymbolPixmapCache[key] = pixmap - #return pixmap + return device def makeSymbolPixmap(size, pen, brush, symbol): ## deprecated From aa85ed2828a8b6823ed1b03f6e0e81b6e6dc0e9d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 7 Jun 2013 17:05:54 -0400 Subject: [PATCH 046/121] fixed QString -> str conversions in flowchart --- pyqtgraph/flowchart/Flowchart.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index a68cf542a7..be0d86e58b 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -560,6 +560,7 @@ def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc self.fileDialog.fileSelected.connect(self.saveFile) return #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") + fileName = str(fileName) configfile.writeConfigFile(self.saveState(), fileName) self.sigFileSaved.emit(fileName) @@ -681,7 +682,7 @@ def loadClicked(self): #self.setCurrentFile(newFile) def fileSaved(self, fileName): - self.setCurrentFile(fileName) + self.setCurrentFile(str(fileName)) self.ui.saveBtn.success("Saved.") def saveClicked(self): @@ -710,7 +711,7 @@ def saveAsClicked(self): #self.setCurrentFile(newFile) def setCurrentFile(self, fileName): - self.currentFileName = fileName + self.currentFileName = str(fileName) if fileName is None: self.ui.fileNameLabel.setText("[ new ]") else: From 2243082d4b679f0495be4bdc3201c0c796dd7afb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 16 Jun 2013 23:31:27 -0400 Subject: [PATCH 047/121] Added export methods to GLViewWidget --- pyqtgraph/opengl/GLViewWidget.py | 66 +++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 40bd853e38..493d2523a3 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -1,7 +1,10 @@ from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL from OpenGL.GL import * +import OpenGL.GL.framebufferobjects as glfbo import numpy as np from pyqtgraph import Vector +import pyqtgraph.functions as fn + ##Vector = QtGui.QVector3D class GLViewWidget(QtOpenGL.QGLWidget): @@ -287,4 +290,65 @@ def checkOpenGLVersion(self, msg): raise - \ No newline at end of file + + def readQImage(self): + """ + Read the current buffer pixels out as a QImage. + """ + w = self.width() + h = self.height() + self.repaint() + pixels = np.empty((h, w, 4), dtype=np.ubyte) + pixels[:] = 128 + pixels[...,0] = 50 + pixels[...,3] = 255 + + glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels) + + # swap B,R channels for Qt + tmp = pixels[...,0].copy() + pixels[...,0] = pixels[...,2] + pixels[...,2] = tmp + pixels = pixels[::-1] # flip vertical + + img = fn.makeQImage(pixels, transpose=False) + return img + + + def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE): + w,h = size + self.makeCurrent() + + try: + fb = glfbo.glGenFramebuffers(1) + glfbo.glBindFramebuffer(glfbo.GL_FRAMEBUFFER, fb ) + + glEnable(GL_TEXTURE_2D) + tex = glGenTextures(1) + glBindTexture(GL_TEXTURE_2D, tex) + data = np.zeros((w,h,4), dtype=np.ubyte) + + ## Test texture dimensions first + glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, None) + if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0: + raise Exception("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % shape[:2]) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data.transpose((1,0,2))) + + ## render to texture + glfbo.glFramebufferTexture2D(glfbo.GL_FRAMEBUFFER, glfbo.GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0) + glViewport(0, 0, w, h) + self.paintGL() + + ## read texture back to array + data = glGetTexImage(GL_TEXTURE_2D, 0, format, type) + data = np.fromstring(data, dtype=np.ubyte).reshape(h,w,4).transpose(1,0,2)[:, ::-1] + + finally: + glViewport(0, 0, self.width(), self.height()) + glfbo.glBindFramebuffer(glfbo.GL_FRAMEBUFFER, 0) + glBindTexture(GL_TEXTURE_2D, 0) + + return data + + + From 3656b022375396e3ae78a26084541c5be727bb73 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 18 Jun 2013 10:55:25 -0400 Subject: [PATCH 048/121] Enabled piecewise export --- pyqtgraph/opengl/GLViewWidget.py | 89 ++++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 22 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 493d2523a3..830348878f 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -34,6 +34,7 @@ def __init__(self, parent=None): 'elevation': 30, ## camera's angle of elevation in degrees 'azimuth': 45, ## camera's azimuthal angle in degrees ## (rotation around z-axis 0 points along x-axis) + 'viewport': None, ## glViewport params; None == whole widget } self.items = [] self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown] @@ -66,25 +67,46 @@ def initializeGL(self): glClearColor(0.0, 0.0, 0.0, 0.0) self.resizeGL(self.width(), self.height()) + def getViewport(self): + vp = self.opts['viewport'] + if vp is None: + return (0, 0, self.width(), self.height()) + else: + return vp + def resizeGL(self, w, h): - glViewport(0, 0, w, h) + pass + #glViewport(*self.getViewport()) #self.update() - def setProjection(self): + def setProjection(self, region=None): + # Xw = (Xnd + 1) * width/2 + X + if region is None: + region = (0, 0, self.width(), self.height()) ## Create the projection matrix glMatrixMode(GL_PROJECTION) glLoadIdentity() - w = self.width() - h = self.height() + #w = self.width() + #h = self.height() + x0, y0, w, h = self.getViewport() dist = self.opts['distance'] fov = self.opts['fov'] - nearClip = dist * 0.001 farClip = dist * 1000. r = nearClip * np.tan(fov * 0.5 * np.pi / 180.) t = r * h / w - glFrustum( -r, r, -t, t, nearClip, farClip) + + # convert screen coordinates (region) to normalized device coordinates + # Xnd = (Xw - X0) * 2/width - 1 + ## Note that X0 and width in these equations must be the values used in viewport + left = r * ((region[0]-x0) * (2.0/w) - 1) + right = r * ((region[0]+region[2]-x0) * (2.0/w) - 1) + bottom = t * ((region[1]-y0) * (2.0/h) - 1) + top = t * ((region[1]+region[3]-y0) * (2.0/h) - 1) + + glFrustum( left, right, bottom, top, nearClip, farClip) + #glFrustum(-r, r, -t, t, nearClip, farClip) def setModelview(self): glMatrixMode(GL_MODELVIEW) @@ -96,8 +118,17 @@ def setModelview(self): glTranslatef(-center.x(), -center.y(), -center.z()) - def paintGL(self): - self.setProjection() + def paintGL(self, region=None, viewport=None): + """ + viewport specifies the arguments to glViewport. If None, then we use self.opts['viewport'] + region specifies the sub-region of self.opts['viewport'] that should be rendered. + Note that we may use viewport != self.opts['viewport'] when exporting. + """ + if viewport is None: + glViewport(*self.getViewport()) + else: + glViewport(*viewport) + self.setProjection(region=region) self.setModelview() glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) self.drawItemTree() @@ -316,39 +347,53 @@ def readQImage(self): def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE): - w,h = size + w,h = map(int, size) + self.makeCurrent() try: + output = np.empty((w, h, 4), dtype=np.ubyte) fb = glfbo.glGenFramebuffers(1) glfbo.glBindFramebuffer(glfbo.GL_FRAMEBUFFER, fb ) glEnable(GL_TEXTURE_2D) tex = glGenTextures(1) glBindTexture(GL_TEXTURE_2D, tex) - data = np.zeros((w,h,4), dtype=np.ubyte) + texwidth = 512 + data = np.zeros((texwidth,texwidth,4), dtype=np.ubyte) ## Test texture dimensions first - glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, None) + glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, texwidth, texwidth, 0, GL_RGBA, GL_UNSIGNED_BYTE, None) if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0: raise Exception("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % shape[:2]) - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data.transpose((1,0,2))) - - ## render to texture - glfbo.glFramebufferTexture2D(glfbo.GL_FRAMEBUFFER, glfbo.GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0) - glViewport(0, 0, w, h) - self.paintGL() + ## create teture + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texwidth, texwidth, 0, GL_RGBA, GL_UNSIGNED_BYTE, data.transpose((1,0,2))) + + self.opts['viewport'] = (0, 0, w, h) # viewport is the complete image; this ensures that paintGL(region=...) + # is interpreted correctly. - ## read texture back to array - data = glGetTexImage(GL_TEXTURE_2D, 0, format, type) - data = np.fromstring(data, dtype=np.ubyte).reshape(h,w,4).transpose(1,0,2)[:, ::-1] + for x in range(0, w, texwidth): + for y in range(0, h, texwidth): + x2 = min(x+texwidth, w) + y2 = min(y+texwidth, h) + w2 = x2-x + h2 = y2-y + + ## render to texture + glfbo.glFramebufferTexture2D(glfbo.GL_FRAMEBUFFER, glfbo.GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0) + self.paintGL(region=(x, h-y-h2, w2, h2), viewport=(0, 0, w2, h2)) # only render sub-region + + ## read texture back to array + data = glGetTexImage(GL_TEXTURE_2D, 0, format, type) + data = np.fromstring(data, dtype=np.ubyte).reshape(texwidth,texwidth,4).transpose(1,0,2)[:, ::-1] + output[x:x2, y:y2] = data[:w2, -h2:] finally: - glViewport(0, 0, self.width(), self.height()) + self.opts['viewport'] = None glfbo.glBindFramebuffer(glfbo.GL_FRAMEBUFFER, 0) glBindTexture(GL_TEXTURE_2D, 0) - return data + return output From e864043e76d9c1d02d59f88ded79f7cd88f7cbbc Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 18 Jun 2013 21:46:50 -0400 Subject: [PATCH 049/121] delete texture and framebuffer after export --- pyqtgraph/opengl/GLViewWidget.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 830348878f..afab475cc3 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -350,7 +350,8 @@ def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE): w,h = map(int, size) self.makeCurrent() - + tex = None + fb = None try: output = np.empty((w, h, 4), dtype=np.ubyte) fb = glfbo.glGenFramebuffers(1) @@ -387,11 +388,15 @@ def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE): data = glGetTexImage(GL_TEXTURE_2D, 0, format, type) data = np.fromstring(data, dtype=np.ubyte).reshape(texwidth,texwidth,4).transpose(1,0,2)[:, ::-1] output[x:x2, y:y2] = data[:w2, -h2:] - + finally: self.opts['viewport'] = None glfbo.glBindFramebuffer(glfbo.GL_FRAMEBUFFER, 0) glBindTexture(GL_TEXTURE_2D, 0) + if tex is not None: + glDeleteTextures([tex]) + if fb is not None: + glfbo.glDeleteFramebuffers([fb]) return output From 1b17bc6adba6fa44d2c767025bcfa1c1d86ea5cf Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 19 Jun 2013 09:10:14 -0400 Subject: [PATCH 050/121] export uses padding to prevent edge effects --- pyqtgraph/opengl/GLViewWidget.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index afab475cc3..12984c8679 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -346,7 +346,7 @@ def readQImage(self): return img - def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE): + def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE, textureSize=1024, padding=256): w,h = map(int, size) self.makeCurrent() @@ -360,7 +360,7 @@ def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE): glEnable(GL_TEXTURE_2D) tex = glGenTextures(1) glBindTexture(GL_TEXTURE_2D, tex) - texwidth = 512 + texwidth = textureSize data = np.zeros((texwidth,texwidth,4), dtype=np.ubyte) ## Test texture dimensions first @@ -372,22 +372,23 @@ def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE): self.opts['viewport'] = (0, 0, w, h) # viewport is the complete image; this ensures that paintGL(region=...) # is interpreted correctly. - - for x in range(0, w, texwidth): - for y in range(0, h, texwidth): - x2 = min(x+texwidth, w) - y2 = min(y+texwidth, h) + p2 = 2 * padding + for x in range(-padding, w-padding, texwidth-p2): + for y in range(-padding, h-padding, texwidth-p2): + x2 = min(x+texwidth, w+padding) + y2 = min(y+texwidth, h+padding) w2 = x2-x h2 = y2-y ## render to texture glfbo.glFramebufferTexture2D(glfbo.GL_FRAMEBUFFER, glfbo.GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0) + self.paintGL(region=(x, h-y-h2, w2, h2), viewport=(0, 0, w2, h2)) # only render sub-region ## read texture back to array data = glGetTexImage(GL_TEXTURE_2D, 0, format, type) data = np.fromstring(data, dtype=np.ubyte).reshape(texwidth,texwidth,4).transpose(1,0,2)[:, ::-1] - output[x:x2, y:y2] = data[:w2, -h2:] + output[x+padding:x2-padding, y+padding:y2-padding] = data[padding:w2-padding, -(h2-padding):-padding] finally: self.opts['viewport'] = None From fa354ea4a398711c62bd17aa87d2609fd017da11 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 19 Jun 2013 19:30:23 -0400 Subject: [PATCH 051/121] bugfix in ViewBox.clear --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 0a625d48bc..9edca06f9b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -295,7 +295,7 @@ def clear(self): for i in self.addedItems[:]: self.removeItem(i) for ch in self.childGroup.childItems(): - ch.setParent(None) + ch.setParentItem(None) def resizeEvent(self, ev): #self.setRange(self.range, padding=0) From cbd0efe79a6ef3642ec5ede840c98799c7e43842 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 19 Jun 2013 19:32:55 -0400 Subject: [PATCH 052/121] ImageItem informs ViewBox when its size changes Minor edits --- pyqtgraph/graphicsItems/ImageItem.py | 8 +++++--- pyqtgraph/graphicsItems/LegendItem.py | 1 - pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index fad88bee75..530db7fb95 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -196,10 +196,12 @@ def setImage(self, image=None, autoLevels=None, **kargs): return else: gotNewData = True - if self.image is None or image.shape != self.image.shape: - self.prepareGeometryChange() + shapeChanged = (self.image is None or image.shape != self.image.shape) self.image = image.view(np.ndarray) - + if shapeChanged: + self.prepareGeometryChange() + self.informViewBoundsChanged() + prof.mark('1') if autoLevels is None: diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 86973b04fe..69ddffea3f 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -92,7 +92,6 @@ def removeItem(self, name): # Thanks, Ulrich! # cycle for a match for sample, label in self.items: - print label.text, name if label.text == name: # hit self.items.remove( (sample, label) ) # remove from itemlist self.layout.removeItem(sample) # remove from layout diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index bec6a318e9..c83249014f 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -35,6 +35,8 @@ def drawSymbol(painter, symbol, size, pen, brush): + if symbol is None: + return painter.scale(size, size) painter.setPen(pen) painter.setBrush(brush) From adda8ae24d3db24cf933ddd959055afa82c47994 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 19 Jun 2013 19:36:46 -0400 Subject: [PATCH 053/121] New methods in use for converting array -> QImage. This fixes memory leaks with PyQt 4.10 _except_ when using makeQImage(copy=False). Tested on 4.9.3 and 4.10.2; need to be tested against other versions. --- pyqtgraph/functions.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 836ae43356..a9cf26939d 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -911,7 +911,8 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): array.shape[2] == 4. copy If True, the data is copied before converting to QImage. If False, the new QImage points directly to the data in the array. - Note that the array must be contiguous for this to work. + Note that the array must be contiguous for this to work + (see numpy.ascontiguousarray). transpose If True (the default), the array x/y axes are transposed before creating the image. Note that Qt expects the axes to be in (height, width) order whereas pyqtgraph usually prefers the @@ -961,12 +962,22 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): #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) + #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) 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 = QtGui.QImage(imgData.ctypes.data, imgData.shape[1], imgData.shape[0], imgFormat) + except: + if copy: + # does not leak memory, is not mutable + img = QtGui.QImage(buffer(imgData), imgData.shape[1], imgData.shape[0], imgFormat) + else: + # mutable, but leaks memory + img = QtGui.QImage(memoryview(imgData), imgData.shape[1], imgData.shape[0], imgFormat) + img.data = imgData return img #try: From f03703e78f6aae178a855e3dc21897000f6b2c80 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 20 Jun 2013 08:44:46 -0400 Subject: [PATCH 054/121] corrected exception error message --- pyqtgraph/graphicsItems/PlotCurveItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index d707a3475c..ebcd0d3882 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -312,10 +312,10 @@ def updateData(self, *args, **kargs): if self.opts['stepMode'] is True: if len(self.xData) != len(self.yData)+1: ## allow difference of 1 for step mode plots - raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (str(x.shape), str(y.shape))) + raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (self.xData.shape, self.yData.shape)) else: if self.xData.shape != self.yData.shape: ## allow difference of 1 for step mode plots - raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) + raise Exception("X and Y arrays must be the same shape--got %s and %s." % (self.xData.shape, self.yData.shape)) self.path = None self.fillPath = None From 79e2b1403b5ae833843b38dbf5b20a882819d40c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 3 Jul 2013 08:54:18 -0400 Subject: [PATCH 055/121] minor edits --- doc/extensions/qt_doc.py | 6 +++++ doc/source/conf.py | 2 +- doc/source/qtcrashcourse.rst | 8 +++++++ pyqtgraph/graphicsItems/ROI.py | 41 +--------------------------------- 4 files changed, 16 insertions(+), 41 deletions(-) diff --git a/doc/extensions/qt_doc.py b/doc/extensions/qt_doc.py index 17d1ef32e7..75c848fc41 100644 --- a/doc/extensions/qt_doc.py +++ b/doc/extensions/qt_doc.py @@ -11,6 +11,12 @@ def setup(app): + # probably we will be making a wrapper around autodoc + app.setup_extension('sphinx.ext.autodoc') + + # would it be useful to define a new domain? + #app.add_domain(QtDomain) + ## Add new configuration options app.add_config_value('todo_include_todos', False, False) diff --git a/doc/source/conf.py b/doc/source/conf.py index 893f79f54d..5475fc60cb 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -27,7 +27,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'qt_doc'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/doc/source/qtcrashcourse.rst b/doc/source/qtcrashcourse.rst index 23a561b94c..f117bb7f89 100644 --- a/doc/source/qtcrashcourse.rst +++ b/doc/source/qtcrashcourse.rst @@ -76,15 +76,23 @@ Qt detects and reacts to user interaction by executing its *event loop*. GraphicsView and GraphicsItems ------------------------------ +More information about the architecture of Qt GraphicsView: +http://qt-project.org/doc/qt-4.8/graphicsview.html + Coordinate Systems and Transformations -------------------------------------- +More information about the coordinate systems in Qt GraphicsView: +http://qt-project.org/doc/qt-4.8/graphicsview.html#the-graphics-view-coordinate-system + Mouse and Keyboard Input ------------------------ + + QTimer, Multi-Threading ----------------------- diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index bdfc850822..a5e25a2f73 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1621,7 +1621,7 @@ def __init__(self, positions, closed=False, pos=None, **args): if pos is None: pos = [0,0] - #pen=args.get('pen', fn.mkPen((100,100,255))) + ROI.__init__(self, pos, size=[1,1], **args) self.closed = closed self.segments = [] @@ -1632,33 +1632,6 @@ def __init__(self, positions, closed=False, pos=None, **args): start = -1 if self.closed else 0 for i in range(start, len(self.handles)-1): self.addSegment(self.handles[i]['item'], self.handles[i+1]['item']) - #for i in range(len(positions)-1): - #h2 = self.addFreeHandle(positions[i+1]) - #segment = LineSegmentROI(handles=(h, h2), pen=pen, parent=self, movable=False) - #self.segments.append(segment) - #h = h2 - - - #for i, s in enumerate(self.segments): - #h = s.handles[0] - #self.addFreeHandle(h['pos'], item=h['item']) - #s.setZValue(self.zValue() +1) - - #h = self.segments[-1].handles[1] - #self.addFreeHandle(h['pos'], item=h['item']) - - #if closed: - #h1 = self.handles[-1]['item'] - #h2 = self.handles[0]['item'] - #self.segments.append(LineSegmentROI([positions[-1], positions[0]], pos=pos, handles=(h1, h2), pen=pen, parent=self, movable=False)) - #h2.setParentItem(self.segments[-1]) - - - #for s in self.segments: - #self.setSegmentSettings(s) - - #def movePoint(self, *args, **kargs): - #pass def addSegment(self, h1, h2, index=None): seg = LineSegmentROI(handles=(h1, h2), pen=self.pen, parent=self, movable=False) @@ -1675,9 +1648,6 @@ def addSegment(self, h1, h2, index=None): def setMouseHover(self, hover): ## Inform all the ROI's segments that the mouse is(not) hovering over it - #if self.mouseHovering == hover: - #return - #self.mouseHovering = hover ROI.setMouseHover(self, hover) for s in self.segments: s.setMouseHover(hover) @@ -1702,15 +1672,6 @@ def segmentClicked(self, segment, ev=None, pos=None): ## pos should be in this i self.addSegment(h3, h2, index=i+1) segment.replaceHandle(h2, h3) - - #def report(self): - #for s in self.segments: - #print s - #for h in s.handles: - #print " ", h - #for h in self.handles: - #print h - def removeHandle(self, handle, updateSegments=True): ROI.removeHandle(self, handle) handle.sigRemoveRequested.disconnect(self.removeHandle) From 8c13a3e7e37243e024f7b4d9fcc9a870cf8d94c4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 3 Jul 2013 11:20:49 -0400 Subject: [PATCH 056/121] copy from acq4 --- pyqtgraph/Point.py | 6 + pyqtgraph/SRTTransform.py | 5 +- pyqtgraph/SRTTransform3D.py | 19 +- pyqtgraph/debug.py | 9 + pyqtgraph/flowchart/library/Operators.py | 14 +- pyqtgraph/functions.py | 5 + pyqtgraph/graphicsItems/AxisItem.py | 33 +++- pyqtgraph/graphicsItems/GraphicsItem.py | 1 + pyqtgraph/graphicsItems/PlotCurveItem.py | 1 + pyqtgraph/graphicsItems/PlotDataItem.py | 179 ++++++++++++++---- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 92 +++++++-- .../PlotItem/plotConfigTemplate.ui | 143 ++++++++++---- .../PlotItem/plotConfigTemplate_pyqt.py | 79 +++++--- .../PlotItem/plotConfigTemplate_pyside.py | 79 +++++--- pyqtgraph/graphicsItems/ScatterPlotItem.py | 8 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 89 +++++++-- pyqtgraph/metaarray/MetaArray.py | 3 + pyqtgraph/multiprocess/remoteproxy.py | 31 +-- pyqtgraph/widgets/ScatterPlotWidget.py | 9 +- 19 files changed, 607 insertions(+), 198 deletions(-) diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index ea35d11960..682f19f783 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -80,6 +80,12 @@ def __rdiv__(self, a): def __div__(self, a): return self._math_('__div__', a) + def __truediv__(self, a): + return self._math_('__truediv__', a) + + def __rtruediv__(self, a): + return self._math_('__rtruediv__', a) + def __rpow__(self, a): return self._math_('__rpow__', a) diff --git a/pyqtgraph/SRTTransform.py b/pyqtgraph/SRTTransform.py index a861f94087..efb24f60b9 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -130,11 +130,14 @@ def setRotate(self, angle): self._state['angle'] = angle self.update() - def __div__(self, t): + def __truediv__(self, t): """A / B == B^-1 * A""" dt = t.inverted()[0] * self return SRTTransform(dt) + def __div__(self, t): + return self.__truediv__(t) + def __mul__(self, t): return SRTTransform(QtGui.QTransform.__mul__(self, t)) diff --git a/pyqtgraph/SRTTransform3D.py b/pyqtgraph/SRTTransform3D.py index 77583b5a3f..7d87dcb86c 100644 --- a/pyqtgraph/SRTTransform3D.py +++ b/pyqtgraph/SRTTransform3D.py @@ -123,7 +123,6 @@ def setFromMatrix(self, m): m = self.matrix().reshape(4,4) ## translation is 4th column self._state['pos'] = m[:3,3] - ## scale is vector-length of first three columns scale = (m[:3,:3]**2).sum(axis=0)**0.5 ## see whether there is an inversion @@ -141,18 +140,30 @@ def setFromMatrix(self, m): print("Scale: %s" % str(scale)) print("Original matrix: %s" % str(m)) raise - eigIndex = np.argwhere(np.abs(evals-1) < 1e-7) + eigIndex = np.argwhere(np.abs(evals-1) < 1e-6) if len(eigIndex) < 1: print("eigenvalues: %s" % str(evals)) print("eigenvectors: %s" % str(evecs)) print("index: %s, %s" % (str(eigIndex), str(evals-1))) raise Exception("Could not determine rotation axis.") - axis = evecs[eigIndex[0,0]].real + axis = evecs[:,eigIndex[0,0]].real axis /= ((axis**2).sum())**0.5 self._state['axis'] = axis ## trace(r) == 2 cos(angle) + 1, so: - self._state['angle'] = np.arccos((r.trace()-1)*0.5) * 180 / np.pi + cos = (r.trace()-1)*0.5 ## this only gets us abs(angle) + + ## The off-diagonal values can be used to correct the angle ambiguity, + ## but we need to figure out which element to use: + axisInd = np.argmax(np.abs(axis)) + rInd,sign = [((1,2), -1), ((0,2), 1), ((0,1), -1)][axisInd] + + ## Then we have r-r.T = sin(angle) * 2 * sign * axis[axisInd]; + ## solve for sin(angle) + sin = (r-r.T)[rInd] / (2. * sign * axis[axisInd]) + + ## finally, we get the complete angle from arctan(sin/cos) + self._state['angle'] = np.arctan2(sin, cos) * 180 / np.pi if self._state['angle'] == 0: self._state['axis'] = (0,0,1) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index ae2b21ac76..a175be9ca2 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -28,6 +28,15 @@ def w(*args, **kargs): return rv return w +def warnOnException(func): + """Decorator which catches/ignores exceptions and prints a stack trace.""" + def w(*args, **kwds): + try: + func(*args, **kwds) + except: + printExc('Ignored exception:') + return w + def getExc(indent=4, prefix='| '): tb = traceback.format_exc() lines = [] diff --git a/pyqtgraph/flowchart/library/Operators.py b/pyqtgraph/flowchart/library/Operators.py index 412af5736d..579d2cd2b7 100644 --- a/pyqtgraph/flowchart/library/Operators.py +++ b/pyqtgraph/flowchart/library/Operators.py @@ -24,7 +24,15 @@ def __init__(self, name, fn): }) def process(self, **args): - fn = getattr(args['A'], self.fn) + if isinstance(self.fn, tuple): + for name in self.fn: + try: + fn = getattr(args['A'], name) + break + except AttributeError: + pass + else: + fn = getattr(args['A'], self.fn) out = fn(args['B']) if out is NotImplemented: raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B'])))) @@ -60,5 +68,7 @@ class DivideNode(BinOpNode): """Returns A / B. Does not check input types.""" nodeName = 'Divide' def __init__(self, name): - BinOpNode.__init__(self, name, '__div__') + # try truediv first, followed by div + BinOpNode.__init__(self, name, ('__truediv__', '__div__')) + diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 5f820a9a38..4168836eb4 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -264,6 +264,7 @@ def mkPen(*args, **kargs): color = kargs.get('color', None) width = kargs.get('width', 1) style = kargs.get('style', None) + dash = kargs.get('dash', None) cosmetic = kargs.get('cosmetic', True) hsv = kargs.get('hsv', None) @@ -291,6 +292,8 @@ def mkPen(*args, **kargs): pen.setCosmetic(cosmetic) if style is not None: pen.setStyle(style) + if dash is not None: + pen.setDashPattern(dash) return pen def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0): @@ -1948,6 +1951,8 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): s2 = spacing**2 yvals = np.empty(len(data)) + if len(data) == 0: + return yvals yvals[0] = 0 for i in range(1,len(data)): x = data[i] # current x value to be placed diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index e31030dfb6..97f0ef1c58 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -42,12 +42,18 @@ def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLen self.label.rotate(-90) self.style = { - 'tickTextOffset': 3, ## spacing between text and axis + 'tickTextOffset': (5, 2), ## (horizontal, vertical) spacing between text and axis 'tickTextWidth': 30, ## space reserved for tick text 'tickTextHeight': 18, 'autoExpandTextSpace': True, ## automatically expand text space if needed 'tickFont': None, 'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick + 'textFillLimits': [ ## how much of the axis to fill up with tick text, maximally. + (0, 0.8), ## never fill more than 80% of the axis + (2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis + (4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis + (6, 0.2), ## If we already have 6 ticks with text, fill no more than 20% of the axis + ] } self.textWidth = 30 ## Keeps track of maximum width / height of tick text @@ -209,14 +215,14 @@ def _updateMaxTextSize(self, x): ## to accomodate. if self.orientation in ['left', 'right']: mx = max(self.textWidth, x) - if mx > self.textWidth: + if mx > self.textWidth or mx < self.textWidth-10: self.textWidth = mx if self.style['autoExpandTextSpace'] is True: self.setWidth() #return True ## size has changed else: mx = max(self.textHeight, x) - if mx > self.textHeight: + if mx > self.textHeight or mx < self.textHeight-10: self.textHeight = mx if self.style['autoExpandTextSpace'] is True: self.setHeight() @@ -236,7 +242,7 @@ def setHeight(self, h=None): h = self.textHeight else: h = self.style['tickTextHeight'] - h += max(0, self.tickLength) + self.style['tickTextOffset'] + h += max(0, self.tickLength) + self.style['tickTextOffset'][1] if self.label.isVisible(): h += self.label.boundingRect().height() * 0.8 self.setMaximumHeight(h) @@ -252,7 +258,7 @@ def setWidth(self, w=None): w = self.textWidth else: w = self.style['tickTextWidth'] - w += max(0, self.tickLength) + self.style['tickTextOffset'] + w += max(0, self.tickLength) + self.style['tickTextOffset'][0] if self.label.isVisible(): w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate self.setMaximumWidth(w) @@ -430,7 +436,7 @@ def tickSpacing(self, minVal, maxVal, size): return [] ## decide optimal minor tick spacing in pixels (this is just aesthetics) - pixelSpacing = np.log(size+10) * 5 + pixelSpacing = size / np.log(size) optimalTickCount = max(2., size / pixelSpacing) ## optimal minor tick spacing @@ -720,7 +726,7 @@ def generateDrawSpecs(self, p): - textOffset = self.style['tickTextOffset'] ## spacing between axis and text + textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text #if self.style['autoExpandTextSpace'] is True: #textWidth = self.textWidth #textHeight = self.textHeight @@ -728,7 +734,7 @@ def generateDrawSpecs(self, p): #textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text #textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text - + textSize2 = 0 textRects = [] textSpecs = [] ## list of draw for i in range(len(tickLevels)): @@ -770,9 +776,16 @@ def generateDrawSpecs(self, p): textSize = np.sum([r.width() for r in textRects]) textSize2 = np.max([r.height() for r in textRects]) - ## If the strings are too crowded, stop drawing text now + ## If the strings are too crowded, stop drawing text now. + ## We use three different crowding limits based on the number + ## of texts drawn so far. textFillRatio = float(textSize) / lengthInPixels - if textFillRatio > 0.7: + finished = False + for nTexts, limit in self.style['textFillLimits']: + if len(textSpecs) >= nTexts and textFillRatio >= limit: + finished = True + break + if finished: break #spacing, values = tickLevels[best] diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 40ff6bc58c..a129436ea8 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -533,6 +533,7 @@ def viewRangeChanged(self): def viewTransformChanged(self): """ Called whenever the transformation matrix of the view has changed. + (eg, the view range has changed or the view was resized) """ pass diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index d707a3475c..4c66bf728f 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -375,6 +375,7 @@ def shape(self): return QtGui.QPainterPath() return self.path + @pg.debug.warnOnException ## raising an exception here causes crash def paint(self, p, opt, widget): prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) if self.xData is None: diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 76b743592b..1ae528ba2f 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -84,24 +84,28 @@ def __init__(self, *args, **kargs): **Optimization keyword arguments:** - ============ ===================================================================== - antialias (bool) By default, antialiasing is disabled to improve performance. - Note that in some cases (in particluar, when pxMode=True), points - will be rendered antialiased even if this is set to False. - decimate (int) Sub-sample data by selecting every nth sample before plotting - onlyVisible (bool) If True, only plot data that is visible within the X range of - the containing ViewBox. This can improve performance when plotting - very large data sets where only a fraction of the data is visible - at any time. - autoResample (bool) If True, resample the data before plotting to avoid plotting - multiple line segments per pixel. This can improve performance when - viewing very high-density data, but increases the initial overhead - and memory usage. - sampleRate (float) The sample rate of the data along the X axis (for data with - a fixed sample rate). Providing this value improves performance of - the *onlyVisible* and *autoResample* options. - identical *deprecated* - ============ ===================================================================== + ================ ===================================================================== + antialias (bool) By default, antialiasing is disabled to improve performance. + Note that in some cases (in particluar, when pxMode=True), points + will be rendered antialiased even if this is set to False. + decimate deprecated. + downsample (int) Reduce the number of samples displayed by this value + downsampleMethod 'subsample': Downsample by taking the first of N samples. + This method is fastest and least accurate. + 'mean': Downsample by taking the mean of N samples. + 'peak': Downsample by drawing a saw wave that follows the min + and max of the original data. This method produces the best + visual representation of the data but is slower. + autoDownsample (bool) If True, resample the data before plotting to avoid plotting + multiple line segments per pixel. This can improve performance when + viewing very high-density data, but increases the initial overhead + and memory usage. + clipToView (bool) If True, only plot data that is visible within the X range of + the containing ViewBox. This can improve performance when plotting + very large data sets where only a fraction of the data is visible + at any time. + identical *deprecated* + ================ ===================================================================== **Meta-info keyword arguments:** @@ -131,7 +135,6 @@ def __init__(self, *args, **kargs): self.opts = { 'fftMode': False, 'logMode': [False, False], - 'downsample': False, 'alphaHint': 1.0, 'alphaMode': False, @@ -149,6 +152,11 @@ def __init__(self, *args, **kargs): 'antialias': pg.getConfigOption('antialias'), 'pointMode': None, + 'downsample': 1, + 'autoDownsample': False, + 'downsampleMethod': 'peak', + 'clipToView': False, + 'data': None, } self.setData(*args, **kargs) @@ -175,6 +183,7 @@ def setFftMode(self, mode): return self.opts['fftMode'] = mode self.xDisp = self.yDisp = None + self.xClean = self.yClean = None self.updateItems() self.informViewBoundsChanged() @@ -183,6 +192,7 @@ def setLogMode(self, xMode, yMode): return self.opts['logMode'] = [xMode, yMode] self.xDisp = self.yDisp = None + self.xClean = self.yClean = None self.updateItems() self.informViewBoundsChanged() @@ -269,13 +279,51 @@ def setSymbolSize(self, size): #self.scatter.setSymbolSize(symbolSize) self.updateItems() - def setDownsampling(self, ds): - if self.opts['downsample'] == ds: + def setDownsampling(self, ds=None, auto=None, method=None): + """ + Set the downsampling mode of this item. Downsampling reduces the number + of samples drawn to increase performance. + + =========== ================================================================= + Arguments + ds (int) Reduce visible plot samples by this factor. To disable, + set ds=1. + auto (bool) If True, automatically pick *ds* based on visible range + mode 'subsample': Downsample by taking the first of N samples. + This method is fastest and least accurate. + 'mean': Downsample by taking the mean of N samples. + 'peak': Downsample by drawing a saw wave that follows the min + and max of the original data. This method produces the best + visual representation of the data but is slower. + =========== ================================================================= + """ + changed = False + if ds is not None: + if self.opts['downsample'] != ds: + changed = True + self.opts['downsample'] = ds + + if auto is not None and self.opts['autoDownsample'] != auto: + self.opts['autoDownsample'] = auto + changed = True + + if method is not None: + if self.opts['downsampleMethod'] != method: + changed = True + self.opts['downsampleMethod'] = method + + if changed: + self.xDisp = self.yDisp = None + self.updateItems() + + def setClipToView(self, clip): + if self.opts['clipToView'] == clip: return - self.opts['downsample'] = ds + self.opts['clipToView'] = clip self.xDisp = self.yDisp = None self.updateItems() + def setData(self, *args, **kargs): """ Clear any data displayed by this item and display new data. @@ -315,7 +363,7 @@ def setData(self, *args, **kargs): raise Exception('Invalid data type %s' % type(data)) elif len(args) == 2: - seq = ('listOfValues', 'MetaArray') + seq = ('listOfValues', 'MetaArray', 'empty') if dataType(args[0]) not in seq or dataType(args[1]) not in seq: raise Exception('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1])))) if not isinstance(args[0], np.ndarray): @@ -376,6 +424,7 @@ def setData(self, *args, **kargs): self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by self.yData = y.view(np.ndarray) + self.xClean = self.yClean = None self.xDisp = None self.yDisp = None prof.mark('set data') @@ -423,23 +472,28 @@ def updateItems(self): def getData(self): if self.xData is None: return (None, None) - if self.xDisp is None: + + if self.xClean is None: nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData) if any(nanMask): self.dataMask = ~nanMask - x = self.xData[self.dataMask] - y = self.yData[self.dataMask] + self.xClean = self.xData[self.dataMask] + self.yClean = self.yData[self.dataMask] else: self.dataMask = None - x = self.xData - y = self.yData - + self.xClean = self.xData + self.yClean = self.yData - ds = self.opts['downsample'] - if ds > 1: - x = x[::ds] - #y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing - y = y[::ds] + if self.xDisp is None: + x = self.xClean + y = self.yClean + + + #ds = self.opts['downsample'] + #if isinstance(ds, int) and ds > 1: + #x = x[::ds] + ##y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing + #y = y[::ds] if self.opts['fftMode']: f = np.fft.fft(y) / len(y) y = abs(f[1:len(f)/2]) @@ -457,6 +511,53 @@ def getData(self): y = y[self.dataMask] else: self.dataMask = None + + ds = self.opts['downsample'] + if not isinstance(ds, int): + ds = 1 + + if self.opts['autoDownsample']: + # this option presumes that x-values have uniform spacing + range = self.viewRect() + if range is not None: + dx = float(x[-1]-x[0]) / (len(x)-1) + x0 = (range.left()-x[0]) / dx + x1 = (range.right()-x[0]) / dx + width = self.getViewBox().width() + ds = int(max(1, int(0.2 * (x1-x0) / width))) + ## downsampling is expensive; delay until after clipping. + + if self.opts['clipToView']: + # this option presumes that x-values have uniform spacing + range = self.viewRect() + if range is not None: + dx = float(x[-1]-x[0]) / (len(x)-1) + # clip to visible region extended by downsampling value + x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1) + x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1) + x = x[x0:x1] + y = y[x0:x1] + + if ds > 1: + if self.opts['downsampleMethod'] == 'subsample': + x = x[::ds] + y = y[::ds] + elif self.opts['downsampleMethod'] == 'mean': + n = len(x) / ds + x = x[:n*ds:ds] + y = y[:n*ds].reshape(n,ds).mean(axis=1) + elif self.opts['downsampleMethod'] == 'peak': + n = len(x) / ds + x1 = np.empty((n,2)) + x1[:] = x[:n*ds:ds,np.newaxis] + x = x1.reshape(n*2) + y1 = np.empty((n,2)) + y2 = y[:n*ds].reshape((n, ds)) + y1[:,0] = y2.max(axis=1) + y1[:,1] = y2.min(axis=1) + y = y1.reshape(n*2) + + self.xDisp = x self.yDisp = y #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() @@ -542,6 +643,8 @@ def clear(self): #self.scatters = [] self.xData = None self.yData = None + self.xClean = None + self.yClean = None self.xDisp = None self.yDisp = None self.curve.setData([]) @@ -557,6 +660,14 @@ def scatterClicked(self, plt, points): self.sigClicked.emit(self) self.sigPointsClicked.emit(self, points) + def viewRangeChanged(self): + # view range has changed; re-plot if needed + if self.opts['clipToView'] or self.opts['autoDownsample']: + self.xDisp = self.yDisp = None + self.updateItems() + + + def dataType(obj): if hasattr(obj, '__len__') and len(obj) == 0: diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index c226b9c43b..ff3dc7b34c 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -256,6 +256,11 @@ def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None c.logYCheck.toggled.connect(self.updateLogMode) c.downsampleSpin.valueChanged.connect(self.updateDownsampling) + c.downsampleCheck.toggled.connect(self.updateDownsampling) + c.autoDownsampleCheck.toggled.connect(self.updateDownsampling) + c.subsampleRadio.toggled.connect(self.updateDownsampling) + c.meanRadio.toggled.connect(self.updateDownsampling) + c.clipToViewCheck.toggled.connect(self.updateDownsampling) self.ctrl.avgParamList.itemClicked.connect(self.avgParamListClicked) self.ctrl.averageGroup.toggled.connect(self.avgToggled) @@ -526,7 +531,8 @@ def addItem(self, item, *args, **kargs): (alpha, auto) = self.alphaState() item.setAlpha(alpha, auto) item.setFftMode(self.ctrl.fftCheck.isChecked()) - item.setDownsampling(self.downsampleMode()) + item.setDownsampling(*self.downsampleMode()) + item.setClipToView(self.clipToViewMode()) item.setPointMode(self.pointMode()) ## Hide older plots if needed @@ -568,8 +574,8 @@ def addLine(self, x=None, y=None, z=None, **kwds): :func:`InfiniteLine.__init__() `. Returns the item created. """ - angle = 0 if x is None else 90 - pos = x if x is not None else y + pos = kwds.get('pos', x if x is not None else y) + angle = kwds.get('angle', 0 if x is None else 90) line = InfiniteLine(pos, angle, **kwds) self.addItem(line) if z is not None: @@ -941,23 +947,81 @@ def updateLogMode(self): self.enableAutoRange() self.recomputeAverages() - + def setDownsampling(self, ds=None, auto=None, mode=None): + """Change the default downsampling mode for all PlotDataItems managed by this plot. + + =========== ================================================================= + Arguments + ds (int) Reduce visible plot samples by this factor, or + (bool) To enable/disable downsampling without changing the value. + auto (bool) If True, automatically pick *ds* based on visible range + mode 'subsample': Downsample by taking the first of N samples. + This method is fastest and least accurate. + 'mean': Downsample by taking the mean of N samples. + 'peak': Downsample by drawing a saw wave that follows the min + and max of the original data. This method produces the best + visual representation of the data but is slower. + =========== ================================================================= + """ + if ds is not None: + if ds is False: + self.ctrl.downsampleCheck.setChecked(False) + elif ds is True: + self.ctrl.downsampleCheck.setChecked(True) + else: + self.ctrl.downsampleCheck.setChecked(True) + self.ctrl.downsampleSpin.setValue(ds) + + if auto is not None: + if auto and ds is not False: + self.ctrl.downsampleCheck.setChecked(True) + self.ctrl.autoDownsampleCheck.setChecked(auto) + + if mode is not None: + if mode == 'subsample': + self.ctrl.subsampleRadio.setChecked(True) + elif mode == 'mean': + self.ctrl.meanRadio.setChecked(True) + elif mode == 'peak': + self.ctrl.peakRadio.setChecked(True) + else: + raise ValueError("mode argument must be 'subsample', 'mean', or 'peak'.") + def updateDownsampling(self): - ds = self.downsampleMode() + ds, auto, method = self.downsampleMode() + clip = self.ctrl.clipToViewCheck.isChecked() for c in self.curves: - c.setDownsampling(ds) + c.setDownsampling(ds, auto, method) + c.setClipToView(clip) self.recomputeAverages() - def downsampleMode(self): - if self.ctrl.decimateGroup.isChecked(): - if self.ctrl.manualDecimateRadio.isChecked(): - ds = self.ctrl.downsampleSpin.value() - else: - ds = True + if self.ctrl.downsampleCheck.isChecked(): + ds = self.ctrl.downsampleSpin.value() else: - ds = False - return ds + ds = 1 + + auto = self.ctrl.downsampleCheck.isChecked() and self.ctrl.autoDownsampleCheck.isChecked() + + if self.ctrl.subsampleRadio.isChecked(): + method = 'subsample' + elif self.ctrl.meanRadio.isChecked(): + method = 'mean' + elif self.ctrl.peakRadio.isChecked(): + method = 'peak' + + return ds, auto, method + + def setClipToView(self, clip): + """Set the default clip-to-view mode for all PlotDataItems managed by this plot. + If *clip* is True, then PlotDataItems will attempt to draw only points within the visible + range of the ViewBox.""" + self.ctrl.clipToViewCheck.setChecked(clip) + + def clipToViewMode(self): + return self.ctrl.clipToViewCheck.isChecked() + + def updateDecimation(self): if self.ctrl.maxTracesCheck.isChecked(): diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui index 516ec7217e..dffc62d04f 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui @@ -6,8 +6,8 @@ 0 0 - 258 - 605 + 481 + 840 @@ -16,8 +16,8 @@ - 10 - 200 + 0 + 640 242 182 @@ -46,21 +46,15 @@ - + - 0 - 70 - 242 - 160 + 10 + 140 + 191 + 171 - - Downsample - - - true - 0 @@ -68,57 +62,54 @@ 0 - - - - Manual + + + + Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced. - - true + + Clip to View - - - - 1 - - - 100000 + + + + If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed. - - 1 + + Max Traces: - - + + - Auto - - - false + Downsample - - + + - If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed. + Downsample by drawing a saw wave that follows the min and max of the original data. This method produces the best visual representation of the data but is slower. - Max Traces: + Peak + + + true - + If multiple curves are displayed in this plot, check "Max Traces" and set this value to limit the number of traces that are displayed. - + If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden). @@ -128,6 +119,74 @@ + + + + Downsample by taking the mean of N samples. + + + Mean + + + + + + + Downsample by taking the first of N samples. This method is fastest and least accurate. + + + Subsample + + + + + + + Automatically downsample data based on the visible range. This assumes X values are uniformly spaced. + + + Auto + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 30 + 20 + + + + + + + + Downsample data before plotting. (plot every Nth sample) + + + x + + + 1 + + + 100000 + + + 1 + + + diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py index d34cd2974f..5335ee7678 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './graphicsItems/PlotItem/plotConfigTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui' # -# Created: Sun Sep 9 14:41:32 2012 -# by: PyQt4 UI code generator 4.9.1 +# Created: Mon Jul 1 23:21:08 2013 +# by: PyQt4 UI code generator 4.9.3 # # WARNING! All changes made in this file will be lost! @@ -17,9 +17,9 @@ class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) - Form.resize(258, 605) + Form.resize(481, 840) self.averageGroup = QtGui.QGroupBox(Form) - self.averageGroup.setGeometry(QtCore.QRect(10, 200, 242, 182)) + self.averageGroup.setGeometry(QtCore.QRect(0, 640, 242, 182)) self.averageGroup.setCheckable(True) self.averageGroup.setChecked(False) self.averageGroup.setObjectName(_fromUtf8("averageGroup")) @@ -30,37 +30,50 @@ def setupUi(self, Form): self.avgParamList = QtGui.QListWidget(self.averageGroup) self.avgParamList.setObjectName(_fromUtf8("avgParamList")) self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1) - self.decimateGroup = QtGui.QGroupBox(Form) - self.decimateGroup.setGeometry(QtCore.QRect(0, 70, 242, 160)) - self.decimateGroup.setCheckable(True) + self.decimateGroup = QtGui.QFrame(Form) + self.decimateGroup.setGeometry(QtCore.QRect(10, 140, 191, 171)) self.decimateGroup.setObjectName(_fromUtf8("decimateGroup")) self.gridLayout_4 = QtGui.QGridLayout(self.decimateGroup) self.gridLayout_4.setMargin(0) self.gridLayout_4.setSpacing(0) self.gridLayout_4.setObjectName(_fromUtf8("gridLayout_4")) - self.manualDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.manualDecimateRadio.setChecked(True) - self.manualDecimateRadio.setObjectName(_fromUtf8("manualDecimateRadio")) - self.gridLayout_4.addWidget(self.manualDecimateRadio, 0, 0, 1, 1) - self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup) - self.downsampleSpin.setMinimum(1) - self.downsampleSpin.setMaximum(100000) - self.downsampleSpin.setProperty("value", 1) - self.downsampleSpin.setObjectName(_fromUtf8("downsampleSpin")) - self.gridLayout_4.addWidget(self.downsampleSpin, 0, 1, 1, 1) - self.autoDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.autoDecimateRadio.setChecked(False) - self.autoDecimateRadio.setObjectName(_fromUtf8("autoDecimateRadio")) - self.gridLayout_4.addWidget(self.autoDecimateRadio, 1, 0, 1, 1) + self.clipToViewCheck = QtGui.QCheckBox(self.decimateGroup) + self.clipToViewCheck.setObjectName(_fromUtf8("clipToViewCheck")) + self.gridLayout_4.addWidget(self.clipToViewCheck, 7, 0, 1, 3) self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) self.maxTracesCheck.setObjectName(_fromUtf8("maxTracesCheck")) - self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1) + self.gridLayout_4.addWidget(self.maxTracesCheck, 8, 0, 1, 2) + self.downsampleCheck = QtGui.QCheckBox(self.decimateGroup) + self.downsampleCheck.setObjectName(_fromUtf8("downsampleCheck")) + self.gridLayout_4.addWidget(self.downsampleCheck, 0, 0, 1, 3) + self.peakRadio = QtGui.QRadioButton(self.decimateGroup) + self.peakRadio.setChecked(True) + self.peakRadio.setObjectName(_fromUtf8("peakRadio")) + self.gridLayout_4.addWidget(self.peakRadio, 6, 1, 1, 2) self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) self.maxTracesSpin.setObjectName(_fromUtf8("maxTracesSpin")) - self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 1, 1) + self.gridLayout_4.addWidget(self.maxTracesSpin, 8, 2, 1, 1) self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) self.forgetTracesCheck.setObjectName(_fromUtf8("forgetTracesCheck")) - self.gridLayout_4.addWidget(self.forgetTracesCheck, 3, 0, 1, 2) + self.gridLayout_4.addWidget(self.forgetTracesCheck, 9, 0, 1, 3) + self.meanRadio = QtGui.QRadioButton(self.decimateGroup) + self.meanRadio.setObjectName(_fromUtf8("meanRadio")) + self.gridLayout_4.addWidget(self.meanRadio, 3, 1, 1, 2) + self.subsampleRadio = QtGui.QRadioButton(self.decimateGroup) + self.subsampleRadio.setObjectName(_fromUtf8("subsampleRadio")) + self.gridLayout_4.addWidget(self.subsampleRadio, 2, 1, 1, 2) + self.autoDownsampleCheck = QtGui.QCheckBox(self.decimateGroup) + self.autoDownsampleCheck.setChecked(True) + self.autoDownsampleCheck.setObjectName(_fromUtf8("autoDownsampleCheck")) + self.gridLayout_4.addWidget(self.autoDownsampleCheck, 1, 2, 1, 1) + spacerItem = QtGui.QSpacerItem(30, 20, QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Minimum) + self.gridLayout_4.addItem(spacerItem, 2, 0, 1, 1) + self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup) + self.downsampleSpin.setMinimum(1) + self.downsampleSpin.setMaximum(100000) + self.downsampleSpin.setProperty("value", 1) + self.downsampleSpin.setObjectName(_fromUtf8("downsampleSpin")) + self.gridLayout_4.addWidget(self.downsampleSpin, 1, 1, 1, 1) self.transformGroup = QtGui.QFrame(Form) self.transformGroup.setGeometry(QtCore.QRect(0, 0, 154, 79)) self.transformGroup.setObjectName(_fromUtf8("transformGroup")) @@ -129,14 +142,24 @@ def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) self.averageGroup.setToolTip(QtGui.QApplication.translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None, QtGui.QApplication.UnicodeUTF8)) self.averageGroup.setTitle(QtGui.QApplication.translate("Form", "Average", None, QtGui.QApplication.UnicodeUTF8)) - self.decimateGroup.setTitle(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) - self.manualDecimateRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.autoDecimateRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.clipToViewCheck.setToolTip(QtGui.QApplication.translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8)) + self.clipToViewCheck.setText(QtGui.QApplication.translate("Form", "Clip to View", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleCheck.setText(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) + self.peakRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by drawing a saw wave that follows the min and max of the original data. This method produces the best visual representation of the data but is slower.", None, QtGui.QApplication.UnicodeUTF8)) + self.peakRadio.setText(QtGui.QApplication.translate("Form", "Peak", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesSpin.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) self.forgetTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None, QtGui.QApplication.UnicodeUTF8)) self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", None, QtGui.QApplication.UnicodeUTF8)) + self.meanRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the mean of N samples.", None, QtGui.QApplication.UnicodeUTF8)) + self.meanRadio.setText(QtGui.QApplication.translate("Form", "Mean", None, QtGui.QApplication.UnicodeUTF8)) + self.subsampleRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the first of N samples. This method is fastest and least accurate.", None, QtGui.QApplication.UnicodeUTF8)) + self.subsampleRadio.setText(QtGui.QApplication.translate("Form", "Subsample", None, QtGui.QApplication.UnicodeUTF8)) + self.autoDownsampleCheck.setToolTip(QtGui.QApplication.translate("Form", "Automatically downsample data based on the visible range. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8)) + self.autoDownsampleCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleSpin.setToolTip(QtGui.QApplication.translate("Form", "Downsample data before plotting. (plot every Nth sample)", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleSpin.setSuffix(QtGui.QApplication.translate("Form", "x", None, QtGui.QApplication.UnicodeUTF8)) self.fftCheck.setText(QtGui.QApplication.translate("Form", "Power Spectrum (FFT)", None, QtGui.QApplication.UnicodeUTF8)) self.logXCheck.setText(QtGui.QApplication.translate("Form", "Log X", None, QtGui.QApplication.UnicodeUTF8)) self.logYCheck.setText(QtGui.QApplication.translate("Form", "Log Y", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py index 85b563a700..b8e0b19e39 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './graphicsItems/PlotItem/plotConfigTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui' # -# Created: Sun Sep 9 14:41:32 2012 -# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# Created: Mon Jul 1 23:21:08 2013 +# by: pyside-uic 0.2.13 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! @@ -12,9 +12,9 @@ class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(258, 605) + Form.resize(481, 840) self.averageGroup = QtGui.QGroupBox(Form) - self.averageGroup.setGeometry(QtCore.QRect(10, 200, 242, 182)) + self.averageGroup.setGeometry(QtCore.QRect(0, 640, 242, 182)) self.averageGroup.setCheckable(True) self.averageGroup.setChecked(False) self.averageGroup.setObjectName("averageGroup") @@ -25,37 +25,50 @@ def setupUi(self, Form): self.avgParamList = QtGui.QListWidget(self.averageGroup) self.avgParamList.setObjectName("avgParamList") self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1) - self.decimateGroup = QtGui.QGroupBox(Form) - self.decimateGroup.setGeometry(QtCore.QRect(0, 70, 242, 160)) - self.decimateGroup.setCheckable(True) + self.decimateGroup = QtGui.QFrame(Form) + self.decimateGroup.setGeometry(QtCore.QRect(10, 140, 191, 171)) self.decimateGroup.setObjectName("decimateGroup") self.gridLayout_4 = QtGui.QGridLayout(self.decimateGroup) self.gridLayout_4.setContentsMargins(0, 0, 0, 0) self.gridLayout_4.setSpacing(0) self.gridLayout_4.setObjectName("gridLayout_4") - self.manualDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.manualDecimateRadio.setChecked(True) - self.manualDecimateRadio.setObjectName("manualDecimateRadio") - self.gridLayout_4.addWidget(self.manualDecimateRadio, 0, 0, 1, 1) - self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup) - self.downsampleSpin.setMinimum(1) - self.downsampleSpin.setMaximum(100000) - self.downsampleSpin.setProperty("value", 1) - self.downsampleSpin.setObjectName("downsampleSpin") - self.gridLayout_4.addWidget(self.downsampleSpin, 0, 1, 1, 1) - self.autoDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.autoDecimateRadio.setChecked(False) - self.autoDecimateRadio.setObjectName("autoDecimateRadio") - self.gridLayout_4.addWidget(self.autoDecimateRadio, 1, 0, 1, 1) + self.clipToViewCheck = QtGui.QCheckBox(self.decimateGroup) + self.clipToViewCheck.setObjectName("clipToViewCheck") + self.gridLayout_4.addWidget(self.clipToViewCheck, 7, 0, 1, 3) self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) self.maxTracesCheck.setObjectName("maxTracesCheck") - self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1) + self.gridLayout_4.addWidget(self.maxTracesCheck, 8, 0, 1, 2) + self.downsampleCheck = QtGui.QCheckBox(self.decimateGroup) + self.downsampleCheck.setObjectName("downsampleCheck") + self.gridLayout_4.addWidget(self.downsampleCheck, 0, 0, 1, 3) + self.peakRadio = QtGui.QRadioButton(self.decimateGroup) + self.peakRadio.setChecked(True) + self.peakRadio.setObjectName("peakRadio") + self.gridLayout_4.addWidget(self.peakRadio, 6, 1, 1, 2) self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) self.maxTracesSpin.setObjectName("maxTracesSpin") - self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 1, 1) + self.gridLayout_4.addWidget(self.maxTracesSpin, 8, 2, 1, 1) self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) self.forgetTracesCheck.setObjectName("forgetTracesCheck") - self.gridLayout_4.addWidget(self.forgetTracesCheck, 3, 0, 1, 2) + self.gridLayout_4.addWidget(self.forgetTracesCheck, 9, 0, 1, 3) + self.meanRadio = QtGui.QRadioButton(self.decimateGroup) + self.meanRadio.setObjectName("meanRadio") + self.gridLayout_4.addWidget(self.meanRadio, 3, 1, 1, 2) + self.subsampleRadio = QtGui.QRadioButton(self.decimateGroup) + self.subsampleRadio.setObjectName("subsampleRadio") + self.gridLayout_4.addWidget(self.subsampleRadio, 2, 1, 1, 2) + self.autoDownsampleCheck = QtGui.QCheckBox(self.decimateGroup) + self.autoDownsampleCheck.setChecked(True) + self.autoDownsampleCheck.setObjectName("autoDownsampleCheck") + self.gridLayout_4.addWidget(self.autoDownsampleCheck, 1, 2, 1, 1) + spacerItem = QtGui.QSpacerItem(30, 20, QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Minimum) + self.gridLayout_4.addItem(spacerItem, 2, 0, 1, 1) + self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup) + self.downsampleSpin.setMinimum(1) + self.downsampleSpin.setMaximum(100000) + self.downsampleSpin.setProperty("value", 1) + self.downsampleSpin.setObjectName("downsampleSpin") + self.gridLayout_4.addWidget(self.downsampleSpin, 1, 1, 1, 1) self.transformGroup = QtGui.QFrame(Form) self.transformGroup.setGeometry(QtCore.QRect(0, 0, 154, 79)) self.transformGroup.setObjectName("transformGroup") @@ -124,14 +137,24 @@ def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) self.averageGroup.setToolTip(QtGui.QApplication.translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None, QtGui.QApplication.UnicodeUTF8)) self.averageGroup.setTitle(QtGui.QApplication.translate("Form", "Average", None, QtGui.QApplication.UnicodeUTF8)) - self.decimateGroup.setTitle(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) - self.manualDecimateRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.autoDecimateRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.clipToViewCheck.setToolTip(QtGui.QApplication.translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8)) + self.clipToViewCheck.setText(QtGui.QApplication.translate("Form", "Clip to View", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleCheck.setText(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) + self.peakRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by drawing a saw wave that follows the min and max of the original data. This method produces the best visual representation of the data but is slower.", None, QtGui.QApplication.UnicodeUTF8)) + self.peakRadio.setText(QtGui.QApplication.translate("Form", "Peak", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesSpin.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) self.forgetTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None, QtGui.QApplication.UnicodeUTF8)) self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", None, QtGui.QApplication.UnicodeUTF8)) + self.meanRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the mean of N samples.", None, QtGui.QApplication.UnicodeUTF8)) + self.meanRadio.setText(QtGui.QApplication.translate("Form", "Mean", None, QtGui.QApplication.UnicodeUTF8)) + self.subsampleRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the first of N samples. This method is fastest and least accurate.", None, QtGui.QApplication.UnicodeUTF8)) + self.subsampleRadio.setText(QtGui.QApplication.translate("Form", "Subsample", None, QtGui.QApplication.UnicodeUTF8)) + self.autoDownsampleCheck.setToolTip(QtGui.QApplication.translate("Form", "Automatically downsample data based on the visible range. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8)) + self.autoDownsampleCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleSpin.setToolTip(QtGui.QApplication.translate("Form", "Downsample data before plotting. (plot every Nth sample)", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleSpin.setSuffix(QtGui.QApplication.translate("Form", "x", None, QtGui.QApplication.UnicodeUTF8)) self.fftCheck.setText(QtGui.QApplication.translate("Form", "Power Spectrum (FFT)", None, QtGui.QApplication.UnicodeUTF8)) self.logXCheck.setText(QtGui.QApplication.translate("Form", "Log X", None, QtGui.QApplication.UnicodeUTF8)) self.logYCheck.setText(QtGui.QApplication.translate("Form", "Log Y", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 29bfeaac23..a6a46bf5d0 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -15,7 +15,7 @@ ## Build all symbol paths -Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+']]) +Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+', 'x']]) Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) coords = { @@ -32,6 +32,9 @@ for x,y in c[1:]: Symbols[k].lineTo(x, y) Symbols[k].closeSubpath() +tr = QtGui.QTransform() +tr.rotate(45) +Symbols['x'] = tr.map(Symbols['+']) def drawSymbol(painter, symbol, size, pen, brush): @@ -689,7 +692,8 @@ def generateFragments(self): def setExportMode(self, *args, **kwds): GraphicsObject.setExportMode(self, *args, **kwds) self.invalidate() - + + @pg.debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): #p.setPen(fn.mkPen('r')) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 338cdde4e1..29bd6d231a 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -141,6 +141,12 @@ def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, self.rbScaleBox.hide() self.addItem(self.rbScaleBox, ignoreBounds=True) + ## show target rect for debugging + self.target = QtGui.QGraphicsRectItem(0, 0, 1, 1) + self.target.setPen(fn.mkPen('r')) + self.target.setParentItem(self) + self.target.hide() + self.axHistory = [] # maintain a history of zoom locations self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo" @@ -275,6 +281,9 @@ def addItem(self, item, ignoreBounds=False): """ if item.zValue() < self.zValue(): item.setZValue(self.zValue()+1) + scene = self.scene() + if scene is not None and scene is not item.scene(): + scene.addItem(item) ## Necessary due to Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616 item.setParentItem(self.childGroup) if not ignoreBounds: self.addedItems.append(item) @@ -294,7 +303,7 @@ def clear(self): for i in self.addedItems[:]: self.removeItem(i) for ch in self.childGroup.childItems(): - ch.setParent(None) + ch.setParentItem(None) def resizeEvent(self, ev): #self.setRange(self.range, padding=0) @@ -389,10 +398,28 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru p = (mx-mn) * xpad mn -= p mx += p - if self.state['targetRange'][ax] != [mn, mx]: self.state['targetRange'][ax] = [mn, mx] changed[ax] = True + + aspect = self.state['aspectLocked'] # size ratio / view ratio + if aspect is not False and len(changes) == 1: + ## need to adjust orthogonal target range to match + size = [self.width(), self.height()] + tr1 = self.state['targetRange'][ax] + tr2 = self.state['targetRange'][1-ax] + if size[1] == 0 or aspect == 0: + ratio = 1.0 + else: + ratio = (size[0] / float(size[1])) / aspect + if ax == 0: + ratio = 1.0 / ratio + w = (tr1[1]-tr1[0]) * ratio + d = 0.5 * (w - (tr2[1]-tr2[0])) + self.state['targetRange'][1-ax] = [tr2[0]-d, tr2[1]+d] + + + if any(changed) and disableAutoRange: if all(changed): @@ -406,6 +433,8 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru self.sigStateChanged.emit(self) + self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) + if update: self.updateMatrix(changed) @@ -494,7 +523,7 @@ def scaleBy(self, s=None, center=None, x=None, y=None): scale = Point(scale) if self.state['aspectLocked'] is not False: - scale[0] = self.state['aspectLocked'] * scale[1] + scale[0] = scale[1] vr = self.targetRect() if center is None: @@ -706,6 +735,7 @@ def linkView(self, axis, view): else: if self.autoRangeEnabled()[axis] is False: slot() + self.sigStateChanged.emit(self) @@ -807,13 +837,17 @@ def setAspectLocked(self, lock=True, ratio=1): """ If the aspect ratio is locked, view scaling must always preserve the aspect ratio. By default, the ratio is set to 1; x and y both have the same scaling. - This ratio can be overridden (width/height), or use None to lock in the current ratio. + This ratio can be overridden (xScale/yScale), or use None to lock in the current ratio. """ if not lock: self.state['aspectLocked'] = False else: + rect = self.rect() vr = self.viewRect() - currentRatio = vr.width() / vr.height() + if rect.height() == 0 or vr.width() == 0 or vr.height() == 0: + currentRatio = 1.0 + else: + currentRatio = (rect.width()/float(rect.height())) / (vr.width()/vr.height()) if ratio is None: ratio = currentRatio self.state['aspectLocked'] = ratio @@ -1092,10 +1126,10 @@ def childrenBounds(self, frac=None, orthoRange=(None,None), items=None): 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) or np.isnan(xr).any() or np.isinf(xr).any(): + if xr is None or (xr[0] is None and xr[1] is None) or np.isnan(xr).any() or np.isinf(xr).any(): useX = False xr = (0,0) - if yr is None or yr == (None, None) or np.isnan(yr).any() or np.isinf(yr).any(): + if yr is None or (yr[0] is None and yr[1] is None) or np.isnan(yr).any() or np.isinf(yr).any(): useY = False yr = (0,0) @@ -1194,32 +1228,41 @@ def updateMatrix(self, changed=None): if changed is None: changed = [False, False] changed = list(changed) - #print "udpateMatrix:" - #print " range:", self.range tr = self.targetRect() - bounds = self.rect() #boundingRect() - #print bounds + bounds = self.rect() ## set viewRect, given targetRect and possibly aspect ratio constraint - if self.state['aspectLocked'] is False or bounds.height() == 0: + aspect = self.state['aspectLocked'] + if aspect is False or bounds.height() == 0: self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] else: - viewRatio = bounds.width() / bounds.height() - targetRatio = self.state['aspectLocked'] * tr.width() / tr.height() + ## aspect is (widget w/h) / (view range w/h) + + ## This is the view range aspect ratio we have requested + targetRatio = tr.width() / tr.height() + ## This is the view range aspect ratio we need to obey aspect constraint + viewRatio = (bounds.width() / bounds.height()) / aspect + if targetRatio > viewRatio: - ## target is wider than view - dy = 0.5 * (tr.width() / (self.state['aspectLocked'] * viewRatio) - tr.height()) + ## view range needs to be taller than target + dy = 0.5 * (tr.width() / viewRatio - tr.height()) if dy != 0: changed[1] = True - self.state['viewRange'] = [self.state['targetRange'][0][:], [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]] + self.state['viewRange'] = [ + self.state['targetRange'][0][:], + [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] + ] else: - dx = 0.5 * (tr.height() * viewRatio * self.state['aspectLocked'] - tr.width()) + ## view range needs to be wider than target + dx = 0.5 * (tr.height() * viewRatio - tr.width()) if dx != 0: changed[0] = True - self.state['viewRange'] = [[self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], self.state['targetRange'][1][:]] + self.state['viewRange'] = [ + [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], + self.state['targetRange'][1][:] + ] vr = self.viewRect() - #print " bounds:", bounds if vr.height() == 0 or vr.width() == 0: return scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height()) @@ -1253,6 +1296,12 @@ def paint(self, p, opt, widget): p.setPen(self.border) #p.fillRect(bounds, QtGui.QColor(0, 0, 0)) p.drawPath(bounds) + + #p.setPen(fn.mkPen('r')) + #path = QtGui.QPainterPath() + #path.addRect(self.targetRect()) + #tr = self.mapFromView(path) + #p.drawPath(tr) def updateBackground(self): bg = self.state['background'] diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 0797c75e2b..f55c60dc2c 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -328,6 +328,9 @@ def __mul__(self, b): def __div__(self, b): return self._binop('__div__', b) + def __truediv__(self, b): + return self._binop('__truediv__', b) + def _binop(self, op, b): if isinstance(b, MetaArray): b = b.asarray() diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 6cd65f6ee7..d0d75c1eb5 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -887,6 +887,12 @@ def __sub__(self, *args): def __div__(self, *args): return self._getSpecialAttr('__div__')(*args) + def __truediv__(self, *args): + return self._getSpecialAttr('__truediv__')(*args) + + def __floordiv__(self, *args): + return self._getSpecialAttr('__floordiv__')(*args) + def __mul__(self, *args): return self._getSpecialAttr('__mul__')(*args) @@ -902,6 +908,12 @@ def __isub__(self, *args): def __idiv__(self, *args): return self._getSpecialAttr('__idiv__')(*args, _callSync='off') + def __itruediv__(self, *args): + return self._getSpecialAttr('__itruediv__')(*args, _callSync='off') + + def __ifloordiv__(self, *args): + return self._getSpecialAttr('__ifloordiv__')(*args, _callSync='off') + def __imul__(self, *args): return self._getSpecialAttr('__imul__')(*args, _callSync='off') @@ -914,17 +926,11 @@ def __rshift__(self, *args): def __lshift__(self, *args): return self._getSpecialAttr('__lshift__')(*args) - def __floordiv__(self, *args): - return self._getSpecialAttr('__pow__')(*args) - def __irshift__(self, *args): - return self._getSpecialAttr('__rshift__')(*args, _callSync='off') + return self._getSpecialAttr('__irshift__')(*args, _callSync='off') def __ilshift__(self, *args): - return self._getSpecialAttr('__lshift__')(*args, _callSync='off') - - def __ifloordiv__(self, *args): - return self._getSpecialAttr('__pow__')(*args, _callSync='off') + return self._getSpecialAttr('__ilshift__')(*args, _callSync='off') def __eq__(self, *args): return self._getSpecialAttr('__eq__')(*args) @@ -974,6 +980,12 @@ def __rsub__(self, *args): def __rdiv__(self, *args): return self._getSpecialAttr('__rdiv__')(*args) + def __rfloordiv__(self, *args): + return self._getSpecialAttr('__rfloordiv__')(*args) + + def __rtruediv__(self, *args): + return self._getSpecialAttr('__rtruediv__')(*args) + def __rmul__(self, *args): return self._getSpecialAttr('__rmul__')(*args) @@ -986,9 +998,6 @@ def __rrshift__(self, *args): def __rlshift__(self, *args): return self._getSpecialAttr('__rlshift__')(*args) - def __rfloordiv__(self, *args): - return self._getSpecialAttr('__rpow__')(*args) - def __rand__(self, *args): return self._getSpecialAttr('__rand__')(*args) diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index fe785e0458..e9e24dd79d 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -190,10 +190,15 @@ def updatePlot(self): for ax in [0,1]: if not enum[ax]: continue - for i in range(int(xy[ax].max())+1): + imax = int(xy[ax].max()) if len(xy[ax]) > 0 else 0 + for i in range(imax+1): keymask = xy[ax] == i scatter = pg.pseudoScatter(xy[1-ax][keymask], bidir=True) - scatter *= 0.2 / np.abs(scatter).max() + if len(scatter) == 0: + continue + smax = np.abs(scatter).max() + if smax != 0: + scatter *= 0.2 / smax xy[ax][keymask] += scatter if self.scatterPlot is not None: From f2d09911029ea3ee649978eb6416fa771369ab2f Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Thu, 4 Jul 2013 05:52:16 +0800 Subject: [PATCH 057/121] Minor fixes for py3k --- examples/__main__.py | 13 ++++++------- pyqtgraph/Point.py | 2 +- pyqtgraph/functions.py | 7 ++++--- pyqtgraph/multiprocess/remoteproxy.py | 6 +++--- pyqtgraph/opengl/GLViewWidget.py | 4 ++-- pyqtgraph/opengl/__init__.py | 4 ++-- pyqtgraph/opengl/items/GLLinePlotItem.py | 2 +- pyqtgraph/opengl/items/GLScatterPlotItem.py | 2 +- pyqtgraph/opengl/items/GLSurfacePlotItem.py | 4 ++-- pyqtgraph/opengl/shaders.py | 4 ++-- pyqtgraph/widgets/RemoteGraphicsView.py | 2 +- 11 files changed, 25 insertions(+), 25 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index 2ecc810df0..4aa23e8ed7 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -1,13 +1,12 @@ import sys, os, subprocess, time -try: - from . import initExample -except ValueError: - #__package__ = os.path.split(os.path.dirname(__file__))[-1] - sys.excepthook(*sys.exc_info()) - print("examples/ can not be executed as a script; please run 'python -m examples' instead.") - sys.exit(1) +if __name__ == "__main__" and (__package__ is None or __package__==''): + parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, parent_dir) + import examples + __package__ = "examples" +from . import initExample from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE if USE_PYSIDE: diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index 682f19f783..4d04f01c11 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -152,4 +152,4 @@ def copy(self): return Point(self) def toQPoint(self): - return QtCore.QPoint(*self) \ No newline at end of file + return QtCore.QPoint(*self) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 5a78616d3c..1c179995fd 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -5,6 +5,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation. """ +from __future__ import division from .python2_3 import asUnicode Colors = { 'b': (0,0,255,255), @@ -1864,9 +1865,9 @@ def isosurface(data, level): for i in [0,1,2]: vim = vertexInds[:,3] == i vi = vertexInds[vim, :3] - viFlat = (vi * (np.array(data.strides[:3]) / data.itemsize)[np.newaxis,:]).sum(axis=1) + viFlat = (vi * (np.array(data.strides[:3]) // data.itemsize)[np.newaxis,:]).sum(axis=1) v1 = dataFlat[viFlat] - v2 = dataFlat[viFlat + data.strides[i]/data.itemsize] + v2 = dataFlat[viFlat + data.strides[i]//data.itemsize] vertexes[vim,i] += (level-v1) / (v2-v1) ### compute the set of vertex indexes for each face. @@ -1892,7 +1893,7 @@ def isosurface(data, level): #p = debug.Profiler('isosurface', disabled=False) ## this helps speed up an indexing operation later on - cs = np.array(cutEdges.strides)/cutEdges.itemsize + cs = np.array(cutEdges.strides)//cutEdges.itemsize cutEdges = cutEdges.flatten() ## this, strangely, does not seem to help. diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index f33ebc8308..7622b6e7b9 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -932,9 +932,9 @@ def __irshift__(self, *args): def __ilshift__(self, *args): return self._getSpecialAttr('__ilshift__')(*args, _callSync='off') - def __eq__(self, *args): - return self._getSpecialAttr('__eq__')(*args) - + #def __eq__(self, *args): + # return self._getSpecialAttr('__eq__')(*args) + def __ne__(self, *args): return self._getSpecialAttr('__ne__')(*args) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 12984c8679..1cd3a047fd 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -139,7 +139,7 @@ def drawItemTree(self, item=None): else: items = item.childItems() items.append(item) - items.sort(lambda a,b: cmp(a.depthValue(), b.depthValue())) + items.sort(key=lambda x: x.depthValue()) for i in items: if not i.visible(): continue @@ -154,7 +154,7 @@ def drawItemTree(self, item=None): ver = glGetString(GL_VERSION) if ver is not None: ver = ver.split()[0] - if int(ver.split('.')[0]) < 2: + if int(ver.split(b'.')[0]) < 2: print(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver) else: print(msg) diff --git a/pyqtgraph/opengl/__init__.py b/pyqtgraph/opengl/__init__.py index 199c372c1b..5345e1872f 100644 --- a/pyqtgraph/opengl/__init__.py +++ b/pyqtgraph/opengl/__init__.py @@ -23,8 +23,8 @@ importAll('items', globals(), locals()) \ -from MeshData import MeshData +from .MeshData import MeshData ## for backward compatibility: #MeshData.MeshData = MeshData ## breaks autodoc. -import shaders +from . import shaders diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py index bb5ce2f6e3..888af6643d 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -83,7 +83,7 @@ def paint(self): glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); - glDrawArrays(GL_LINE_STRIP, 0, self.pos.size / self.pos.shape[-1]) + glDrawArrays(GL_LINE_STRIP, 0, int(self.pos.size / self.pos.shape[-1])) finally: glDisableClientState(GL_VERTEX_ARRAY) diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index e9bbde648c..b02a9dda36 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -146,7 +146,7 @@ def paint(self): else: glNormal3f(self.size, 0, 0) ## vertex shader uses norm.x to determine point size #glPointSize(self.size) - glDrawArrays(GL_POINTS, 0, pos.size / pos.shape[-1]) + glDrawArrays(GL_POINTS, 0, int(pos.size / pos.shape[-1])) finally: glDisableClientState(GL_NORMAL_ARRAY) glDisableClientState(GL_VERTEX_ARRAY) diff --git a/pyqtgraph/opengl/items/GLSurfacePlotItem.py b/pyqtgraph/opengl/items/GLSurfacePlotItem.py index 46c54fc288..88d50facbb 100644 --- a/pyqtgraph/opengl/items/GLSurfacePlotItem.py +++ b/pyqtgraph/opengl/items/GLSurfacePlotItem.py @@ -1,5 +1,5 @@ from OpenGL.GL import * -from GLMeshItem import GLMeshItem +from .GLMeshItem import GLMeshItem from .. MeshData import MeshData from pyqtgraph.Qt import QtGui import pyqtgraph as pg @@ -136,4 +136,4 @@ def generateFaces(self): start = row * cols * 2 faces[start:start+cols] = rowtemplate1 + row * (cols+1) faces[start+cols:start+(cols*2)] = rowtemplate2 + row * (cols+1) - self._faces = faces \ No newline at end of file + self._faces = faces diff --git a/pyqtgraph/opengl/shaders.py b/pyqtgraph/opengl/shaders.py index b1652850d6..e8ca28d994 100644 --- a/pyqtgraph/opengl/shaders.py +++ b/pyqtgraph/opengl/shaders.py @@ -354,7 +354,7 @@ def __exit__(self, *args): def uniform(self, name): """Return the location integer for a uniform variable in this program""" - return glGetUniformLocation(self.program(), name) + return glGetUniformLocation(self.program(), bytes(name,'utf_8')) #def uniformBlockInfo(self, blockName): #blockIndex = glGetUniformBlockIndex(self.program(), blockName) @@ -390,4 +390,4 @@ def __enter__(self): ## bind buffer to the same binding point glBindBufferBase(GL_UNIFORM_BUFFER, bindPoint, buf) -initShaders() \ No newline at end of file +initShaders() diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index d1a21e9727..80f0fb4b5e 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -128,7 +128,7 @@ def __init__(self, *args, **kwds): 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) + self.shmFile.write(b'\x00' * (mmap.PAGESIZE+1)) fd = self.shmFile.fileno() self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE) atexit.register(self.close) From 934e50ad551d3029c4b3e4bb0b38cd58944ead7e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 4 Jul 2013 08:34:18 -0400 Subject: [PATCH 058/121] Added python3 support for efficient method in arrayToQPath --- pyqtgraph/functions.py | 147 ++++++++++++++++++++--------------------- 1 file changed, 73 insertions(+), 74 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 5a78616d3c..2fc1a8a269 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1077,14 +1077,29 @@ def arrayToQPath(x, y, connect='all'): 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, + + ## 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]) - + #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') + ## Speed this up using >> operator ## Format is: ## numVerts(i4) 0(i4) @@ -1094,76 +1109,60 @@ def arrayToQPath(x, y, connect='all'): ## 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 - - ## Avoiding this method because QByteArray(str) leaks memory in PySide - #buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here - - path.strn = arr.data[12:lastInd+4] # make sure data doesn't run away - buf = QtCore.QByteArray.fromRawData(path.strn) - #prof.mark('create buffer') - ds = QtCore.QDataStream(buf) - - ds >> path - #prof.mark('load') - - #prof.finish() + 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') + byteview = arr.view(dtype=np.ubyte) + byteview[:12] = 0 + byteview.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: - ## 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') - + raise Exception('connect argument must be "all", "pairs", or array') + + #prof.mark('fill array') + # write last 0 + lastInd = 20*(n+1) + byteview.data[lastInd:lastInd+4] = struct.pack('>i', 0) + #prof.mark('footer') + # create datastream object and stream into path + + ## Avoiding this method because QByteArray(str) leaks memory in PySide + #buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here + + path.strn = byteview.data[12:lastInd+4] # make sure data doesn't run away + try: + buf = QtCore.QByteArray.fromRawData(path.strn) + except TypeError: + buf = QtCore.QByteArray(bytes(path.strn)) + #prof.mark('create buffer') + ds = QtCore.QDataStream(buf) + + ds >> path + #prof.mark('load') + + #prof.finish() + return path #def isosurface(data, level): From a20e732f650a9d6dd9bbcbd2e6dfee624953583a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 4 Jul 2013 11:21:50 -0400 Subject: [PATCH 059/121] Added GL picking, matrix retrieval methods --- pyqtgraph/opengl/GLViewWidget.py | 67 +++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 12984c8679..d8f70055f0 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -80,23 +80,26 @@ def resizeGL(self, w, h): #self.update() def setProjection(self, region=None): + m = self.projectionMatrix(region) + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + a = np.array(m.copyDataTo()).reshape((4,4)) + glMultMatrixf(a.transpose()) + + def projectionMatrix(self, region=None): # Xw = (Xnd + 1) * width/2 + X if region is None: region = (0, 0, self.width(), self.height()) - ## Create the projection matrix - glMatrixMode(GL_PROJECTION) - glLoadIdentity() - #w = self.width() - #h = self.height() + x0, y0, w, h = self.getViewport() dist = self.opts['distance'] fov = self.opts['fov'] nearClip = dist * 0.001 farClip = dist * 1000. - + r = nearClip * np.tan(fov * 0.5 * np.pi / 180.) t = r * h / w - + # convert screen coordinates (region) to normalized device coordinates # Xnd = (Xw - X0) * 2/width - 1 ## Note that X0 and width in these equations must be the values used in viewport @@ -104,21 +107,46 @@ def setProjection(self, region=None): right = r * ((region[0]+region[2]-x0) * (2.0/w) - 1) bottom = t * ((region[1]-y0) * (2.0/h) - 1) top = t * ((region[1]+region[3]-y0) * (2.0/h) - 1) - - glFrustum( left, right, bottom, top, nearClip, farClip) - #glFrustum(-r, r, -t, t, nearClip, farClip) + + tr = QtGui.QMatrix4x4() + tr.frustum(left, right, bottom, top, nearClip, farClip) + return tr def setModelview(self): glMatrixMode(GL_MODELVIEW) glLoadIdentity() - glTranslatef( 0.0, 0.0, -self.opts['distance']) - glRotatef(self.opts['elevation']-90, 1, 0, 0) - glRotatef(self.opts['azimuth']+90, 0, 0, -1) + m = self.viewMatrix() + a = np.array(m.copyDataTo()).reshape((4,4)) + glMultMatrixf(a.transpose()) + + def viewMatrix(self): + tr = QtGui.QMatrix4x4() + tr.translate( 0.0, 0.0, -self.opts['distance']) + tr.rotate(self.opts['elevation']-90, 1, 0, 0) + tr.rotate(self.opts['azimuth']+90, 0, 0, -1) center = self.opts['center'] - glTranslatef(-center.x(), -center.y(), -center.z()) + tr.translate(-center.x(), -center.y(), -center.z()) + return tr + + def itemsAt(self, region=None): + #buf = np.zeros(100000, dtype=np.uint) + buf = glSelectBuffer(100000) + try: + glRenderMode(GL_SELECT) + glInitNames() + glPushName(0) + self._itemNames = {} + self.paintGL(region=region, useItemNames=True) + + finally: + hits = glRenderMode(GL_RENDER) + + items = [(h.near, h.names[0]) for h in hits] + items.sort(key=lambda i: i[0]) + return [self._itemNames[i[1]] for i in items] - def paintGL(self, region=None, viewport=None): + def paintGL(self, region=None, viewport=None, useItemNames=False): """ viewport specifies the arguments to glViewport. If None, then we use self.opts['viewport'] region specifies the sub-region of self.opts['viewport'] that should be rendered. @@ -131,9 +159,9 @@ def paintGL(self, region=None, viewport=None): self.setProjection(region=region) self.setModelview() glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) - self.drawItemTree() + self.drawItemTree(useItemNames=useItemNames) - def drawItemTree(self, item=None): + def drawItemTree(self, item=None, useItemNames=False): if item is None: items = [x for x in self.items if x.parentItem() is None] else: @@ -146,6 +174,9 @@ def drawItemTree(self, item=None): if i is item: try: glPushAttrib(GL_ALL_ATTRIB_BITS) + if useItemNames: + glLoadName(id(i)) + self._itemNames[id(i)] = i i.paint() except: import pyqtgraph.debug @@ -168,7 +199,7 @@ def drawItemTree(self, item=None): tr = i.transform() a = np.array(tr.copyDataTo()).reshape((4,4)) glMultMatrixf(a.transpose()) - self.drawItemTree(i) + self.drawItemTree(i, useItemNames=useItemNames) finally: glMatrixMode(GL_MODELVIEW) glPopMatrix() From c0eec1862cf238e86fd6bb592dee937651eba1c9 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Fri, 5 Jul 2013 00:08:41 +0800 Subject: [PATCH 060/121] revert mess create by git-bzr From 7cd3e663f9f686bcd4d68880593b72c8dd6a1b24 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 10 Jul 2013 00:02:16 -0400 Subject: [PATCH 061/121] experimental GL video widget temporary fix for text parameter ignoring expanded option Don't use os.EX_OK in pg.exit() --- examples/VideoSpeedTest.py | 5 ++ examples/VideoTemplate.ui | 73 +++++++++++++++------ examples/VideoTemplate_pyqt.py | 65 +++++++++++++------ examples/VideoTemplate_pyside.py | 65 +++++++++++++------ examples/parametertree.py | 15 +++-- pyqtgraph/__init__.py | 2 +- pyqtgraph/graphicsItems/ROI.py | 8 ++- pyqtgraph/parametertree/parameterTypes.py | 8 ++- pyqtgraph/widgets/RawImageWidget.py | 78 +++++++++++++++++++---- 9 files changed, 243 insertions(+), 76 deletions(-) diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index dd392189a6..d7a4e1e0e4 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -130,8 +130,13 @@ def update(): if ui.rawRadio.isChecked(): ui.rawImg.setImage(data[ptr%data.shape[0]], lut=useLut, levels=useScale) + ui.stack.setCurrentIndex(1) + elif ui.rawGLRadio.isChecked(): + ui.rawGLImg.setImage(data[ptr%data.shape[0]], lut=useLut, levels=useScale) + ui.stack.setCurrentIndex(2) else: img.setImage(data[ptr%data.shape[0]], autoLevels=False, levels=useScale, lut=useLut) + ui.stack.setCurrentIndex(0) #img.setImage(data[ptr%data.shape[0]], autoRange=False) ptr += 1 diff --git a/examples/VideoTemplate.ui b/examples/VideoTemplate.ui index 3dddb9285d..d73b0dc94f 100644 --- a/examples/VideoTemplate.ui +++ b/examples/VideoTemplate.ui @@ -6,8 +6,8 @@ 0 0 - 985 - 674 + 695 + 798 @@ -17,33 +17,62 @@ - - - - - 0 - 0 - - - - - - - - + - RawImageWidget (unscaled; faster) + RawImageWidget true - + - GraphicsView + ImageItem (scaled; slower) + GraphicsView + ImageItem + + + + + + + 2 + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + RawGLImageWidget @@ -250,6 +279,12 @@ QDoubleSpinBox
pyqtgraph
+ + RawImageGLWidget + QWidget +
pyqtgraph.widgets.RawImageWidget
+ 1 +
diff --git a/examples/VideoTemplate_pyqt.py b/examples/VideoTemplate_pyqt.py index c3430e2dc7..f61a5e4658 100644 --- a/examples/VideoTemplate_pyqt.py +++ b/examples/VideoTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './examples/VideoTemplate.ui' +# Form implementation generated from reading ui file './VideoTemplate.ui' # -# Created: Sun Nov 4 18:24:20 2012 -# by: PyQt4 UI code generator 4.9.1 +# Created: Tue Jul 9 23:38:17 2013 +# by: PyQt4 UI code generator 4.9.3 # # WARNING! All changes made in this file will be lost! @@ -17,31 +17,55 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName(_fromUtf8("MainWindow")) - MainWindow.resize(985, 674) + MainWindow.resize(695, 798) self.centralwidget = QtGui.QWidget(MainWindow) self.centralwidget.setObjectName(_fromUtf8("centralwidget")) self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget) self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) self.gridLayout = QtGui.QGridLayout() self.gridLayout.setObjectName(_fromUtf8("gridLayout")) - self.rawImg = RawImageWidget(self.centralwidget) + self.rawRadio = QtGui.QRadioButton(self.centralwidget) + self.rawRadio.setChecked(True) + self.rawRadio.setObjectName(_fromUtf8("rawRadio")) + self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1) + self.gfxRadio = QtGui.QRadioButton(self.centralwidget) + self.gfxRadio.setObjectName(_fromUtf8("gfxRadio")) + self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1) + self.stack = QtGui.QStackedWidget(self.centralwidget) + self.stack.setObjectName(_fromUtf8("stack")) + self.page = QtGui.QWidget() + self.page.setObjectName(_fromUtf8("page")) + self.gridLayout_3 = QtGui.QGridLayout(self.page) + self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) + self.graphicsView = GraphicsView(self.page) + self.graphicsView.setObjectName(_fromUtf8("graphicsView")) + self.gridLayout_3.addWidget(self.graphicsView, 0, 0, 1, 1) + self.stack.addWidget(self.page) + self.page_2 = QtGui.QWidget() + self.page_2.setObjectName(_fromUtf8("page_2")) + self.gridLayout_4 = QtGui.QGridLayout(self.page_2) + self.gridLayout_4.setObjectName(_fromUtf8("gridLayout_4")) + self.rawImg = RawImageWidget(self.page_2) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth()) self.rawImg.setSizePolicy(sizePolicy) self.rawImg.setObjectName(_fromUtf8("rawImg")) - self.gridLayout.addWidget(self.rawImg, 0, 0, 1, 1) - self.graphicsView = GraphicsView(self.centralwidget) - self.graphicsView.setObjectName(_fromUtf8("graphicsView")) - self.gridLayout.addWidget(self.graphicsView, 0, 1, 1, 1) - self.rawRadio = QtGui.QRadioButton(self.centralwidget) - self.rawRadio.setChecked(True) - self.rawRadio.setObjectName(_fromUtf8("rawRadio")) - self.gridLayout.addWidget(self.rawRadio, 1, 0, 1, 1) - self.gfxRadio = QtGui.QRadioButton(self.centralwidget) - self.gfxRadio.setObjectName(_fromUtf8("gfxRadio")) - self.gridLayout.addWidget(self.gfxRadio, 1, 1, 1, 1) + self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1) + self.stack.addWidget(self.page_2) + self.page_3 = QtGui.QWidget() + self.page_3.setObjectName(_fromUtf8("page_3")) + self.gridLayout_5 = QtGui.QGridLayout(self.page_3) + self.gridLayout_5.setObjectName(_fromUtf8("gridLayout_5")) + self.rawGLImg = RawImageGLWidget(self.page_3) + self.rawGLImg.setObjectName(_fromUtf8("rawGLImg")) + self.gridLayout_5.addWidget(self.rawGLImg, 0, 0, 1, 1) + self.stack.addWidget(self.page_3) + self.gridLayout.addWidget(self.stack, 0, 0, 1, 1) + self.rawGLRadio = QtGui.QRadioButton(self.centralwidget) + self.rawGLRadio.setObjectName(_fromUtf8("rawGLRadio")) + self.gridLayout.addWidget(self.rawGLRadio, 4, 0, 1, 1) self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4) self.label = QtGui.QLabel(self.centralwidget) self.label.setObjectName(_fromUtf8("label")) @@ -130,12 +154,14 @@ def setupUi(self, MainWindow): MainWindow.setCentralWidget(self.centralwidget) self.retranslateUi(MainWindow) + self.stack.setCurrentIndex(2) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8)) - self.rawRadio.setText(QtGui.QApplication.translate("MainWindow", "RawImageWidget (unscaled; faster)", None, QtGui.QApplication.UnicodeUTF8)) - self.gfxRadio.setText(QtGui.QApplication.translate("MainWindow", "GraphicsView + ImageItem (scaled; slower)", None, QtGui.QApplication.UnicodeUTF8)) + self.rawRadio.setText(QtGui.QApplication.translate("MainWindow", "RawImageWidget", None, QtGui.QApplication.UnicodeUTF8)) + self.gfxRadio.setText(QtGui.QApplication.translate("MainWindow", "GraphicsView + ImageItem", None, QtGui.QApplication.UnicodeUTF8)) + self.rawGLRadio.setText(QtGui.QApplication.translate("MainWindow", "RawGLImageWidget", None, QtGui.QApplication.UnicodeUTF8)) self.label.setText(QtGui.QApplication.translate("MainWindow", "Data type", None, QtGui.QApplication.UnicodeUTF8)) self.dtypeCombo.setItemText(0, QtGui.QApplication.translate("MainWindow", "uint8", None, QtGui.QApplication.UnicodeUTF8)) self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8)) @@ -150,4 +176,5 @@ def retranslateUi(self, MainWindow): self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8)) self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget +from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget +from pyqtgraph import GradientWidget, SpinBox, GraphicsView, RawImageWidget diff --git a/examples/VideoTemplate_pyside.py b/examples/VideoTemplate_pyside.py index d19e0f23fd..d0db5effd9 100644 --- a/examples/VideoTemplate_pyside.py +++ b/examples/VideoTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './examples/VideoTemplate.ui' +# Form implementation generated from reading ui file './VideoTemplate.ui' # -# Created: Sun Nov 4 18:24:21 2012 -# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# Created: Tue Jul 9 23:38:19 2013 +# by: pyside-uic 0.2.13 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! @@ -12,31 +12,55 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") - MainWindow.resize(985, 674) + MainWindow.resize(695, 798) self.centralwidget = QtGui.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget) self.gridLayout_2.setObjectName("gridLayout_2") self.gridLayout = QtGui.QGridLayout() self.gridLayout.setObjectName("gridLayout") - self.rawImg = RawImageWidget(self.centralwidget) + self.rawRadio = QtGui.QRadioButton(self.centralwidget) + self.rawRadio.setChecked(True) + self.rawRadio.setObjectName("rawRadio") + self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1) + self.gfxRadio = QtGui.QRadioButton(self.centralwidget) + self.gfxRadio.setObjectName("gfxRadio") + self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1) + self.stack = QtGui.QStackedWidget(self.centralwidget) + self.stack.setObjectName("stack") + self.page = QtGui.QWidget() + self.page.setObjectName("page") + self.gridLayout_3 = QtGui.QGridLayout(self.page) + self.gridLayout_3.setObjectName("gridLayout_3") + self.graphicsView = GraphicsView(self.page) + self.graphicsView.setObjectName("graphicsView") + self.gridLayout_3.addWidget(self.graphicsView, 0, 0, 1, 1) + self.stack.addWidget(self.page) + self.page_2 = QtGui.QWidget() + self.page_2.setObjectName("page_2") + self.gridLayout_4 = QtGui.QGridLayout(self.page_2) + self.gridLayout_4.setObjectName("gridLayout_4") + self.rawImg = RawImageWidget(self.page_2) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth()) self.rawImg.setSizePolicy(sizePolicy) self.rawImg.setObjectName("rawImg") - self.gridLayout.addWidget(self.rawImg, 0, 0, 1, 1) - self.graphicsView = GraphicsView(self.centralwidget) - self.graphicsView.setObjectName("graphicsView") - self.gridLayout.addWidget(self.graphicsView, 0, 1, 1, 1) - self.rawRadio = QtGui.QRadioButton(self.centralwidget) - self.rawRadio.setChecked(True) - self.rawRadio.setObjectName("rawRadio") - self.gridLayout.addWidget(self.rawRadio, 1, 0, 1, 1) - self.gfxRadio = QtGui.QRadioButton(self.centralwidget) - self.gfxRadio.setObjectName("gfxRadio") - self.gridLayout.addWidget(self.gfxRadio, 1, 1, 1, 1) + self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1) + self.stack.addWidget(self.page_2) + self.page_3 = QtGui.QWidget() + self.page_3.setObjectName("page_3") + self.gridLayout_5 = QtGui.QGridLayout(self.page_3) + self.gridLayout_5.setObjectName("gridLayout_5") + self.rawGLImg = RawImageGLWidget(self.page_3) + self.rawGLImg.setObjectName("rawGLImg") + self.gridLayout_5.addWidget(self.rawGLImg, 0, 0, 1, 1) + self.stack.addWidget(self.page_3) + self.gridLayout.addWidget(self.stack, 0, 0, 1, 1) + self.rawGLRadio = QtGui.QRadioButton(self.centralwidget) + self.rawGLRadio.setObjectName("rawGLRadio") + self.gridLayout.addWidget(self.rawGLRadio, 4, 0, 1, 1) self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4) self.label = QtGui.QLabel(self.centralwidget) self.label.setObjectName("label") @@ -125,12 +149,14 @@ def setupUi(self, MainWindow): MainWindow.setCentralWidget(self.centralwidget) self.retranslateUi(MainWindow) + self.stack.setCurrentIndex(2) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8)) - self.rawRadio.setText(QtGui.QApplication.translate("MainWindow", "RawImageWidget (unscaled; faster)", None, QtGui.QApplication.UnicodeUTF8)) - self.gfxRadio.setText(QtGui.QApplication.translate("MainWindow", "GraphicsView + ImageItem (scaled; slower)", None, QtGui.QApplication.UnicodeUTF8)) + self.rawRadio.setText(QtGui.QApplication.translate("MainWindow", "RawImageWidget", None, QtGui.QApplication.UnicodeUTF8)) + self.gfxRadio.setText(QtGui.QApplication.translate("MainWindow", "GraphicsView + ImageItem", None, QtGui.QApplication.UnicodeUTF8)) + self.rawGLRadio.setText(QtGui.QApplication.translate("MainWindow", "RawGLImageWidget", None, QtGui.QApplication.UnicodeUTF8)) self.label.setText(QtGui.QApplication.translate("MainWindow", "Data type", None, QtGui.QApplication.UnicodeUTF8)) self.dtypeCombo.setItemText(0, QtGui.QApplication.translate("MainWindow", "uint8", None, QtGui.QApplication.UnicodeUTF8)) self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8)) @@ -145,4 +171,5 @@ def retranslateUi(self, MainWindow): self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8)) self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget +from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget +from pyqtgraph import GradientWidget, SpinBox, GraphicsView, RawImageWidget diff --git a/examples/parametertree.py b/examples/parametertree.py index 4c5d7275e8..c600d1be70 100644 --- a/examples/parametertree.py +++ b/examples/parametertree.py @@ -139,14 +139,19 @@ def restore(): ## Create two ParameterTree widgets, both accessing the same data t = ParameterTree() t.setParameters(p, showTop=False) -t.show() t.setWindowTitle('pyqtgraph example: Parameter Tree') -t.resize(400,800) t2 = ParameterTree() t2.setParameters(p, showTop=False) -t2.show() -t2.resize(400,800) - + +win = QtGui.QWidget() +layout = QtGui.QGridLayout() +win.setLayout(layout) +layout.addWidget(QtGui.QLabel("These are two views of the same data. They should always display the same values."), 0, 0, 1, 2) +layout.addWidget(t, 1, 0, 1, 1) +layout.addWidget(t2, 1, 1, 1, 1) +win.show() +win.resize(800,800) + ## test save/restore s = p.saveState() p.restoreState(s) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index d83e0ec001..b1a05835ee 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -255,7 +255,7 @@ def exit(): ## close file handles os.closerange(3, 4096) ## just guessing on the maximum descriptor count.. - os._exit(os.EX_OK) + os._exit(0) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index a5e25a2f73..033aab42a8 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -49,7 +49,13 @@ class ROI(GraphicsObject): sigRegionChanged Emitted any time the position of the ROI changes, including while it is being dragged by the user. sigHoverEvent Emitted when the mouse hovers over the ROI. - sigClicked Emitted when the user clicks on the ROI + sigClicked Emitted when the user clicks on the ROI. + Note that clicking is disabled by default to prevent + stealing clicks from objects behind the ROI. To + enable clicking, call + roi.setAcceptedMouseButtons(QtCore.Qt.LeftButton). + See QtGui.QGraphicsItem documentation for more + details. sigRemoveRequested Emitted when the user selects 'remove' from the ROI's context menu (if available). ----------------------- ---------------------------------------------------- diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 28e1e618cb..51f0be6425 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -619,9 +619,15 @@ def __init__(self, param, depth): self.addChild(self.subItem) def treeWidgetChanged(self): + ## TODO: fix so that superclass method can be called + ## (WidgetParameter should just natively support this style) + #WidgetParameterItem.treeWidgetChanged(self) self.treeWidget().setFirstItemColumnSpanned(self.subItem, True) self.treeWidget().setItemWidget(self.subItem, 0, self.textBox) - self.setExpanded(True) + + # for now, these are copied from ParameterItem.treeWidgetChanged + self.setHidden(not self.param.opts.get('visible', True)) + self.setExpanded(self.param.opts.get('expanded', True)) def makeWidget(self): self.textBox = QtGui.QTextEdit() diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py index ea5c98a012..a780f46337 100644 --- a/pyqtgraph/widgets/RawImageWidget.py +++ b/pyqtgraph/widgets/RawImageWidget.py @@ -11,8 +11,8 @@ class RawImageWidget(QtGui.QWidget): """ Widget optimized for very fast video display. - Generally using an ImageItem inside GraphicsView is fast enough, - but if you need even more performance, this widget is about as fast as it gets (but only in unscaled mode). + Generally using an ImageItem inside GraphicsView is fast enough. + On some systems this may provide faster video. See the VideoSpeedTest example for benchmarking. """ def __init__(self, parent=None, scaled=False): """ @@ -59,26 +59,82 @@ def paintEvent(self, ev): p.end() if HAVE_OPENGL: + from OpenGL.GL import * class RawImageGLWidget(QtOpenGL.QGLWidget): """ Similar to RawImageWidget, but uses a GL widget to do all drawing. - Generally this will be about as fast as using GraphicsView + ImageItem, - but performance may vary on some platforms. + Perfomance varies between platforms; see examples/VideoSpeedTest for benchmarking. """ def __init__(self, parent=None, scaled=False): QtOpenGL.QGLWidget.__init__(self, parent=None) self.scaled = scaled self.image = None + self.uploaded = False + self.smooth = False + self.opts = None - def setImage(self, img): - self.image = fn.makeQImage(img) + def setImage(self, img, *args, **kargs): + """ + img must be ndarray of shape (x,y), (x,y,3), or (x,y,4). + Extra arguments are sent to functions.makeARGB + """ + self.opts = (img, args, kargs) + self.image = None + self.uploaded = False self.update() - def paintEvent(self, ev): + def initializeGL(self): + self.texture = glGenTextures(1) + + def uploadTexture(self): + glEnable(GL_TEXTURE_2D) + glBindTexture(GL_TEXTURE_2D, self.texture) + if self.smooth: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + else: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) + #glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER) + shape = self.image.shape + + ### Test texture dimensions first + #glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, None) + #if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0: + #raise Exception("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % shape[:2]) + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, self.image.transpose((1,0,2))) + glDisable(GL_TEXTURE_2D) + + def paintGL(self): if self.image is None: - return - p = QtGui.QPainter(self) - p.drawImage(self.rect(), self.image) - p.end() + if self.opts is None: + return + img, args, kwds = self.opts + kwds['useRGBA'] = True + self.image, alpha = fn.makeARGB(img, *args, **kwds) + + if not self.uploaded: + self.uploadTexture() + + glViewport(0, 0, self.width(), self.height()) + glEnable(GL_TEXTURE_2D) + glBindTexture(GL_TEXTURE_2D, self.texture) + glColor4f(1,1,1,1) + + glBegin(GL_QUADS) + glTexCoord2f(0,0) + glVertex3f(-1,-1,0) + glTexCoord2f(1,0) + glVertex3f(1, -1, 0) + glTexCoord2f(1,1) + glVertex3f(1, 1, 0) + glTexCoord2f(0,1) + glVertex3f(-1, 1, 0) + glEnd() + glDisable(GL_TEXTURE_3D) + From 5a2b9462055fefda8faaac5eb139d62ec09bc21f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 10 Jul 2013 14:30:16 -0400 Subject: [PATCH 062/121] ViewBox bugfixes: - drag rect now has large ZValue - fixed view linking with inverted y axis --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index ea04bb1663..7657a6bdc9 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -139,6 +139,7 @@ def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100)) + self.rbScaleBox.setZValue(1e9) self.rbScaleBox.hide() self.addItem(self.rbScaleBox, ignoreBounds=True) @@ -792,12 +793,15 @@ def linkedViewChanged(self, view, axis): else: overlap = min(sg.bottom(), vg.bottom()) - max(sg.top(), vg.top()) if overlap < min(vg.height()/3, sg.height()/3): ## if less than 1/3 of views overlap, - ## then just replicate the view + ## then just replicate the view y1 = vr.top() y2 = vr.bottom() else: ## views overlap; line them up upp = float(vr.height()) / vg.height() - y2 = vr.bottom() - (sg.y()-vg.y()) * upp + if self.yInverted(): + y2 = vr.bottom() + (sg.bottom()-vg.bottom()) * upp + else: + y2 = vr.bottom() + (sg.top()-vg.top()) * upp y1 = y2 - sg.height() * upp self.enableAutoRange(ViewBox.YAxis, False) self.setYRange(y1, y2, padding=0) From 46901ae83ae4ede0fa4bcd0db58404f37eccec2a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 12 Jul 2013 13:14:09 -0400 Subject: [PATCH 063/121] ListParameter bugfix: allow unhashable types as parameter values. --- examples/parametertree.py | 2 +- pyqtgraph/parametertree/parameterTypes.py | 55 ++++++++++++----------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/examples/parametertree.py b/examples/parametertree.py index c600d1be70..c0eb50dbff 100644 --- a/examples/parametertree.py +++ b/examples/parametertree.py @@ -67,7 +67,7 @@ def addNew(self, typ): {'name': 'Float', 'type': 'float', 'value': 10.5, 'step': 0.1}, {'name': 'String', 'type': 'str', 'value': "hi"}, {'name': 'List', 'type': 'list', 'values': [1,2,3], 'value': 2}, - {'name': 'Named List', 'type': 'list', 'values': {"one": 1, "two": 2, "three": 3}, 'value': 2}, + {'name': 'Named List', 'type': 'list', 'values': {"one": 1, "two": "twosies", "three": [3,3,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'}, diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 51f0be6425..c3a9420e2d 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -476,32 +476,16 @@ def makeWidget(self): return w def value(self): - #vals = self.param.opts['limits'] key = asUnicode(self.widget.currentText()) - #if isinstance(vals, dict): - #return vals[key] - #else: - #return key - #print key, self.forward return self.forward.get(key, None) def setValue(self, val): - #vals = self.param.opts['limits'] - #if isinstance(vals, dict): - #key = None - #for k,v in vals.iteritems(): - #if v == val: - #key = k - #if key is None: - #raise Exception("Value '%s' not allowed." % val) - #else: - #key = unicode(val) self.targetValue = val - if val not in self.reverse: + if val not in self.reverse[0]: self.widget.setCurrentIndex(0) else: - key = self.reverse[val] + key = self.reverse[1][self.reverse[0].index(val)] ind = self.widget.findText(key) self.widget.setCurrentIndex(ind) @@ -531,8 +515,8 @@ class ListParameter(Parameter): itemClass = ListParameterItem def __init__(self, **opts): - self.forward = OrderedDict() ## name: value - self.reverse = OrderedDict() ## value: name + self.forward = OrderedDict() ## {name: value, ...} + self.reverse = ([], []) ## ([value, ...], [name, ...]) ## Parameter uses 'limits' option to define the set of allowed values if 'values' in opts: @@ -547,23 +531,40 @@ def setLimits(self, limits): Parameter.setLimits(self, limits) #print self.name(), self.value(), limits - if self.value() not in self.reverse and len(self.reverse) > 0: - self.setValue(list(self.reverse.keys())[0]) + if len(self.reverse) > 0 and self.value() not in self.reverse[0]: + self.setValue(self.reverse[0][0]) + + #def addItem(self, name, value=None): + #if name in self.forward: + #raise Exception("Name '%s' is already in use for this parameter" % name) + #limits = self.opts['limits'] + #if isinstance(limits, dict): + #limits = limits.copy() + #limits[name] = value + #self.setLimits(limits) + #else: + #if value is not None: + #raise Exception ## raise exception or convert to dict? + #limits = limits[:] + #limits.append(name) + ## what if limits == None? @staticmethod def mapping(limits): - ## Return forward and reverse mapping dictionaries given a limit specification - forward = OrderedDict() ## name: value - reverse = OrderedDict() ## value: name + ## Return forward and reverse mapping objects given a limit specification + forward = OrderedDict() ## {name: value, ...} + reverse = ([], []) ## ([value, ...], [name, ...]) if isinstance(limits, dict): for k, v in limits.items(): forward[k] = v - reverse[v] = k + reverse[0].append(v) + reverse[1].append(k) else: for v in limits: n = asUnicode(v) forward[n] = v - reverse[v] = n + reverse[0].append(v) + reverse[1].append(n) return forward, reverse registerParameterType('list', ListParameter, override=True) From 6131427deae168419a31e5c30571ae234365e495 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 13 Jul 2013 16:06:48 -0400 Subject: [PATCH 064/121] added error message when GL shaders are not available --- pyqtgraph/opengl/shaders.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/opengl/shaders.py b/pyqtgraph/opengl/shaders.py index b1652850d6..5ef20776f4 100644 --- a/pyqtgraph/opengl/shaders.py +++ b/pyqtgraph/opengl/shaders.py @@ -1,3 +1,4 @@ +import OpenGL from OpenGL.GL import * from OpenGL.GL import shaders import re @@ -218,6 +219,8 @@ def shader(self): if self.compiled is None: try: self.compiled = shaders.compileShader(self.code, self.shaderType) + except OpenGL.NullFunctionError: + raise Exception("This OpenGL implementation does not support shader programs; many features on pyqtgraph will not work.") except RuntimeError as exc: ## Format compile errors a bit more nicely if len(exc.args) == 3: From 3eeffd3b1dc187234bf3c4a467fc03381aa85077 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 13 Jul 2013 16:42:36 -0400 Subject: [PATCH 065/121] GLLinePLotItem accepts array of colors (thanks Felix!) --- README.txt | 1 + pyqtgraph/opengl/items/GLLinePlotItem.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.txt b/README.txt index d209ef0100..85e2b24a29 100644 --- a/README.txt +++ b/README.txt @@ -12,6 +12,7 @@ Contributors: Christian Gavin Michael Cristopher Hogg Ulrich Leutner + Felix Schill Requirements: PyQt 4.7+ or PySide diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py index bb5ce2f6e3..75d48c864d 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -30,8 +30,9 @@ def setData(self, **kwds): Arguments: ------------------------------------------------------------------------ pos (N,3) array of floats specifying point locations. - color tuple of floats (0.0-1.0) specifying - a color for the entire item. + color (N,4) array of floats (0.0-1.0) or + tuple of floats specifying + a single color for the entire item. width float specifying line width antialias enables smooth line drawing ==================== ================================================== @@ -71,9 +72,18 @@ def paint(self): self.setupGLState() glEnableClientState(GL_VERTEX_ARRAY) + try: glVertexPointerf(self.pos) - glColor4f(*self.color) + + if isinstance(self.color, np.ndarray): + glEnableClientState(GL_COLOR_ARRAY) + glColorPointerf(self.color) + else: + if isinstance(self.color, QtGui.QColor): + glColor4f(*fn.glColor(self.color)) + else: + glColor4f(*self.color) glLineWidth(self.width) #glPointSize(self.width) @@ -85,6 +95,7 @@ def paint(self): glDrawArrays(GL_LINE_STRIP, 0, self.pos.size / self.pos.shape[-1]) finally: + glDisableClientState(GL_COLOR_ARRAY) glDisableClientState(GL_VERTEX_ARRAY) From ef8c47e8c84f5e5c6cb7c43c81fd3224584ffc52 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 4 Aug 2013 14:35:28 -0400 Subject: [PATCH 066/121] Allow QtProcess without local QApplication --- pyqtgraph/multiprocess/processes.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 2b345e8b3c..7d147a1d86 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -325,7 +325,8 @@ class QtProcess(Process): GUI. - A QTimer is also started on the parent process which polls for requests from the child process. This allows Qt signals emitted within the child - process to invoke slots on the parent process and vice-versa. + process to invoke slots on the parent process and vice-versa. This can + be disabled using processRequests=False in the constructor. Example:: @@ -342,18 +343,29 @@ def slot(): def __init__(self, **kwds): if 'target' not in kwds: kwds['target'] = startQtEventLoop + self._processRequests = kwds.pop('processRequests', True) Process.__init__(self, **kwds) self.startEventTimer() def startEventTimer(self): from pyqtgraph.Qt import QtGui, QtCore ## avoid module-level import to keep bootstrap snappy. self.timer = QtCore.QTimer() - app = QtGui.QApplication.instance() - if app is None: - raise Exception("Must create QApplication before starting QtProcess") + if self._processRequests: + app = QtGui.QApplication.instance() + if app is None: + raise Exception("Must create QApplication before starting QtProcess, or use QtProcess(processRequests=False)") + self.startRequestProcessing() + + def startRequestProcessing(self, interval=0.01): + """Start listening for requests coming from the child process. + This allows signals to be connected from the child process to the parent. + """ self.timer.timeout.connect(self.processRequests) - self.timer.start(10) + self.timer.start(interval*1000) + def stopRequestProcessing(self): + self.timer.stop() + def processRequests(self): try: Process.processRequests(self) From 79bd7ea187a3b65d16c5ce3a9e08b5b932318ed1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 4 Aug 2013 14:36:14 -0400 Subject: [PATCH 067/121] documentation, bugfix --- pyqtgraph/graphicsItems/PlotCurveItem.py | 3 +++ pyqtgraph/graphicsItems/PlotDataItem.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 881dcf2d33..742c73ef88 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -261,6 +261,9 @@ def setData(self, *args, **kargs): by :func:`mkBrush ` is allowed. antialias (bool) Whether to use antialiasing when drawing. This is disabled by default because it decreases performance. + stepMode If True, two orthogonal lines are drawn for each sample + as steps. This is commonly used when drawing histograms. + Note that in this case, len(x) == len(y) + 1 ============== ======================================================== If non-keyword arguments are used, they will be interpreted as diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 1ae528ba2f..f76a8b7423 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -475,7 +475,7 @@ def getData(self): if self.xClean is None: nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData) - if any(nanMask): + if nanMask.any(): self.dataMask = ~nanMask self.xClean = self.xData[self.dataMask] self.yClean = self.yData[self.dataMask] From 2095a4c8aeb3d8ba5ac45836fe0c34dfebdd1151 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 16 Aug 2013 21:28:03 -0400 Subject: [PATCH 068/121] Support for FFT with non-uniform time sampling --- pyqtgraph/graphicsItems/PlotDataItem.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index f76a8b7423..f9f2febeb5 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -495,10 +495,7 @@ def getData(self): ##y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing #y = y[::ds] if self.opts['fftMode']: - f = np.fft.fft(y) / len(y) - y = abs(f[1:len(f)/2]) - dt = x[-1] - x[0] - x = np.linspace(0, 0.5*len(x)/dt, len(y)) + x,y = self._fourierTransform(x, y) if self.opts['logMode'][0]: x = np.log10(x) if self.opts['logMode'][1]: @@ -666,8 +663,21 @@ def viewRangeChanged(self): self.xDisp = self.yDisp = None self.updateItems() - - + def _fourierTransform(self, x, y): + ## Perform fourier transform. If x values are not sampled uniformly, + ## then use interpolate.griddata to resample before taking fft. + dx = np.diff(x) + uniform = not np.any(np.abs(dx-dx[0]) > (abs(dx[0]) / 1000.)) + if not uniform: + import scipy.interpolate as interp + x2 = np.linspace(x[0], x[-1], len(x)) + y = interp.griddata(x, y, x2, method='linear') + x = x2 + f = np.fft.fft(y) / len(y) + y = abs(f[1:len(f)/2]) + dt = x[-1] - x[0] + x = np.linspace(0, 0.5*len(x)/dt, len(y)) + return x, y def dataType(obj): if hasattr(obj, '__len__') and len(obj) == 0: From 6b3cfbc6fb90b9cdf8f7bbdaaf890e0d2ddd411f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 18 Aug 2013 23:02:01 -0400 Subject: [PATCH 069/121] Fixed parametertree selection bug --- pyqtgraph/parametertree/ParameterTree.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/ParameterTree.py b/pyqtgraph/parametertree/ParameterTree.py index e57430eaf9..866875e536 100644 --- a/pyqtgraph/parametertree/ParameterTree.py +++ b/pyqtgraph/parametertree/ParameterTree.py @@ -1,6 +1,7 @@ from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.widgets.TreeWidget import TreeWidget import os, weakref, re +from .ParameterItem import ParameterItem #import functions as fn @@ -103,7 +104,7 @@ def selectionChanged(self, *args): sel = self.selectedItems() if len(sel) != 1: sel = None - if self.lastSel is not None: + if self.lastSel is not None and isinstance(self.lastSel, ParameterItem): self.lastSel.selected(False) if sel is None: self.lastSel = None From 160b1ee45f2625f23152b393ffd776ff1991e3dd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 21 Aug 2013 10:40:19 -0600 Subject: [PATCH 070/121] Python3 bugfixes --- pyqtgraph/exporters/Exporter.py | 2 +- pyqtgraph/exporters/ImageExporter.py | 2 +- pyqtgraph/flowchart/Flowchart.py | 7 ++++--- pyqtgraph/opengl/GLViewWidget.py | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/exporters/Exporter.py b/pyqtgraph/exporters/Exporter.py index 43a8c33035..6371a3b973 100644 --- a/pyqtgraph/exporters/Exporter.py +++ b/pyqtgraph/exporters/Exporter.py @@ -119,7 +119,7 @@ def getPaintItems(self, root=None): else: childs = root.childItems() rootItem = [root] - childs.sort(lambda a,b: cmp(a.zValue(), b.zValue())) + childs.sort(key=lambda a: a.zValue()) while len(childs) > 0: ch = childs.pop(0) tree = self.getPaintItems(ch) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index bdb8b9be18..d1d78e7ddd 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -42,7 +42,7 @@ def parameters(self): 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()] + filter = ["*."+bytes(f).decode('UTF-8') for f in QtGui.QImageWriter.supportedImageFormats()] preferred = ['*.png', '*.tif', '*.jpg'] for p in preferred[::-1]: if p in filter: diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index be0d86e58b..81f9e16377 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -376,10 +376,10 @@ def processOrder(self): #tdeps[t] = lastNode if lastInd is not None: dels.append((lastInd+1, t)) - dels.sort(lambda a,b: cmp(b[0], a[0])) + #dels.sort(lambda a,b: cmp(b[0], a[0])) + dels.sort(key=lambda a: a[0], reverse=True) for i, t in dels: ops.insert(i, ('d', t)) - return ops @@ -491,7 +491,8 @@ def restoreState(self, state, clear=False): self.clear() Node.restoreState(self, state) nodes = state['nodes'] - nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0])) + #nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0])) + nodes.sort(key=lambda a: a['pos'][0]) for n in nodes: if n['name'] in self._nodes: #self._nodes[n['name']].graphicsItem().moveBy(*n['pos']) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index d8f70055f0..fe52065a97 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -167,7 +167,7 @@ def drawItemTree(self, item=None, useItemNames=False): else: items = item.childItems() items.append(item) - items.sort(lambda a,b: cmp(a.depthValue(), b.depthValue())) + items.sort(key=lambda a: a.depthValue()) for i in items: if not i.visible(): continue From d3f56c6df3376e79e61a8ec8f62cff08d5851ce8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 22 Aug 2013 10:02:39 -0600 Subject: [PATCH 071/121] fixed PySide bug listing image formats --- pyqtgraph/exporters/ImageExporter.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index d1d78e7ddd..b14ed51366 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -1,6 +1,6 @@ from .Exporter import Exporter from pyqtgraph.parametertree import Parameter -from pyqtgraph.Qt import QtGui, QtCore, QtSvg +from pyqtgraph.Qt import QtGui, QtCore, QtSvg, USE_PYSIDE import pyqtgraph as pg import numpy as np @@ -42,7 +42,10 @@ def parameters(self): def export(self, fileName=None, toBytes=False, copy=False): if fileName is None and not toBytes and not copy: - filter = ["*."+bytes(f).decode('UTF-8') for f in QtGui.QImageWriter.supportedImageFormats()] + if USE_PYSIDE: + filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()] + else: + filter = ["*."+bytes(f).decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] preferred = ['*.png', '*.tif', '*.jpg'] for p in preferred[::-1]: if p in filter: From 824e4b378bec6372aadc07e5aaad95b2a7f6a744 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 23 Aug 2013 22:08:32 -0600 Subject: [PATCH 072/121] Corrected behavior of GraphicsView.setBackground --- pyqtgraph/widgets/GraphicsView.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 6ddfe93033..0c8921f685 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -82,6 +82,7 @@ def __init__(self, parent=None, useOpenGL=None, background='default'): ## This might help, but it's probably dangerous in the general case.. #self.setOptimizationFlag(self.DontSavePainterState, True) + self.setBackgroundRole(QtGui.QPalette.NoRole) self.setBackground(background) self.setFocusPolicy(QtCore.Qt.StrongFocus) @@ -138,12 +139,9 @@ def setBackground(self, background): self._background = background if background == 'default': background = pyqtgraph.getConfigOption('background') - if background is None: - self.setBackgroundRole(QtGui.QPalette.NoRole) - else: - brush = fn.mkBrush(background) - self.setBackgroundBrush(brush) - + brush = fn.mkBrush(background) + self.setBackgroundBrush(brush) + def paintEvent(self, ev): self.scene().prepareForPaint() #print "GV: paint", ev.rect() From 42553854a9f4f3df9642a0dcf5abd04a459f08b4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 23 Aug 2013 22:27:09 -0600 Subject: [PATCH 073/121] pg.plot() and pg.PlotWidget() now accept background argument ImageExporter correctly handles QBrush with style=NoBrush --- pyqtgraph/__init__.py | 2 +- pyqtgraph/exporters/ImageExporter.py | 6 +++++- pyqtgraph/widgets/PlotWidget.py | 8 +++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index b1a05835ee..c1b620419a 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -281,7 +281,7 @@ def plot(*args, **kargs): #if len(args)+len(kargs) > 0: #w.plot(*args, **kargs) - pwArgList = ['title', 'labels', 'name', 'left', 'right', 'top', 'bottom'] + pwArgList = ['title', 'labels', 'name', 'left', 'right', 'top', 'bottom', 'background'] pwArgs = {} dataArgs = {} for k in kargs: diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index b14ed51366..a9b44ab441 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -17,7 +17,11 @@ def __init__(self, item): scene = item.scene() else: scene = item - bg = scene.views()[0].backgroundBrush().color() + bgbrush = scene.views()[0].backgroundBrush() + bg = bgbrush.color() + if bgbrush.style() == QtCore.Qt.NoBrush: + bg.setAlpha(0) + self.params = Parameter(name='params', type='group', children=[ {'name': 'width', 'type': 'int', 'value': tr.width(), 'limits': (0, None)}, {'name': 'height', 'type': 'int', 'value': tr.height(), 'limits': (0, None)}, diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index 1fa07f2ab4..7b3c685c8a 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -40,10 +40,12 @@ class PlotWidget(GraphicsView): For all other methods, use :func:`getPlotItem `. """ - def __init__(self, parent=None, **kargs): - """When initializing PlotWidget, all keyword arguments except *parent* are passed + def __init__(self, parent=None, background='default', **kargs): + """When initializing PlotWidget, *parent* and *background* are passed to + :func:`GraphicsWidget.__init__() ` + and all others are passed to :func:`PlotItem.__init__() `.""" - GraphicsView.__init__(self, parent) + GraphicsView.__init__(self, parent, background=background) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.enableMouse(False) self.plotItem = PlotItem(**kargs) From 91aa2f1c16fbe0075ae0fc4426e7c2a3f038c2ac Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 27 Aug 2013 12:00:26 -0600 Subject: [PATCH 074/121] fixed TextParameter editor disappearing after focus lost --- pyqtgraph/parametertree/parameterTypes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index c3a9420e2d..3300171fcd 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -616,6 +616,7 @@ def activate(self): class TextParameterItem(WidgetParameterItem): def __init__(self, param, depth): WidgetParameterItem.__init__(self, param, depth) + self.hideWidget = False self.subItem = QtGui.QTreeWidgetItem() self.addChild(self.subItem) From dfa2c8a502c93d464238b58aa3feb24582212ab3 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Thu, 5 Sep 2013 04:37:41 +0800 Subject: [PATCH 075/121] solve some issue with opengl and python3 --- pyqtgraph/opengl/shaders.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/opengl/shaders.py b/pyqtgraph/opengl/shaders.py index 515de33a0e..8f0d6e1bb0 100644 --- a/pyqtgraph/opengl/shaders.py +++ b/pyqtgraph/opengl/shaders.py @@ -1,4 +1,7 @@ -import OpenGL +try: + from OpenGL import NullFunctionError +except ImportError: + from OpenGL.error import NullFunctionError from OpenGL.GL import * from OpenGL.GL import shaders import re @@ -219,7 +222,7 @@ def shader(self): if self.compiled is None: try: self.compiled = shaders.compileShader(self.code, self.shaderType) - except OpenGL.NullFunctionError: + except NullFunctionError: raise Exception("This OpenGL implementation does not support shader programs; many features on pyqtgraph will not work.") except RuntimeError as exc: ## Format compile errors a bit more nicely @@ -227,9 +230,12 @@ def shader(self): err, code, typ = exc.args if not err.startswith('Shader compile failure'): raise - code = code[0].split('\n') + code = code[0].decode('utf_8').split('\n') err, c, msgs = err.partition(':') err = err + '\n' + msgs = re.sub('b\'','',msgs) + msgs = re.sub('\'$','',msgs) + msgs = re.sub('\\\\n','\n',msgs) msgs = msgs.split('\n') errNums = [()] * len(code) for i, msg in enumerate(msgs): @@ -357,7 +363,7 @@ def __exit__(self, *args): def uniform(self, name): """Return the location integer for a uniform variable in this program""" - return glGetUniformLocation(self.program(), bytes(name,'utf_8')) + return glGetUniformLocation(self.program(), name.encode('utf_8')) #def uniformBlockInfo(self, blockName): #blockIndex = glGetUniformBlockIndex(self.program(), blockName) From f997b3079b7b723ff3ee7f65b100620ee1cb4eb5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 6 Sep 2013 15:36:36 -0400 Subject: [PATCH 076/121] Added GLBarGraphItem example GLMeshItem accepts ShaderProgram or name of predefined program Added missing documentation to GLGraphicsItem minor edits --- examples/GLBarGraphItem.py | 47 +++++++++++++++++++++++++++ pyqtgraph/PlotData.py | 1 + pyqtgraph/multiprocess/remoteproxy.py | 6 +++- pyqtgraph/opengl/GLGraphicsItem.py | 23 +++++++++++++ pyqtgraph/opengl/MeshData.py | 6 ++-- pyqtgraph/opengl/items/GLMeshItem.py | 6 +++- 6 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 examples/GLBarGraphItem.py diff --git a/examples/GLBarGraphItem.py b/examples/GLBarGraphItem.py new file mode 100644 index 0000000000..d14eba87d9 --- /dev/null +++ b/examples/GLBarGraphItem.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +Demonstrate use of GLLinePlotItem to draw cross-sections of a surface. + +""" +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph.opengl as gl +import pyqtgraph as pg +import numpy as np + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.opts['distance'] = 40 +w.show() +w.setWindowTitle('pyqtgraph example: GLBarGraphItem') + +gx = gl.GLGridItem() +gx.rotate(90, 0, 1, 0) +gx.translate(-10, 0, 10) +w.addItem(gx) +gy = gl.GLGridItem() +gy.rotate(90, 1, 0, 0) +gy.translate(0, -10, 10) +w.addItem(gy) +gz = gl.GLGridItem() +gz.translate(0, 0, 0) +w.addItem(gz) + +# regular grid of starting positions +pos = np.mgrid[0:10, 0:10, 0:1].reshape(3,10,10).transpose(1,2,0) +# fixed widths, random heights +size = np.empty((10,10,3)) +size[...,0:2] = 0.4 +size[...,2] = np.random.normal(size=(10,10)) + +bg = gl.GLBarGraphItem(pos, size) +w.addItem(bg) + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/pyqtgraph/PlotData.py b/pyqtgraph/PlotData.py index 0bf13ca87c..e5faadda02 100644 --- a/pyqtgraph/PlotData.py +++ b/pyqtgraph/PlotData.py @@ -15,6 +15,7 @@ class PlotData(object): - removal of nan/inf values - option for single value shared by entire column - cached downsampling + - cached min / max / hasnan / isuniform """ def __init__(self): self.fields = {} diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index f33ebc8308..e4c3f34fc4 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -205,7 +205,11 @@ def handleRequest(self): fnkwds[k] = np.fromstring(byteData[ind], dtype=dtype).reshape(shape) if len(fnkwds) == 0: ## need to do this because some functions do not allow keyword arguments. - result = obj(*fnargs) + try: + result = obj(*fnargs) + except: + print("Failed to call object %s: %d, %s" % (obj, len(fnargs), fnargs[1:])) + raise else: result = obj(*fnargs, **fnkwds) diff --git a/pyqtgraph/opengl/GLGraphicsItem.py b/pyqtgraph/opengl/GLGraphicsItem.py index 59bc4449b2..9680fba7e4 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -40,6 +40,7 @@ def __init__(self, parentItem=None): self.__glOpts = {} def setParentItem(self, item): + """Set this item's parent in the scenegraph hierarchy.""" if self.__parent is not None: self.__parent.__children.remove(self) if item is not None: @@ -98,9 +99,11 @@ def updateGLOptions(self, opts): def parentItem(self): + """Return a this item's parent in the scenegraph hierarchy.""" return self.__parent def childItems(self): + """Return a list of this item's children in the scenegraph hierarchy.""" return list(self.__children) def _setView(self, v): @@ -124,10 +127,15 @@ def depthValue(self): return self.__depthValue def setTransform(self, tr): + """Set the local transform for this object. + Must be a :class:`Transform3D ` instance. This transform + determines how the local coordinate system of the item is mapped to the coordinate + system of its parent.""" self.__transform = Transform3D(tr) self.update() def resetTransform(self): + """Reset this item's transform to an identity transformation.""" self.__transform.setToIdentity() self.update() @@ -148,9 +156,12 @@ def applyTransform(self, tr, local): self.setTransform(tr * self.transform()) def transform(self): + """Return this item's transform object.""" return self.__transform def viewTransform(self): + """Return the transform mapping this item's local coordinate system to the + view coordinate system.""" tr = self.__transform p = self while True: @@ -190,16 +201,24 @@ def scale(self, x, y, z, local=True): def hide(self): + """Hide this item. + This is equivalent to setVisible(False).""" self.setVisible(False) def show(self): + """Make this item visible if it was previously hidden. + This is equivalent to setVisible(True).""" self.setVisible(True) def setVisible(self, vis): + """Set the visibility of this item.""" self.__visible = vis self.update() def visible(self): + """Return True if the item is currently set to be visible. + Note that this does not guarantee that the item actually appears in the + view, as it may be obscured or outside of the current view area.""" return self.__visible @@ -237,6 +256,10 @@ def paint(self): self.setupGLState() def update(self): + """ + Indicates that this item needs to be redrawn, and schedules an update + with the view it is displayed in. + """ v = self.view() if v is None: return diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index 12a9b83b5e..71e566c936 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -247,9 +247,9 @@ def faceNormals(self, indexed=None): return self._faceNormals elif indexed == 'faces': if self._faceNormalsIndexedByFaces is None: - norms = np.empty((self._faceNormals.shape[0], 3, 3)) - norms[:] = self._faceNormals[:,np.newaxis,:] - self._faceNormalsIndexedByFaces = norms + norms = np.empty((self._faceNormals.shape[0], 3, 3)) + norms[:] = self._faceNormals[:,np.newaxis,:] + self._faceNormalsIndexedByFaces = norms return self._faceNormalsIndexedByFaces else: raise Exception("Invalid indexing mode. Accepts: None, 'faces'") diff --git a/pyqtgraph/opengl/items/GLMeshItem.py b/pyqtgraph/opengl/items/GLMeshItem.py index 66d5436141..5b245e6488 100644 --- a/pyqtgraph/opengl/items/GLMeshItem.py +++ b/pyqtgraph/opengl/items/GLMeshItem.py @@ -69,7 +69,11 @@ def setShader(self, shader): self.update() def shader(self): - return shaders.getShaderProgram(self.opts['shader']) + shader = self.opts['shader'] + if isinstance(shader, shaders.ShaderProgram): + return shader + else: + return shaders.getShaderProgram(shader) def setColor(self, c): """Set the default color to use when no vertex or face colors are specified.""" From bb3533ab8173fc48b538360ec2d60f0e541f762e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 10 Sep 2013 02:51:57 -0400 Subject: [PATCH 077/121] Workaround for pyside bug: https://bugs.launchpad.net/pyqtgraph/+bug/1223173 --- pyqtgraph/Vector.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/Vector.py b/pyqtgraph/Vector.py index e9c109d860..4b4fb02fb7 100644 --- a/pyqtgraph/Vector.py +++ b/pyqtgraph/Vector.py @@ -5,7 +5,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation. """ -from .Qt import QtGui, QtCore +from .Qt import QtGui, QtCore, USE_PYSIDE import numpy as np class Vector(QtGui.QVector3D): @@ -33,7 +33,13 @@ def __init__(self, *args): def __len__(self): return 3 - + + def __add__(self, b): + # workaround for pyside bug. see https://bugs.launchpad.net/pyqtgraph/+bug/1223173 + if USE_PYSIDE and isinstance(b, QtGui.QVector3D): + b = Vector(b) + return QtGui.QVector3D.__add__(self, b) + #def __reduce__(self): #return (Point, (self.x(), self.y())) From 35ea55897e970cce193058cb7626d21cf8172279 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Tue, 10 Sep 2013 20:57:56 +0800 Subject: [PATCH 078/121] python3 bugfixes (SVGexpoter) --- pyqtgraph/exporters/SVGExporter.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 672897ab3b..7c48c8a917 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -1,4 +1,5 @@ from .Exporter import Exporter +from pyqtgraph.python2_3 import asUnicode from pyqtgraph.parametertree import Parameter from pyqtgraph.Qt import QtGui, QtCore, QtSvg import pyqtgraph as pg @@ -91,8 +92,8 @@ def export(self, fileName=None, toBytes=False, copy=False): 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')) + with open(fileName, 'wt') as fh: + fh.write(asUnicode(xml)) xmlHeader = """\ @@ -221,8 +222,8 @@ def _generateItemSvg(item, nodes=None, root=None): ## this is taken care of in generateSvg instead. #if hasattr(item, 'setExportMode'): #item.setExportMode(False) - - xmlStr = str(arr) + + xmlStr = bytes(arr).decode('utf-8') doc = xml.parseString(xmlStr) try: @@ -340,7 +341,7 @@ def correctCoordinates(node, item): if match is None: vals = [1,0,0,1,0,0] else: - vals = map(float, match.groups()[0].split(',')) + vals = [float(a) for a in match.groups()[0].split(',')] tr = np.array([[vals[0], vals[2], vals[4]], [vals[1], vals[3], vals[5]]]) removeTransform = False From 59ada9b1b4c8195e280c0bf6368be25e58b8a0e6 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Tue, 10 Sep 2013 22:12:55 +0800 Subject: [PATCH 079/121] More bugfixes in SVGExporter.py --- pyqtgraph/exporters/SVGExporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 7c48c8a917..821427a46d 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -350,9 +350,9 @@ def correctCoordinates(node, item): continue if ch.tagName == 'polyline': removeTransform = True - coords = np.array([map(float, c.split(',')) for c in ch.getAttribute('points').strip().split(' ')]) + coords = np.array([[float(a) for a in c.split(',')] for c in ch.getAttribute('points').strip().split(' ')]) coords = pg.transformCoordinates(tr, coords, transpose=True) - ch.setAttribute('points', ' '.join([','.join(map(str, c)) for c in coords])) + ch.setAttribute('points', ' '.join([','.join([str(a) for a in c]) for c in coords])) elif ch.tagName == 'path': removeTransform = True newCoords = '' From b48e0e9eb50b5007a95d10eabb269c952129c729 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Tue, 10 Sep 2013 22:34:20 +0800 Subject: [PATCH 080/121] Restore utf-8 compatibility for python 2 --- pyqtgraph/exporters/SVGExporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 821427a46d..62b49d30ae 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -92,8 +92,8 @@ def export(self, fileName=None, toBytes=False, copy=False): md.setData('image/svg+xml', QtCore.QByteArray(xml.encode('UTF-8'))) QtGui.QApplication.clipboard().setMimeData(md) else: - with open(fileName, 'wt') as fh: - fh.write(asUnicode(xml)) + with open(fileName, 'wb') as fh: + fh.write(asUnicode(xml).encode('utf-8')) xmlHeader = """\ From 3df31d18324613c532d4dd184dbc19ba4789e303 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Thu, 12 Sep 2013 12:26:39 +0800 Subject: [PATCH 081/121] add .gitignore and .mailmap --- .gitignore | 4 ++++ .mailmap | 14 ++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .gitignore create mode 100644 .mailmap diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..28ed45aa94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +build +*.pyc +*.swp diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000..7bd3e10e41 --- /dev/null +++ b/.mailmap @@ -0,0 +1,14 @@ +Luke Campagnola Luke Camapgnola <> +Luke Campagnola Luke Campagnola <> +Luke Campagnola Luke Campagnola +Megan Kratz +Megan Kratz meganbkratz@gmail.com <> +Megan Kratz Megan Kratz +Megan Kratz Megan Kratz +Megan Kratz Megan Kratz +Megan Kratz Megan Kratz +Megan Kratz Megan Kratz +Megan Kratz Megan Kratz +Ingo Breßler Ingo Breßler +Ingo Breßler Ingo B. + From 854304f087c57a8deae34d397d0da93b9fd53384 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Thu, 12 Sep 2013 12:36:02 +0800 Subject: [PATCH 082/121] cleaner mailamp --- .mailmap | 2 -- 1 file changed, 2 deletions(-) diff --git a/.mailmap b/.mailmap index 7bd3e10e41..025cf940e1 100644 --- a/.mailmap +++ b/.mailmap @@ -1,7 +1,5 @@ -Luke Campagnola Luke Camapgnola <> Luke Campagnola Luke Campagnola <> Luke Campagnola Luke Campagnola -Megan Kratz Megan Kratz meganbkratz@gmail.com <> Megan Kratz Megan Kratz Megan Kratz Megan Kratz From 85572d5f7a09ca7d42ac4479746584ba2c530676 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Thu, 12 Sep 2013 14:01:52 +0800 Subject: [PATCH 083/121] Convert README to markdown for better github presentation --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.txt | 47 --------------------------------------------- 2 files changed, 56 insertions(+), 47 deletions(-) create mode 100644 README.md delete mode 100644 README.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000000..f1ae9f7d29 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +PyQtGraph +========= + +A pure-Python graphics library for PyQt/PySide +Copyright 2012 Luke Campagnola, University of North Carolina at Chapel Hill +http://www.pyqtgraph.org + +Maintainer +---------- + * Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') + +Contributors +------------ + * Megan Kratz + * Paul Manis + * Ingo Breßler + * Christian Gavin + * Michael Cristopher Hogg + * Ulrich Leutner + * Felix Schill + * Guillaume Poulin + +Requirements +------------ + PyQt 4.7+ or PySide + python 2.6, 2.7, or 3.x + numpy, scipy + For 3D graphics: pyopengl + Known to run on Windows, Linux, and Mac. + +Support +------- + Post at the mailing list / forum: + https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph + +Installation Methods +-------------------- + * To use with a specific project, simply copy the pyqtgraph subdirectory + anywhere that is importable from your project + * To install system-wide from source distribution: + `$ python setup.py install` + * For instalation packages, see the website (pyqtgraph.org) + +Documentation +------------- + There are many examples; run `python -m pyqtgraph.examples` for a menu. + Some (incomplete) documentation exists at this time. + * Easiest place to get documentation is at + http://www.pyqtgraph.org/documentation + * If you acquired this code as a .tar.gz file from the website, then you can also look in + doc/html. + * If you acquired this code via GitHub, then you can build the documentation using sphinx. + From the documentation directory, run: + `$ make html` + Please feel free to pester Luke or post to the forum if you need a specific + section of documentation. diff --git a/README.txt b/README.txt deleted file mode 100644 index d03c6c77b5..0000000000 --- a/README.txt +++ /dev/null @@ -1,47 +0,0 @@ -PyQtGraph - A pure-Python graphics library for PyQt/PySide -Copyright 2012 Luke Campagnola, University of North Carolina at Chapel Hill -http://www.pyqtgraph.org - -Maintainer: - Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') - -Contributors: - Megan Kratz - Paul Manis - Ingo Breßler - Christian Gavin - Michael Cristopher Hogg - Ulrich Leutner - Felix Schill - Guillaume Poulin - -Requirements: - PyQt 4.7+ or PySide - python 2.6, 2.7, or 3.x - numpy, scipy - For 3D graphics: pyopengl - Known to run on Windows, Linux, and Mac. - -Support: - Post at the mailing list / forum: - https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph - -Installation Methods: - - To use with a specific project, simply copy the pyqtgraph subdirectory - anywhere that is importable from your project - - To install system-wide from source distribution: - $ python setup.py install - - For instalation packages, see the website (pyqtgraph.org) - -Documentation: - There are many examples; run "python -m pyqtgraph.examples" for a menu. - Some (incomplete) documentation exists at this time. - - Easiest place to get documentation is at - http://www.pyqtgraph.org/documentation - - If you acquired this code as a .tar.gz file from the website, then you can also look in - doc/html. - - If you acquired this code via BZR, then you can build the documentation using sphinx. - From the documentation directory, run: - $ make html - Please feel free to pester Luke or post to the forum if you need a specific - section of documentation. From 47c55ed4e3f7f4d4da839c5a279df6848149c8ed Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Thu, 12 Sep 2013 14:22:26 +0800 Subject: [PATCH 084/121] Update README.md correct markdown --- README.md | 66 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index f1ae9f7d29..23f47ea766 100644 --- a/README.md +++ b/README.md @@ -2,55 +2,63 @@ PyQtGraph ========= A pure-Python graphics library for PyQt/PySide + Copyright 2012 Luke Campagnola, University of North Carolina at Chapel Hill -http://www.pyqtgraph.org + + Maintainer ---------- - * Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') + + * Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') Contributors ------------ - * Megan Kratz - * Paul Manis - * Ingo Breßler - * Christian Gavin - * Michael Cristopher Hogg - * Ulrich Leutner - * Felix Schill - * Guillaume Poulin + + * Megan Kratz + * Paul Manis + * Ingo Breßler + * Christian Gavin + * Michael Cristopher Hogg + * Ulrich Leutner + * Felix Schill + * Guillaume Poulin Requirements ------------ - PyQt 4.7+ or PySide - python 2.6, 2.7, or 3.x - numpy, scipy - For 3D graphics: pyopengl - Known to run on Windows, Linux, and Mac. + + * PyQt 4.7+ or PySide + * python 2.6, 2.7, or 3.x + * numpy, scipy + * For 3D graphics: pyopengl + * Known to run on Windows, Linux, and Mac. Support ------- - Post at the mailing list / forum: - https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph + + Post at the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) Installation Methods -------------------- - * To use with a specific project, simply copy the pyqtgraph subdirectory + + * To use with a specific project, simply copy the pyqtgraph subdirectory anywhere that is importable from your project - * To install system-wide from source distribution: - `$ python setup.py install` - * For instalation packages, see the website (pyqtgraph.org) + * To install system-wide from source distribution: + `$ python setup.py install` + * For instalation packages, see the website (pyqtgraph.org) Documentation ------------- - There are many examples; run `python -m pyqtgraph.examples` for a menu. - Some (incomplete) documentation exists at this time. - * Easiest place to get documentation is at - http://www.pyqtgraph.org/documentation - * If you acquired this code as a .tar.gz file from the website, then you can also look in + +There are many examples; run `python -m pyqtgraph.examples` for a menu. + +Some (incomplete) documentation exists at this time. + * Easiest place to get documentation is at + * If you acquired this code as a .tar.gz file from the website, then you can also look in doc/html. - * If you acquired this code via GitHub, then you can build the documentation using sphinx. + * If you acquired this code via GitHub, then you can build the documentation using sphinx. From the documentation directory, run: `$ make html` - Please feel free to pester Luke or post to the forum if you need a specific - section of documentation. + +Please feel free to pester Luke or post to the forum if you need a specific + section of documentation. From 58048a703c9ec963b68e91e2cf82da9b66d68d4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 13 Sep 2013 03:27:26 -0400 Subject: [PATCH 085/121] - Removed inf/nan checking from PlotDataItem and PlotCurveItem; improved performance - Added 'connect' option to PlotDataItem and PlotCurveItem to affect which line segments are drawn - arrayToQPath() added 'finite' connection mode which omits non-finite values from connections --- pyqtgraph/functions.py | 4 +- pyqtgraph/graphicsItems/PlotCurveItem.py | 18 +++++-- pyqtgraph/graphicsItems/PlotDataItem.py | 56 ++++++++++++---------- pyqtgraph/graphicsItems/ScatterPlotItem.py | 4 +- 4 files changed, 51 insertions(+), 31 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 3f0c6a3e4f..14e4e076f3 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1133,7 +1133,9 @@ def arrayToQPath(x, y, connect='all'): connect[:,0] = 1 connect[:,1] = 0 connect = connect.flatten() - + if connect == 'finite': + connect = np.isfinite(x) & np.isfinite(y) + arr[1:-1]['c'] = connect if connect == 'all': arr[1:-1]['c'] = 1 elif isinstance(connect, np.ndarray): diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 742c73ef88..2fea3d33d0 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -71,7 +71,8 @@ def __init__(self, *args, **kargs): 'brush': None, 'stepMode': False, 'name': None, - 'antialias': pg.getConfigOption('antialias'), + 'antialias': pg.getConfigOption('antialias'),\ + 'connect': 'all', } self.setClickable(kargs.get('clickable', False)) self.setData(*args, **kargs) @@ -119,10 +120,12 @@ def dataBounds(self, ax, frac=1.0, orthoRange=None): ## Get min/max (or percentiles) of the requested data range if frac >= 1.0: - b = (d.min(), d.max()) + b = (np.nanmin(d), np.nanmax(d)) elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: + mask = np.isfinite(d) + d = d[mask] b = (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) ## adjust for fill level @@ -264,6 +267,12 @@ def setData(self, *args, **kargs): stepMode If True, two orthogonal lines are drawn for each sample as steps. This is commonly used when drawing histograms. Note that in this case, len(x) == len(y) + 1 + connect Argument specifying how vertexes should be connected + by line segments. Default is "all", indicating full + connection. "pairs" causes only even-numbered segments + to be drawn. "finite" causes segments to be omitted if + they are attached to nan or inf values. For any other + connectivity, specify an array of boolean values. ============== ======================================================== If non-keyword arguments are used, they will be interpreted as @@ -326,7 +335,8 @@ def updateData(self, *args, **kargs): if 'name' in kargs: self.opts['name'] = kargs['name'] - + if 'connect' in kargs: + self.opts['connect'] = kargs['connect'] if 'pen' in kargs: self.setPen(kargs['pen']) if 'shadowPen' in kargs: @@ -365,7 +375,7 @@ def generatePath(self, x, y): y[0] = self.opts['fillLevel'] y[-1] = self.opts['fillLevel'] - path = fn.arrayToQPath(x, y, connect='all') + path = fn.arrayToQPath(x, y, connect=self.opts['connect']) return path diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index f9f2febeb5..1e525f8372 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -58,6 +58,8 @@ def __init__(self, *args, **kargs): **Line style keyword arguments:** ========== ================================================ + connect Specifies how / whether vertexes should be connected. + See :func:`arrayToQPath() ` pen Pen to use for drawing line between points. Default is solid grey, 1px width. Use None to disable line drawing. May be any single argument accepted by :func:`mkPen() ` @@ -119,7 +121,7 @@ def __init__(self, *args, **kargs): self.yData = None self.xDisp = None self.yDisp = None - self.dataMask = None + #self.dataMask = None #self.curves = [] #self.scatters = [] self.curve = PlotCurveItem() @@ -133,6 +135,8 @@ def __init__(self, *args, **kargs): #self.clear() self.opts = { + 'connect': 'all', + 'fftMode': False, 'logMode': [False, False], 'alphaHint': 1.0, @@ -386,6 +390,8 @@ def setData(self, *args, **kargs): if 'name' in kargs: self.opts['name'] = kargs['name'] + if 'connect' in kargs: + self.opts['connect'] = kargs['connect'] ## if symbol pen/brush are given with no symbol, then assume symbol is 'o' @@ -445,7 +451,7 @@ def setData(self, *args, **kargs): def updateItems(self): curveArgs = {} - for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias')]: + for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect')]: curveArgs[v] = self.opts[k] scatterArgs = {} @@ -454,7 +460,7 @@ def updateItems(self): scatterArgs[v] = self.opts[k] x,y = self.getData() - scatterArgs['mask'] = self.dataMask + #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) @@ -473,20 +479,20 @@ def getData(self): if self.xData is None: return (None, None) - if self.xClean is None: - nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData) - if nanMask.any(): - self.dataMask = ~nanMask - self.xClean = self.xData[self.dataMask] - self.yClean = self.yData[self.dataMask] - else: - self.dataMask = None - self.xClean = self.xData - self.yClean = self.yData + #if self.xClean is None: + #nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData) + #if nanMask.any(): + #self.dataMask = ~nanMask + #self.xClean = self.xData[self.dataMask] + #self.yClean = self.yData[self.dataMask] + #else: + #self.dataMask = None + #self.xClean = self.xData + #self.yClean = self.yData if self.xDisp is None: - x = self.xClean - y = self.yClean + x = self.xData + y = self.yData #ds = self.opts['downsample'] @@ -500,14 +506,14 @@ def getData(self): x = np.log10(x) if self.opts['logMode'][1]: y = np.log10(y) - 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): - self.dataMask = ~nanMask - x = x[self.dataMask] - y = y[self.dataMask] - else: - self.dataMask = None + #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): + #self.dataMask = ~nanMask + #x = x[self.dataMask] + #y = y[self.dataMask] + #else: + #self.dataMask = None ds = self.opts['downsample'] if not isinstance(ds, int): @@ -640,8 +646,8 @@ def clear(self): #self.scatters = [] self.xData = None self.yData = None - self.xClean = None - self.yClean = None + #self.xClean = None + #self.yClean = None self.xDisp = None self.yDisp = None self.curve.setData([]) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 3070d15a22..97f5aa8f61 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -626,11 +626,13 @@ def dataBounds(self, ax, frac=1.0, orthoRange=None): d2 = d2[mask] if frac >= 1.0: - self.bounds[ax] = (d.min() - self._maxSpotWidth*0.7072, d.max() + self._maxSpotWidth*0.7072) + self.bounds[ax] = (np.nanmin(d) - self._maxSpotWidth*0.7072, np.nanmax(d) + 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)) else: + mask = np.isfinite(d) + d = d[mask] return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) def pixelPadding(self): From 8b58416d1d69827d815386cec6b6107a6583b9ba Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 18 Sep 2013 12:27:01 -0400 Subject: [PATCH 086/121] minor edits --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 7657a6bdc9..d7fd49e514 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -183,7 +183,7 @@ def register(self, name): def unregister(self): """ - Remove this ViewBox forom the list of linkable views. (see :func:`register() `) + Remove this ViewBox from the list of linkable views. (see :func:`register() `) """ del ViewBox.AllViews[self] if self.name is not None: @@ -352,7 +352,7 @@ def targetRect(self): def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True): """ Set the visible range of the ViewBox. - Must specify at least one of *range*, *xRange*, or *yRange*. + Must specify at least one of *rect*, *xRange*, or *yRange*. ============= ===================================================================== **Arguments** From d8f9fb0781eae14ce1f6730645219aadac44dd23 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 18 Sep 2013 12:27:46 -0400 Subject: [PATCH 087/121] Added GLBarGraphItem --- pyqtgraph/opengl/items/GLBarGraphItem.py | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 pyqtgraph/opengl/items/GLBarGraphItem.py diff --git a/pyqtgraph/opengl/items/GLBarGraphItem.py b/pyqtgraph/opengl/items/GLBarGraphItem.py new file mode 100644 index 0000000000..b3060dc9ca --- /dev/null +++ b/pyqtgraph/opengl/items/GLBarGraphItem.py @@ -0,0 +1,29 @@ +from .GLMeshItem import GLMeshItem +from ..MeshData import MeshData +import numpy as np + +class GLBarGraphItem(GLMeshItem): + def __init__(self, pos, size): + """ + pos is (...,3) array of the bar positions (the corner of each bar) + size is (...,3) array of the sizes of each bar + """ + nCubes = reduce(lambda a,b: a*b, pos.shape[:-1]) + cubeVerts = np.mgrid[0:2,0:2,0:2].reshape(3,8).transpose().reshape(1,8,3) + cubeFaces = np.array([ + [0,1,2], [3,2,1], + [4,5,6], [7,6,5], + [0,1,4], [5,4,1], + [2,3,6], [7,6,3], + [0,2,4], [6,4,2], + [1,3,5], [7,5,3]]).reshape(1,12,3) + size = size.reshape((nCubes, 1, 3)) + pos = pos.reshape((nCubes, 1, 3)) + verts = cubeVerts * size + pos + faces = cubeFaces + (np.arange(nCubes) * 8).reshape(nCubes,1,1) + md = MeshData(verts.reshape(nCubes*8,3), faces.reshape(nCubes*12,3)) + + GLMeshItem.__init__(self, meshdata=md, shader='shaded', smooth=False) + + + \ No newline at end of file From 54ca31f91b3367a015164b3648ef35b2b1c808cf Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 19 Sep 2013 12:13:16 -0400 Subject: [PATCH 088/121] Added ImageExporter error message for zero-size export --- pyqtgraph/exporters/ImageExporter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index a9b44ab441..9fb77e2afa 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -64,6 +64,9 @@ def export(self, fileName=None, toBytes=False, copy=False): #self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32) #self.png.fill(pyqtgraph.mkColor(self.params['background'])) + w, h = self.params['width'], self.params['height'] + if w == 0 or h == 0: + raise Exception("Cannot export image with size=0 (requested export size is %dx%d)" % (w,h)) bg = np.empty((self.params['width'], self.params['height'], 4), dtype=np.ubyte) color = self.params['background'] bg[:,:,0] = color.blue() From 4052c3e9d1fc6c999679647a077720e7801d292b Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Thu, 17 Oct 2013 20:19:30 +0200 Subject: [PATCH 089/121] Ordereddict exploses with python2.6 ... Use the official backport --- pyqtgraph/ordereddict.py | 127 ++++++++++++++++++++ pyqtgraph/pgcollections.py | 230 +++++++++++++------------------------ 2 files changed, 209 insertions(+), 148 deletions(-) create mode 100644 pyqtgraph/ordereddict.py diff --git a/pyqtgraph/ordereddict.py b/pyqtgraph/ordereddict.py new file mode 100644 index 0000000000..5b0303f5a3 --- /dev/null +++ b/pyqtgraph/ordereddict.py @@ -0,0 +1,127 @@ +# Copyright (c) 2009 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +from UserDict import DictMixin + +class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py index b01985266b..4a225915e8 100644 --- a/pyqtgraph/pgcollections.py +++ b/pyqtgraph/pgcollections.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -advancedTypes.py - Basic data structures not included with python +advancedTypes.py - Basic data structures not included with python Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. @@ -15,76 +15,10 @@ try: from collections import OrderedDict -except: - # Deprecated; this class is now present in Python 2.7 as collections.OrderedDict +except ImportError: # Only keeping this around for python2.6 support. - class OrderedDict(dict): - """extends dict so that elements are iterated in the order that they were added. - Since this class can not be instantiated with regular dict notation, it instead uses - a list of tuples: - od = OrderedDict([(key1, value1), (key2, value2), ...]) - items set using __setattr__ are added to the end of the key list. - """ - - def __init__(self, data=None): - self.order = [] - if data is not None: - for i in data: - self[i[0]] = i[1] - - def __setitem__(self, k, v): - if not self.has_key(k): - self.order.append(k) - dict.__setitem__(self, k, v) - - def __delitem__(self, k): - self.order.remove(k) - dict.__delitem__(self, k) - - def keys(self): - return self.order[:] - - def items(self): - it = [] - for k in self.keys(): - it.append((k, self[k])) - return it - - def values(self): - return [self[k] for k in self.order] - - def remove(self, key): - del self[key] - #self.order.remove(key) - - def __iter__(self): - for k in self.order: - yield k - - def update(self, data): - """Works like dict.update, but accepts list-of-tuples as well as dict.""" - if isinstance(data, dict): - for k, v in data.iteritems(): - self[k] = v - else: - for k,v in data: - self[k] = v - - def copy(self): - return OrderedDict(self.items()) - - def itervalues(self): - for k in self.order: - yield self[k] - - def iteritems(self): - for k in self.order: - yield (k, self[k]) - - def __deepcopy__(self, memo): - return OrderedDict([(k, copy.deepcopy(v, memo)) for k, v in self.iteritems()]) - - + from ordereddict import OrderedDict + class ReverseDict(dict): """extends dict so that reverse lookups are possible by requesting the key as a list of length 1: @@ -101,7 +35,7 @@ def __init__(self, data=None): for k in data: self.reverse[data[k]] = k dict.__init__(self, data) - + def __getitem__(self, item): if type(item) is list: return self.reverse[item[0]] @@ -114,8 +48,8 @@ def __setitem__(self, item, value): def __deepcopy__(self, memo): raise Exception("deepcopy not implemented") - - + + class BiDict(dict): """extends dict so that reverse lookups are possible by adding each reverse combination to the dict. This only works if all values and keys are unique.""" @@ -125,11 +59,11 @@ def __init__(self, data=None): dict.__init__(self) for k in data: self[data[k]] = k - + def __setitem__(self, item, value): dict.__setitem__(self, item, value) dict.__setitem__(self, value, item) - + def __deepcopy__(self, memo): raise Exception("deepcopy not implemented") @@ -138,7 +72,7 @@ class ThreadsafeDict(dict): Also adds lock/unlock functions for extended exclusive operations Converts all sub-dicts and lists to threadsafe as well. """ - + def __init__(self, *args, **kwargs): self.mutex = threading.RLock() dict.__init__(self, *args, **kwargs) @@ -162,7 +96,7 @@ def __setitem__(self, attr, val): dict.__setitem__(self, attr, val) finally: self.unlock() - + def __contains__(self, attr): self.lock() try: @@ -188,19 +122,19 @@ def clear(self): def lock(self): self.mutex.acquire() - + def unlock(self): self.mutex.release() def __deepcopy__(self, memo): raise Exception("deepcopy not implemented") - + class ThreadsafeList(list): """Extends list so that getitem, setitem, and contains are all thread-safe. Also adds lock/unlock functions for extended exclusive operations Converts all sub-lists and dicts to threadsafe as well. """ - + def __init__(self, *args, **kwargs): self.mutex = threading.RLock() list.__init__(self, *args, **kwargs) @@ -222,7 +156,7 @@ def __setitem__(self, attr, val): list.__setitem__(self, attr, val) finally: self.unlock() - + def __contains__(self, attr): self.lock() try: @@ -238,17 +172,17 @@ def __len__(self): finally: self.unlock() return val - + def lock(self): self.mutex.acquire() - + def unlock(self): self.mutex.release() def __deepcopy__(self, memo): raise Exception("deepcopy not implemented") - - + + def makeThreadsafe(obj): if type(obj) is dict: return ThreadsafeDict(obj) @@ -258,8 +192,8 @@ def makeThreadsafe(obj): return obj else: raise Exception("Not sure how to make object of type %s thread-safe" % str(type(obj))) - - + + class Locker(object): def __init__(self, lock): self.lock = lock @@ -283,10 +217,10 @@ def __init__(self, *args): self[k] = args[0][k] else: raise Exception("CaselessDict may only be instantiated with a single dict.") - + #def keys(self): #return self.keyMap.values() - + def __setitem__(self, key, val): kl = key.lower() if kl in self.keyMap: @@ -294,30 +228,30 @@ def __setitem__(self, key, val): else: OrderedDict.__setitem__(self, key, val) self.keyMap[kl] = key - + def __getitem__(self, key): kl = key.lower() if kl not in self.keyMap: raise KeyError(key) return OrderedDict.__getitem__(self, self.keyMap[kl]) - + def __contains__(self, key): return key.lower() in self.keyMap - + def update(self, d): for k, v in d.iteritems(): self[k] = v - + def copy(self): return CaselessDict(OrderedDict.copy(self)) - + def __delitem__(self, key): kl = key.lower() if kl not in self.keyMap: raise KeyError(key) OrderedDict.__delitem__(self, self.keyMap[kl]) del self.keyMap[kl] - + def __deepcopy__(self, memo): raise Exception("deepcopy not implemented") @@ -329,34 +263,34 @@ def clear(self): class ProtectedDict(dict): """ - A class allowing read-only 'view' of a dict. + A class allowing read-only 'view' of a dict. The object can be treated like a normal dict, but will never modify the original dict it points to. Any values accessed from the dict will also be read-only. """ def __init__(self, data): self._data_ = data - + ## List of methods to directly wrap from _data_ wrapMethods = ['_cmp_', '__contains__', '__eq__', '__format__', '__ge__', '__gt__', '__le__', '__len__', '__lt__', '__ne__', '__reduce__', '__reduce_ex__', '__repr__', '__str__', 'count', 'has_key', 'iterkeys', 'keys', ] - + ## List of methods which wrap from _data_ but return protected results protectMethods = ['__getitem__', '__iter__', 'get', 'items', 'values'] - + ## List of methods to disable disableMethods = ['__delitem__', '__setitem__', 'clear', 'pop', 'popitem', 'setdefault', 'update'] - - - ## Template methods + + + # # Template methods def wrapMethod(methodName): return lambda self, *a, **k: getattr(self._data_, methodName)(*a, **k) - + def protectMethod(methodName): return lambda self, *a, **k: protect(getattr(self._data_, methodName)(*a, **k)) - + def error(self, *args, **kargs): raise Exception("Can not modify read-only list.") - - + + ## Directly (and explicitly) wrap some methods from _data_ ## Many of these methods can not be intercepted using __getattribute__, so they ## must be implemented explicitly @@ -371,33 +305,33 @@ def error(self, *args, **kargs): for methodName in disableMethods: locals()[methodName] = error - + ## Add a few extra methods. def copy(self): raise Exception("It is not safe to copy protected dicts! (instead try deepcopy, but be careful.)") - + def itervalues(self): for v in self._data_.itervalues(): yield protect(v) - + def iteritems(self): for k, v in self._data_.iteritems(): yield (k, protect(v)) - + def deepcopy(self): return copy.deepcopy(self._data_) - + def __deepcopy__(self, memo): return copy.deepcopy(self._data_, memo) - + class ProtectedList(collections.Sequence): """ - A class allowing read-only 'view' of a list or dict. + A class allowing read-only 'view' of a list or dict. The object can be treated like a normal list, but will never modify the original list it points to. Any values accessed from the list will also be read-only. - + Note: It would be nice if we could inherit from list or tuple so that isinstance checks would work. However, doing this causes tuple(obj) to return unprotected results (importantly, this means unpacking into function arguments will also fail) @@ -405,28 +339,28 @@ class ProtectedList(collections.Sequence): def __init__(self, data): self._data_ = data #self.__mro__ = (ProtectedList, object) - + ## List of methods to directly wrap from _data_ wrapMethods = ['__contains__', '__eq__', '__format__', '__ge__', '__gt__', '__le__', '__len__', '__lt__', '__ne__', '__reduce__', '__reduce_ex__', '__repr__', '__str__', 'count', 'index'] - + ## List of methods which wrap from _data_ but return protected results protectMethods = ['__getitem__', '__getslice__', '__mul__', '__reversed__', '__rmul__'] - + ## List of methods to disable disableMethods = ['__delitem__', '__delslice__', '__iadd__', '__imul__', '__setitem__', '__setslice__', 'append', 'extend', 'insert', 'pop', 'remove', 'reverse', 'sort'] - - - ## Template methods + + + # # Template methods def wrapMethod(methodName): return lambda self, *a, **k: getattr(self._data_, methodName)(*a, **k) - + def protectMethod(methodName): return lambda self, *a, **k: protect(getattr(self._data_, methodName)(*a, **k)) - + def error(self, *args, **kargs): raise Exception("Can not modify read-only list.") - - + + ## Directly (and explicitly) wrap some methods from _data_ ## Many of these methods can not be intercepted using __getattribute__, so they ## must be implemented explicitly @@ -441,13 +375,13 @@ def error(self, *args, **kargs): for methodName in disableMethods: locals()[methodName] = error - + ## Add a few extra methods. def __iter__(self): for item in self._data_: yield protect(item) - - + + def __add__(self, op): if isinstance(op, ProtectedList): return protect(self._data_.__add__(op._data_)) @@ -455,7 +389,7 @@ def __add__(self, op): return protect(self._data_.__add__(op)) else: raise TypeError("Argument must be a list.") - + def __radd__(self, op): if isinstance(op, ProtectedList): return protect(op._data_.__add__(self._data_)) @@ -463,13 +397,13 @@ def __radd__(self, op): return protect(op.__add__(self._data_)) else: raise TypeError("Argument must be a list.") - + def deepcopy(self): return copy.deepcopy(self._data_) - + def __deepcopy__(self, memo): return copy.deepcopy(self._data_, memo) - + def poop(self): raise Exception("This is a list. It does not poop.") @@ -478,29 +412,29 @@ class ProtectedTuple(collections.Sequence): """ A class allowing read-only 'view' of a tuple. The object can be treated like a normal tuple, but its contents will be returned as protected objects. - + Note: It would be nice if we could inherit from list or tuple so that isinstance checks would work. However, doing this causes tuple(obj) to return unprotected results (importantly, this means unpacking into function arguments will also fail) """ def __init__(self, data): self._data_ = data - + ## List of methods to directly wrap from _data_ wrapMethods = ['__contains__', '__eq__', '__format__', '__ge__', '__getnewargs__', '__gt__', '__hash__', '__le__', '__len__', '__lt__', '__ne__', '__reduce__', '__reduce_ex__', '__repr__', '__str__', 'count', 'index'] - + ## List of methods which wrap from _data_ but return protected results protectMethods = ['__getitem__', '__getslice__', '__iter__', '__add__', '__mul__', '__reversed__', '__rmul__'] - - - ## Template methods + + + # # Template methods def wrapMethod(methodName): return lambda self, *a, **k: getattr(self._data_, methodName)(*a, **k) - + def protectMethod(methodName): return lambda self, *a, **k: protect(getattr(self._data_, methodName)(*a, **k)) - - + + ## Directly (and explicitly) wrap some methods from _data_ ## Many of these methods can not be intercepted using __getattribute__, so they ## must be implemented explicitly @@ -511,14 +445,14 @@ def protectMethod(methodName): for methodName in protectMethods: locals()[methodName] = protectMethod(methodName) - + ## Add a few extra methods. def deepcopy(self): return copy.deepcopy(self._data_) - + def __deepcopy__(self, memo): return copy.deepcopy(self._data_, memo) - + def protect(obj): @@ -530,14 +464,14 @@ def protect(obj): return ProtectedTuple(obj) else: return obj - - + + if __name__ == '__main__': d = {'x': 1, 'y': [1,2], 'z': ({'a': 2, 'b': [3,4], 'c': (5,6)}, 1, 2)} dp = protect(d) - + l = [1, 'x', ['a', 'b'], ('c', 'd'), {'x': 1, 'y': 2}] lp = protect(l) - + t = (1, 'x', ['a', 'b'], ('c', 'd'), {'x': 1, 'y': 2}) - tp = protect(t) \ No newline at end of file + tp = protect(t) From b993c64c48a864882b67f58e87608544f202c978 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 18 Oct 2013 09:46:48 -0400 Subject: [PATCH 090/121] Workaround for OrderedDict bug: import from 'ordereddict' backport module if available --- pyqtgraph/pgcollections.py | 135 +++++++++++++++++++------------------ 1 file changed, 70 insertions(+), 65 deletions(-) diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py index b01985266b..4de1b7d0db 100644 --- a/pyqtgraph/pgcollections.py +++ b/pyqtgraph/pgcollections.py @@ -15,74 +15,79 @@ try: from collections import OrderedDict -except: - # Deprecated; this class is now present in Python 2.7 as collections.OrderedDict - # Only keeping this around for python2.6 support. - class OrderedDict(dict): - """extends dict so that elements are iterated in the order that they were added. - Since this class can not be instantiated with regular dict notation, it instead uses - a list of tuples: - od = OrderedDict([(key1, value1), (key2, value2), ...]) - items set using __setattr__ are added to the end of the key list. - """ - - def __init__(self, data=None): - self.order = [] - if data is not None: - for i in data: - self[i[0]] = i[1] - - def __setitem__(self, k, v): - if not self.has_key(k): - self.order.append(k) - dict.__setitem__(self, k, v) - - def __delitem__(self, k): - self.order.remove(k) - dict.__delitem__(self, k) +except ImportError: + # fallback: try to use the ordereddict backport when using python 2.6 + try: + from ordereddict import OrderedDict + except ImportError: + # backport not installed: use local OrderedDict + # Deprecated; this class is now present in Python 2.7 as collections.OrderedDict + # Only keeping this around for python2.6 support. + class OrderedDict(dict): + """extends dict so that elements are iterated in the order that they were added. + Since this class can not be instantiated with regular dict notation, it instead uses + a list of tuples: + od = OrderedDict([(key1, value1), (key2, value2), ...]) + items set using __setattr__ are added to the end of the key list. + """ + + def __init__(self, data=None): + self.order = [] + if data is not None: + for i in data: + self[i[0]] = i[1] + + def __setitem__(self, k, v): + if not self.has_key(k): + self.order.append(k) + dict.__setitem__(self, k, v) + + def __delitem__(self, k): + self.order.remove(k) + dict.__delitem__(self, k) - def keys(self): - return self.order[:] - - def items(self): - it = [] - for k in self.keys(): - it.append((k, self[k])) - return it - - def values(self): - return [self[k] for k in self.order] - - def remove(self, key): - del self[key] - #self.order.remove(key) - - def __iter__(self): - for k in self.order: - yield k - - def update(self, data): - """Works like dict.update, but accepts list-of-tuples as well as dict.""" - if isinstance(data, dict): - for k, v in data.iteritems(): - self[k] = v - else: - for k,v in data: - self[k] = v - - def copy(self): - return OrderedDict(self.items()) + def keys(self): + return self.order[:] - def itervalues(self): - for k in self.order: - yield self[k] - - def iteritems(self): - for k in self.order: - yield (k, self[k]) + def items(self): + it = [] + for k in self.keys(): + it.append((k, self[k])) + return it + + def values(self): + return [self[k] for k in self.order] + + def remove(self, key): + del self[key] + #self.order.remove(key) + + def __iter__(self): + for k in self.order: + yield k + + def update(self, data): + """Works like dict.update, but accepts list-of-tuples as well as dict.""" + if isinstance(data, dict): + for k, v in data.iteritems(): + self[k] = v + else: + for k,v in data: + self[k] = v + + def copy(self): + return OrderedDict(self.items()) - def __deepcopy__(self, memo): - return OrderedDict([(k, copy.deepcopy(v, memo)) for k, v in self.iteritems()]) + def itervalues(self): + for k in self.order: + yield self[k] + + def iteritems(self): + for k in self.order: + yield (k, self[k]) + + def __deepcopy__(self, memo): + return OrderedDict([(k, copy.deepcopy(v, memo)) for k, v in self.iteritems()]) From 390f78c8107190b276c08787d817397a4d6bd548 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 18 Oct 2013 14:47:49 -0400 Subject: [PATCH 091/121] Fixed improper tick spacing and axis scaling This requires an API change: - AxisItem.setScale(float) has the usual behavior - AxisItem.setScale(None) is no longer allowed. Instead use: - AxisItem.enableAutoSIPrefix(bool) to enable/disable SI prefix scaling Also makes the API more intuitive since these features are now accessed and implemented independently. --- pyqtgraph/graphicsItems/AxisItem.py | 67 ++++++++++++++++++----------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index d82f5d4123..16cf46525e 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -69,7 +69,8 @@ def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLen self.tickLength = maxTickLength self._tickLevels = None ## used to override the automatic ticking system with explicit ticks self.scale = 1.0 - self.autoScale = True + self.autoSIPrefix = True + self.autoSIPrefixScale = 1.0 self.setRange(0, 1) @@ -149,8 +150,8 @@ def showLabel(self, show=True): self.setWidth() else: self.setHeight() - if self.autoScale: - self.setScale() + if self.autoSIPrefix: + self.updateAutoSIPrefix() def setLabel(self, text=None, units=None, unitPrefix=None, **args): """Set the text displayed adjacent to the axis. @@ -195,10 +196,10 @@ def setLabel(self, text=None, units=None, unitPrefix=None, **args): def labelString(self): if self.labelUnits == '': - if self.scale == 1.0: + if not self.autoSIPrefix or self.autoSIPrefixScale == 1.0: units = '' else: - units = asUnicode('(x%g)') % (1.0/self.scale) + units = asUnicode('(x%g)') % (1.0/self.autoSIPrefixScale) else: #print repr(self.labelUnitPrefix), repr(self.labelUnits) units = asUnicode('(%s%s)') % (self.labelUnitPrefix, self.labelUnits) @@ -295,22 +296,22 @@ def setScale(self, scale=None): to 'V' then a scale of 1000 would cause the axis to display values -100 to 100 and the units would appear as 'mV' """ - if scale is None: - #if self.drawLabel: ## If there is a label, then we are free to rescale the values - if self.label.isVisible(): - #d = self.range[1] - self.range[0] - #(scale, prefix) = fn.siScale(d / 2.) - (scale, prefix) = fn.siScale(max(abs(self.range[0]), abs(self.range[1]))) - if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. - scale = 1.0 - prefix = '' - self.setLabel(unitPrefix=prefix) - else: - scale = 1.0 - self.autoScale = True - else: - self.setLabel(unitPrefix='') - self.autoScale = False + #if scale is None: + ##if self.drawLabel: ## If there is a label, then we are free to rescale the values + #if self.label.isVisible(): + ##d = self.range[1] - self.range[0] + ##(scale, prefix) = fn.siScale(d / 2.) + #(scale, prefix) = fn.siScale(max(abs(self.range[0]), abs(self.range[1]))) + #if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. + #scale = 1.0 + #prefix = '' + #self.setLabel(unitPrefix=prefix) + #else: + #scale = 1.0 + #self.autoScale = True + #else: + #self.setLabel(unitPrefix='') + #self.autoScale = False if scale != self.scale: self.scale = scale @@ -318,14 +319,32 @@ def setScale(self, scale=None): self.picture = None self.update() + def enableAutoSIPrefix(self, enable=True): + self.autoSIPrefix = enable + + def updateAutoSIPrefix(self): + if self.label.isVisible(): + (scale, prefix) = fn.siScale(max(abs(self.range[0]*self.scale), abs(self.range[1]*self.scale))) + if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. + scale = 1.0 + prefix = '' + self.setLabel(unitPrefix=prefix) + else: + scale = 1.0 + + self.autoSIPrefixScale = scale + self.picture = None + self.update() + + def setRange(self, mn, mx): """Set the range of values displayed by the axis. Usually this is handled automatically by linking the axis to a ViewBox with :func:`linkToView `""" if any(np.isinf((mn, mx))) or any(np.isnan((mn, mx))): raise Exception("Not setting range to [%s, %s]" % (str(mn), str(mx))) self.range = [mn, mx] - if self.autoScale: - self.setScale() + if self.autoSIPrefix: + self.updateAutoSIPrefix() self.picture = None self.update() @@ -756,7 +775,7 @@ def generateDrawSpecs(self, p): ## Get the list of strings to display for this level if tickStrings is None: spacing, values = tickLevels[i] - strings = self.tickStrings(values, self.scale, spacing) + strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing) else: strings = tickStrings[i] From 03fd45c24a909e896e17173dfeff6c0db57c688d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 18 Oct 2013 14:59:17 -0400 Subject: [PATCH 092/121] Updated documentation to match previous API change --- pyqtgraph/graphicsItems/AxisItem.py | 42 +++++++++++++---------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 16cf46525e..531e3e9a5c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -287,32 +287,12 @@ def setPen(self, pen): def setScale(self, scale=None): """ - Set the value scaling for this axis. Values on the axis are multiplied - by this scale factor before being displayed as text. By default (scale=None), - this scaling value is automatically determined based on the visible range - and the axis units are updated to reflect the chosen scale factor. + Set the value scaling for this axis. - For example: If the axis spans values from -0.1 to 0.1 and has units set - to 'V' then a scale of 1000 would cause the axis to display values -100 to 100 - and the units would appear as 'mV' + Setting this value causes the axis to draw ticks and tick labels as if + the view coordinate system were scaled. By default, the axis scaling is + 1.0. """ - #if scale is None: - ##if self.drawLabel: ## If there is a label, then we are free to rescale the values - #if self.label.isVisible(): - ##d = self.range[1] - self.range[0] - ##(scale, prefix) = fn.siScale(d / 2.) - #(scale, prefix) = fn.siScale(max(abs(self.range[0]), abs(self.range[1]))) - #if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. - #scale = 1.0 - #prefix = '' - #self.setLabel(unitPrefix=prefix) - #else: - #scale = 1.0 - #self.autoScale = True - #else: - #self.setLabel(unitPrefix='') - #self.autoScale = False - if scale != self.scale: self.scale = scale self.setLabel() @@ -320,6 +300,20 @@ def setScale(self, scale=None): self.update() def enableAutoSIPrefix(self, enable=True): + """ + Enable (or disable) automatic SI prefix scaling on this axis. + + When enabled, this feature automatically determines the best SI prefix + to prepend to the label units, while ensuring that axis values are scaled + accordingly. + + For example, if the axis spans values from -0.1 to 0.1 and has units set + to 'V' then the axis would display values -100 to 100 + and the units would appear as 'mV' + + This feature is enabled by default, and is only available when a suffix + (unit string) is provided to display on the label. + """ self.autoSIPrefix = enable def updateAutoSIPrefix(self): From f19df05bdf273bcf51de45859cd81d8ba060ed66 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 18 Oct 2013 15:02:17 -0400 Subject: [PATCH 093/121] Allow AxisItem.setScale(None) to retain API backward compatibility. --- pyqtgraph/graphicsItems/AxisItem.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 531e3e9a5c..93efb45c03 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -293,6 +293,11 @@ def setScale(self, scale=None): the view coordinate system were scaled. By default, the axis scaling is 1.0. """ + # Deprecated usage, kept for backward compatibility + if scale is None: + scale = 1.0 + self.enableAutoSIPrefix(True) + if scale != self.scale: self.scale = scale self.setLabel() From bb2ecd033c5a31da14e92472181dcae1f7eb0827 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 18 Oct 2013 15:10:44 -0400 Subject: [PATCH 094/121] minor fix --- pyqtgraph/graphicsItems/AxisItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 93efb45c03..bfc8644ad8 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -320,6 +320,7 @@ def enableAutoSIPrefix(self, enable=True): (unit string) is provided to display on the label. """ self.autoSIPrefix = enable + self.updateAutoSIPrefix() def updateAutoSIPrefix(self): if self.label.isVisible(): From 84a845185eb60aa8ad846ed8dc762ec6a2b613d0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 20 Oct 2013 11:06:57 -0400 Subject: [PATCH 095/121] Fix: when ViewBox is resized, update range if it is linked to another view --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index d7fd49e514..e57ea1ee12 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -308,7 +308,10 @@ def clear(self): ch.setParentItem(None) def resizeEvent(self, ev): + #print self.name, "ViewBox.resizeEvent", self.size() #self.setRange(self.range, padding=0) + self.linkedXChanged() + self.linkedYChanged() self.updateAutoRange() self.updateMatrix() self.sigStateChanged.emit(self) @@ -365,6 +368,7 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru ============= ===================================================================== """ + #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding changes = {} @@ -770,6 +774,7 @@ def linkedViewChanged(self, view, axis): if self.linksBlocked or view is None: return + #print self.name, "ViewBox.linkedViewChanged", axis, view.viewRange()[axis] vr = view.viewRect() vg = view.screenGeometry() sg = self.screenGeometry() From 662af1a9c5b6e1d419790f807360c9e171c4fc9b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 25 Oct 2013 10:31:30 -0400 Subject: [PATCH 096/121] ignore test directories in top-level __init__ imports --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 12a4f90fb3..f6eafb6031 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -139,7 +139,7 @@ def importModules(path, globals, locals, excludes=()): d = os.path.join(os.path.split(globals['__file__'])[0], path) files = set() for f in frozenSupport.listdir(d): - if frozenSupport.isdir(os.path.join(d, f)) and f != '__pycache__': + if frozenSupport.isdir(os.path.join(d, f)) and f not in ['__pycache__', 'tests']: files.add(f) elif f[-3:] == '.py' and f != '__init__.py': files.add(f[:-3]) From ab1b1c6adf61ae358d8ad376f48f2b657b39ba72 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 25 Oct 2013 10:33:41 -0400 Subject: [PATCH 097/121] Added ViewBox test suite --- pyqtgraph/graphicsItems/tests/ViewBox.py | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 pyqtgraph/graphicsItems/tests/ViewBox.py diff --git a/pyqtgraph/graphicsItems/tests/ViewBox.py b/pyqtgraph/graphicsItems/tests/ViewBox.py new file mode 100644 index 0000000000..a04e9a29ed --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/ViewBox.py @@ -0,0 +1,74 @@ +""" +ViewBox test cases: + +* call setRange then resize; requested range must be fully visible +* lockAspect works correctly for arbitrary aspect ratio +* autoRange works correctly with aspect locked +* call setRange with aspect locked, then resize +* AutoRange with all the bells and whistles + * item moves / changes transformation / changes bounds + * pan only + * fractional range + + +""" + +import pyqtgraph as pg +app = pg.mkQApp() +win = pg.GraphicsWindow() +vb = win.addViewBox(name="image view") +vb.setAspectLocked() +p1 = win.addPlot(name="plot 1") +p2 = win.addPlot(name="plot 2", row=1, col=0) +win.ci.layout.setRowFixedHeight(1, 150) +win.ci.layout.setColumnFixedWidth(1, 150) + +def viewsMatch(): + r0 = pg.np.array(vb.viewRange()) + r1 = pg.np.array(p1.vb.viewRange()[1]) + r2 = pg.np.array(p2.vb.viewRange()[1]) + match = (abs(r0[1]-r1) <= (abs(r1) * 0.001)).all() and (abs(r0[0]-r2) <= (abs(r2) * 0.001)).all() + return match + +p1.setYLink(vb) +p2.setXLink(vb) +print "link views match:", viewsMatch() +win.show() +print "show views match:", viewsMatch() +imgData = pg.np.zeros((10, 10)) +imgData[0] = 1 +imgData[-1] = 1 +imgData[:,0] = 1 +imgData[:,-1] = 1 +img = pg.ImageItem(imgData) +vb.addItem(img) +p1.plot(x=imgData.sum(axis=0), y=range(10)) +p2.plot(x=range(10), y=imgData.sum(axis=1)) +print "add items views match:", viewsMatch() +#p1.setAspectLocked() +#grid = pg.GridItem() +#vb.addItem(grid) + + +#app.processEvents() +#print "init views match:", viewsMatch() +#p2.setYRange(-300, 300) +#print "setRange views match:", viewsMatch() +#app.processEvents() +#print "setRange views match (after update):", viewsMatch() + +#print "--lock aspect--" +#p1.setAspectLocked(True) +#print "lockAspect views match:", viewsMatch() +#p2.setYRange(-200, 200) +#print "setRange views match:", viewsMatch() +#app.processEvents() +#print "setRange views match (after update):", viewsMatch() + +#win.resize(100, 600) +#app.processEvents() +#vb.setRange(xRange=[-10, 10], padding=0) +#app.processEvents() +#win.resize(600, 100) +#app.processEvents() +#print vb.viewRange() From a4103dd1526ad4d159ae3a9c74cff7e859efafb0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Nov 2013 17:10:59 -0500 Subject: [PATCH 098/121] Mid-way through overhaul. Proposed code path looks like: setRange -> updateViewRange -> matrix dirty -> sigRangeChanged ... -> prepareForPaint -> updateAutoRange, updateMatrix if dirty --- pyqtgraph/graphicsItems/GraphicsObject.py | 3 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 314 +++++++++++++++------ pyqtgraph/graphicsItems/tests/ViewBox.py | 83 ++++-- 3 files changed, 280 insertions(+), 120 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsObject.py b/pyqtgraph/graphicsItems/GraphicsObject.py index e4c5cd81cb..d8f55d2742 100644 --- a/pyqtgraph/graphicsItems/GraphicsObject.py +++ b/pyqtgraph/graphicsItems/GraphicsObject.py @@ -12,6 +12,7 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): """ _qtBaseClass = QtGui.QGraphicsObject def __init__(self, *args): + self.__inform_view_on_changes = True QtGui.QGraphicsObject.__init__(self, *args) self.setFlag(self.ItemSendsGeometryChanges) GraphicsItem.__init__(self) @@ -20,7 +21,7 @@ def itemChange(self, change, value): ret = QtGui.QGraphicsObject.itemChange(self, change, value) if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]: self.parentChanged() - if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: + if self.__inform_view_on_changes and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: self.informViewBoundsChanged() ## workaround for pyqt bug: diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index e57ea1ee12..5241489c0d 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -17,6 +17,10 @@ class ChildGroup(ItemGroup): sigItemsChanged = QtCore.Signal() + def __init__(self, parent): + ItemGroup.__init__(self, parent) + # excempt from telling view when transform changes + self._GraphicsObject__inform_view_on_change = False def itemChange(self, change, value): ret = ItemGroup.itemChange(self, change, value) @@ -195,6 +199,21 @@ def close(self): def implements(self, interface): return interface == 'ViewBox' + def itemChange(self, change, value): + ret = GraphicsWidget.itemChange(self, change, value) + if change == self.ItemSceneChange: + scene = self.scene() + if scene is not None: + scene.sigPrepareForPaint.disconnect(self.prepareForPaint) + elif change == self.ItemSceneHasChanged: + scene = self.scene() + if scene is not None: + scene.sigPrepareForPaint.connect(self.prepareForPaint) + return ret + + def prepareForPaint(self): + #print "prepare" + pass def getState(self, copy=True): """Return the current state of the ViewBox. @@ -308,12 +327,14 @@ def clear(self): ch.setParentItem(None) def resizeEvent(self, ev): - #print self.name, "ViewBox.resizeEvent", self.size() + print self.name, "ViewBox.resizeEvent", self.size() #self.setRange(self.range, padding=0) + x,y = self.targetRange() + self.setRange(xRange=x, yRange=y, padding=0) self.linkedXChanged() self.linkedYChanged() self.updateAutoRange() - self.updateMatrix() + #self.updateMatrix() self.sigStateChanged.emit(self) self.background.setRect(self.rect()) #self._itemBoundsCache.clear() @@ -357,77 +378,97 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru Set the visible range of the ViewBox. Must specify at least one of *rect*, *xRange*, or *yRange*. - ============= ===================================================================== + ================== ===================================================================== **Arguments** - *rect* (QRectF) The full range that should be visible in the view box. - *xRange* (min,max) The range that should be visible along the x-axis. - *yRange* (min,max) The range that should be visible along the y-axis. - *padding* (float) Expand the view by a fraction of the requested range. - By default, this value is set between 0.02 and 0.1 depending on - the size of the ViewBox. - ============= ===================================================================== + *rect* (QRectF) The full range that should be visible in the view box. + *xRange* (min,max) The range that should be visible along the x-axis. + *yRange* (min,max) The range that should be visible along the y-axis. + *padding* (float) Expand the view by a fraction of the requested range. + By default, this value is set between 0.02 and 0.1 depending on + the size of the ViewBox. + *update* (bool) If True, update the range of the ViewBox immediately. + Otherwise, the update is deferred until before the next render. + *disableAutoRange* (bool) If True, auto-ranging is diabled. Otherwise, it is left + unchanged. + ================== ===================================================================== """ - #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding + print self.name, "ViewBox.setRange", rect, xRange, yRange, padding - changes = {} + changes = {} # axes + setRequested = [False, False] if rect is not None: changes = {0: [rect.left(), rect.right()], 1: [rect.top(), rect.bottom()]} + setRequested = [True, True] if xRange is not None: changes[0] = xRange + setRequested[0] = True if yRange is not None: changes[1] = yRange + setRequested[1] = True if len(changes) == 0: print(rect) raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect))) + # Update axes one at a time changed = [False, False] for ax, range in changes.items(): - if padding is None: - xpad = self.suggestPadding(ax) - else: - xpad = padding mn = min(range) mx = max(range) - if mn == mx: ## If we requested 0 range, try to preserve previous scale. Otherwise just pick an arbitrary scale. + + # If we requested 0 range, try to preserve previous scale. + # Otherwise just pick an arbitrary scale. + if mn == mx: dy = self.state['viewRange'][ax][1] - self.state['viewRange'][ax][0] if dy == 0: dy = 1 mn -= dy*0.5 mx += dy*0.5 xpad = 0.0 - if any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])): - raise Exception("Not setting range [%s, %s]" % (str(mn), str(mx))) + # Make sure no nan/inf get through + if not all(np.isfinite([mn, mx])): + raise Exception("Cannot set range [%s, %s]" % (str(mn), str(mx))) + + # Apply padding + if padding is None: + xpad = self.suggestPadding(ax) + else: + xpad = padding p = (mx-mn) * xpad mn -= p mx += p + + # Set target range if self.state['targetRange'][ax] != [mn, mx]: self.state['targetRange'][ax] = [mn, mx] changed[ax] = True - aspect = self.state['aspectLocked'] # size ratio / view ratio - if aspect is not False and len(changes) == 1: - ## need to adjust orthogonal target range to match - size = [self.width(), self.height()] - tr1 = self.state['targetRange'][ax] - tr2 = self.state['targetRange'][1-ax] - if size[1] == 0 or aspect == 0: - ratio = 1.0 - else: - ratio = (size[0] / float(size[1])) / aspect - if ax == 0: - ratio = 1.0 / ratio - w = (tr1[1]-tr1[0]) * ratio - d = 0.5 * (w - (tr2[1]-tr2[0])) - self.state['targetRange'][1-ax] = [tr2[0]-d, tr2[1]+d] - - - + # Update viewRange to match targetRange as closely as possible while + # accounting for aspect ratio constraint + lockX, lockY = setRequested + if lockX and lockY: + lockX = False + lockY = False + self.updateViewRange(lockX, lockY) + + # If ortho axes have auto-visible-only, update them now + # Note that aspect ratio constraints and auto-visible probably do not work together.. + if changed[0] and self.state['autoVisibleOnly'][1]: + self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? + elif changed[1] and self.state['autoVisibleOnly'][0]: + self.updateAutoRange() - if any(changed) and disableAutoRange: + # If nothing has changed, we are done. + if not any(changed): + #if update and self.matrixNeedsUpdate: + #self.updateMatrix(changed) + return + + # Disable auto-range if needed + if disableAutoRange: if all(changed): ax = ViewBox.XYAxes elif changed[0]: @@ -436,26 +477,26 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru ax = ViewBox.YAxis self.enableAutoRange(ax, False) - self.sigStateChanged.emit(self) - self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) - - if update and (any(changed) or self.matrixNeedsUpdate): - self.updateMatrix(changed) + # Update target rect for debugging + if self.target.isVisible(): + self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) - if not update and any(changed): - self.matrixNeedsUpdate = True + ## Update view matrix only if requested + #if update: + #self.updateMatrix(changed) + ## Otherwise, indicate that the matrix needs to be updated + #else: + #self.matrixNeedsUpdate = True - for ax, range in changes.items(): - link = self.linkedView(ax) - if link is not None: - link.linkedViewChanged(self, ax) + ## Inform linked views that the range has changed <> + #for ax, range in changes.items(): + #link = self.linkedView(ax) + #if link is not None: + #link.linkedViewChanged(self, ax) + - if changed[0] and self.state['autoVisibleOnly'][1]: - self.updateAutoRange() - elif changed[1] and self.state['autoVisibleOnly'][0]: - self.updateAutoRange() def setYRange(self, min, max, padding=None, update=True): """ @@ -572,7 +613,7 @@ def translateBy(self, t=None, x=None, y=None): - def enableAutoRange(self, axis=None, enable=True): + def enableAutoRange(self, axis=None, enable=True, x=None, y=None): """ Enable (or disable) auto-range for *axis*, which may be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes for both (if *axis* is omitted, both axes will be changed). @@ -584,25 +625,40 @@ def enableAutoRange(self, axis=None, enable=True): #if not enable: #import traceback #traceback.print_stack() - + + # support simpler interface: + if x is not None or y is not None: + if x is not None: + self.enableAutoRange(ViewBox.XAxis, x) + if y is not None: + self.enableAutoRange(ViewBox.YAxis, y) + return + if enable is True: enable = 1.0 if axis is None: axis = ViewBox.XYAxes + needAutoRangeUpdate = False + if axis == ViewBox.XYAxes or axis == 'xy': - self.state['autoRange'][0] = enable - self.state['autoRange'][1] = enable + axes = [0, 1] elif axis == ViewBox.XAxis or axis == 'x': - self.state['autoRange'][0] = enable + axes = [0] elif axis == ViewBox.YAxis or axis == 'y': - self.state['autoRange'][1] = enable + axes = [1] else: raise Exception('axis argument must be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes.') - if enable: + for ax in axes: + if self.state['autoRange'][ax] != enable: + self.state['autoRange'][ax] = enable + needAutoRangeUpdate |= (enable is not False) + + if needAutoRangeUpdate: self.updateAutoRange() + self.sigStateChanged.emit(self) def disableAutoRange(self, axis=None): @@ -728,6 +784,7 @@ def linkView(self, axis, view): if oldLink is not None: try: getattr(oldLink, signal).disconnect(slot) + oldLink.sigResized.disconnect(slot) except TypeError: ## This can occur if the view has been deleted already pass @@ -738,6 +795,7 @@ def linkView(self, axis, view): else: self.state['linkedViews'][axis] = weakref.ref(view) getattr(view, signal).connect(slot) + view.sigResized.connect(slot) if view.autoRangeEnabled()[axis] is not False: self.enableAutoRange(axis, False) slot() @@ -1233,47 +1291,126 @@ def childrenBoundingRect(self, *args, **kwds): bounds = QtCore.QRectF(range[0][0], range[1][0], range[0][1]-range[0][0], range[1][1]-range[1][0]) return bounds + def updateViewRange(self, forceX=False, forceY=False): + ## Update viewRange to match targetRange as closely as possible, given + ## aspect ratio constraints. - - def updateMatrix(self, changed=None): - ## Make the childGroup's transform match the requested range. + viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] - if changed is None: - changed = [False, False] - changed = list(changed) + # Make correction for aspect ratio constraint + aspect = self.state['aspectLocked'] # size ratio / view ratio tr = self.targetRect() bounds = self.rect() - - ## set viewRect, given targetRect and possibly aspect ratio constraint - aspect = self.state['aspectLocked'] - if aspect is False or bounds.height() == 0: - self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] - else: + if aspect is not False and tr.width() != 0 and bounds.width() != 0: ## aspect is (widget w/h) / (view range w/h) + ## This is the view range aspect ratio we have requested targetRatio = tr.width() / tr.height() ## This is the view range aspect ratio we need to obey aspect constraint viewRatio = (bounds.width() / bounds.height()) / aspect - if targetRatio > viewRatio: + # Decide which range to keep unchanged + print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] + if all(setRequested): + ax = 0 if targetRatio > viewRatio else 1 + print "ax:", ax + elif setRequested[0]: + ax = 0 + else: + ax = 1 + + #### these should affect viewRange, not targetRange! + if ax == 0: ## view range needs to be taller than target dy = 0.5 * (tr.width() / viewRatio - tr.height()) if dy != 0: changed[1] = True - self.state['viewRange'] = [ - self.state['targetRange'][0][:], - [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] - ] + self.state['targetRange'][1] = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] + changed[1] = True else: ## view range needs to be wider than target dx = 0.5 * (tr.height() * viewRatio - tr.width()) if dx != 0: changed[0] = True - self.state['viewRange'] = [ - [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], - self.state['targetRange'][1][:] - ] + self.state['targetRange'][0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] + changed[0] = True + + + ## need to adjust orthogonal target range to match + #size = [self.width(), self.height()] + #tr1 = self.state['targetRange'][ax] + #tr2 = self.state['targetRange'][1-ax] + #if size[1] == 0 or aspect == 0: + #ratio = 1.0 + #else: + #ratio = (size[0] / float(size[1])) / aspect + #if ax == 0: + #ratio = 1.0 / ratio + #w = (tr1[1]-tr1[0]) * ratio + #d = 0.5 * (w - (tr2[1]-tr2[0])) + #self.state['targetRange'][1-ax] = [tr2[0]-d, tr2[1]+d] + #changed[1-ax] = True + + self.state['viewRange'] = viewRange + + # emit range change signals here! + if changed[0]: + self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) + if changed[1]: + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) + if any(changed): + self.sigRangeChanged.emit(self, self.state['viewRange']) + + # Inform linked views that the range has changed <> + for ax, range in changes.items(): + link = self.linkedView(ax) + if link is not None: + link.linkedViewChanged(self, ax) + + self._matrixNeedsUpdate = True + + def updateMatrix(self, changed=None): + ## Make the childGroup's transform match the requested viewRange. + + #if changed is None: + #changed = [False, False] + #changed = list(changed) + #tr = self.targetRect() + #bounds = self.rect() + + ## set viewRect, given targetRect and possibly aspect ratio constraint + #self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] + + #aspect = self.state['aspectLocked'] + #if aspect is False or bounds.height() == 0: + #self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] + #else: + ### aspect is (widget w/h) / (view range w/h) + + ### This is the view range aspect ratio we have requested + #targetRatio = tr.width() / tr.height() + ### This is the view range aspect ratio we need to obey aspect constraint + #viewRatio = (bounds.width() / bounds.height()) / aspect + + #if targetRatio > viewRatio: + ### view range needs to be taller than target + #dy = 0.5 * (tr.width() / viewRatio - tr.height()) + #if dy != 0: + #changed[1] = True + #self.state['viewRange'] = [ + #self.state['targetRange'][0][:], + #[self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] + #] + #else: + ### view range needs to be wider than target + #dx = 0.5 * (tr.height() * viewRatio - tr.width()) + #if dx != 0: + #changed[0] = True + #self.state['viewRange'] = [ + #[self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], + #self.state['targetRange'][1][:] + #] vr = self.viewRect() if vr.height() == 0 or vr.width() == 0: @@ -1294,15 +1431,16 @@ def updateMatrix(self, changed=None): self.childGroup.setTransform(m) - if changed[0]: - self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) - if changed[1]: - self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) - if any(changed): - self.sigRangeChanged.emit(self, self.state['viewRange']) + # moved to viewRangeChanged + #if changed[0]: + #self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) + #if changed[1]: + #self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) + #if any(changed): + #self.sigRangeChanged.emit(self, self.state['viewRange']) self.sigTransformChanged.emit(self) ## segfaults here: 1 - self.matrixNeedsUpdate = False + self._matrixNeedsUpdate = False def paint(self, p, opt, widget): if self.border is not None: diff --git a/pyqtgraph/graphicsItems/tests/ViewBox.py b/pyqtgraph/graphicsItems/tests/ViewBox.py index a04e9a29ed..91d9b6171b 100644 --- a/pyqtgraph/graphicsItems/tests/ViewBox.py +++ b/pyqtgraph/graphicsItems/tests/ViewBox.py @@ -15,41 +15,58 @@ import pyqtgraph as pg app = pg.mkQApp() -win = pg.GraphicsWindow() -vb = win.addViewBox(name="image view") -vb.setAspectLocked() -p1 = win.addPlot(name="plot 1") -p2 = win.addPlot(name="plot 2", row=1, col=0) -win.ci.layout.setRowFixedHeight(1, 150) -win.ci.layout.setColumnFixedWidth(1, 150) -def viewsMatch(): - r0 = pg.np.array(vb.viewRange()) - r1 = pg.np.array(p1.vb.viewRange()[1]) - r2 = pg.np.array(p2.vb.viewRange()[1]) - match = (abs(r0[1]-r1) <= (abs(r1) * 0.001)).all() and (abs(r0[0]-r2) <= (abs(r2) * 0.001)).all() - return match - -p1.setYLink(vb) -p2.setXLink(vb) -print "link views match:", viewsMatch() -win.show() -print "show views match:", viewsMatch() imgData = pg.np.zeros((10, 10)) -imgData[0] = 1 -imgData[-1] = 1 -imgData[:,0] = 1 -imgData[:,-1] = 1 -img = pg.ImageItem(imgData) -vb.addItem(img) -p1.plot(x=imgData.sum(axis=0), y=range(10)) -p2.plot(x=range(10), y=imgData.sum(axis=1)) -print "add items views match:", viewsMatch() -#p1.setAspectLocked() -#grid = pg.GridItem() -#vb.addItem(grid) +imgData[0] = 3 +imgData[-1] = 3 +imgData[:,0] = 3 +imgData[:,-1] = 3 + +def testLinkWithAspectLock(): + global win, vb + win = pg.GraphicsWindow() + vb = win.addViewBox(name="image view") + vb.setAspectLocked() + vb.enableAutoRange(x=False, y=False) + p1 = win.addPlot(name="plot 1") + p2 = win.addPlot(name="plot 2", row=1, col=0) + win.ci.layout.setRowFixedHeight(1, 150) + win.ci.layout.setColumnFixedWidth(1, 150) + + def viewsMatch(): + r0 = pg.np.array(vb.viewRange()) + r1 = pg.np.array(p1.vb.viewRange()[1]) + r2 = pg.np.array(p2.vb.viewRange()[1]) + match = (abs(r0[1]-r1) <= (abs(r1) * 0.001)).all() and (abs(r0[0]-r2) <= (abs(r2) * 0.001)).all() + return match + p1.setYLink(vb) + p2.setXLink(vb) + print "link views match:", viewsMatch() + win.show() + print "show views match:", viewsMatch() + img = pg.ImageItem(imgData) + vb.addItem(img) + vb.autoRange() + p1.plot(x=imgData.sum(axis=0), y=range(10)) + p2.plot(x=range(10), y=imgData.sum(axis=1)) + print "add items views match:", viewsMatch() + #p1.setAspectLocked() + #grid = pg.GridItem() + #vb.addItem(grid) + pg.QtGui.QApplication.processEvents() + pg.QtGui.QApplication.processEvents() + #win.resize(801, 600) +def testAspectLock(): + global win, vb + win = pg.GraphicsWindow() + vb = win.addViewBox(name="image view") + vb.setAspectLocked() + img = pg.ImageItem(imgData) + vb.addItem(img) + + #app.processEvents() #print "init views match:", viewsMatch() #p2.setYRange(-300, 300) @@ -72,3 +89,7 @@ def viewsMatch(): #win.resize(600, 100) #app.processEvents() #print vb.viewRange() + + +if __name__ == '__main__': + testLinkWithAspectLock() From 608352138761a44871740f3b2bd092c0ef300a4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Nov 2013 17:55:57 -0500 Subject: [PATCH 099/121] Initial success. Testing and further reorganization to follow. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 119 ++++++++++----------- 1 file changed, 59 insertions(+), 60 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 5241489c0d..aed0cb8b83 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -91,7 +91,8 @@ def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, self.addedItems = [] #self.gView = view #self.showGrid = showGrid - self.matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred + self._matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred + self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed. self.state = { @@ -212,8 +213,11 @@ def itemChange(self, change, value): return ret def prepareForPaint(self): - #print "prepare" - pass + autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) + if self._autoRangeNeedsUpdate and autoRangeEnabled: + self.updateAutoRange() + if self._matrixNeedsUpdate: + self.updateMatrix() def getState(self, copy=True): """Return the current state of the ViewBox. @@ -327,13 +331,14 @@ def clear(self): ch.setParentItem(None) def resizeEvent(self, ev): - print self.name, "ViewBox.resizeEvent", self.size() + #print self.name, "ViewBox.resizeEvent", self.size() #self.setRange(self.range, padding=0) - x,y = self.targetRange() - self.setRange(xRange=x, yRange=y, padding=0) + #x,y = self.targetRange() + #self.setRange(xRange=x, yRange=y, padding=0) self.linkedXChanged() self.linkedYChanged() self.updateAutoRange() + self.updateViewRange() #self.updateMatrix() self.sigStateChanged.emit(self) self.background.setRect(self.rect()) @@ -393,7 +398,7 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru ================== ===================================================================== """ - print self.name, "ViewBox.setRange", rect, xRange, yRange, padding + #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding changes = {} # axes setRequested = [False, False] @@ -454,19 +459,6 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru lockY = False self.updateViewRange(lockX, lockY) - # If ortho axes have auto-visible-only, update them now - # Note that aspect ratio constraints and auto-visible probably do not work together.. - if changed[0] and self.state['autoVisibleOnly'][1]: - self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? - elif changed[1] and self.state['autoVisibleOnly'][0]: - self.updateAutoRange() - - # If nothing has changed, we are done. - if not any(changed): - #if update and self.matrixNeedsUpdate: - #self.updateMatrix(changed) - return - # Disable auto-range if needed if disableAutoRange: if all(changed): @@ -476,13 +468,27 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru elif changed[1]: ax = ViewBox.YAxis self.enableAutoRange(ax, False) - - self.sigStateChanged.emit(self) - - # Update target rect for debugging - if self.target.isVisible(): - self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) + changed.append(True) + + # If nothing has changed, we are done. + if any(changed): + #if update and self.matrixNeedsUpdate: + #self.updateMatrix(changed) + #return + self.sigStateChanged.emit(self) + + # Update target rect for debugging + if self.target.isVisible(): + self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) + + # If ortho axes have auto-visible-only, update them now + # Note that aspect ratio constraints and auto-visible probably do not work together.. + if changed[0] and self.state['autoVisibleOnly'][1]: + self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? + elif changed[1] and self.state['autoVisibleOnly'][0]: + self.updateAutoRange() + ## Update view matrix only if requested #if update: #self.updateMatrix(changed) @@ -746,6 +752,7 @@ def updateAutoRange(self): args['disableAutoRange'] = False self.setRange(**args) finally: + self._autoRangeNeedsUpdate = False self._updatingRange = False def setXLink(self, view): @@ -891,7 +898,9 @@ def itemsChanged(self): def itemBoundsChanged(self, item): self._itemBoundsCache.pop(item, None) - self.updateAutoRange() + self._autoRangeNeedsUpdate = True + self.update() + #self.updateAutoRange() def invertY(self, b=True): """ @@ -1293,17 +1302,19 @@ def childrenBoundingRect(self, *args, **kwds): def updateViewRange(self, forceX=False, forceY=False): ## Update viewRange to match targetRange as closely as possible, given - ## aspect ratio constraints. + ## aspect ratio constraints. The *force* arguments are used to indicate + ## which axis (if any) should be unchanged when applying constraints. viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] + changed = [False, False] # Make correction for aspect ratio constraint + + ## aspect is (widget w/h) / (view range w/h) aspect = self.state['aspectLocked'] # size ratio / view ratio tr = self.targetRect() bounds = self.rect() if aspect is not False and tr.width() != 0 and bounds.width() != 0: - ## aspect is (widget w/h) / (view range w/h) - ## This is the view range aspect ratio we have requested targetRatio = tr.width() / tr.height() @@ -1311,14 +1322,15 @@ def updateViewRange(self, forceX=False, forceY=False): viewRatio = (bounds.width() / bounds.height()) / aspect # Decide which range to keep unchanged - print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] - if all(setRequested): - ax = 0 if targetRatio > viewRatio else 1 - print "ax:", ax - elif setRequested[0]: + #print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] + if forceX: ax = 0 - else: + elif forceY: ax = 1 + else: + # if we are not required to keep a particular axis unchanged, + # then make the entire target range visible + ax = 0 if targetRatio > viewRatio else 1 #### these should affect viewRange, not targetRange! if ax == 0: @@ -1326,44 +1338,31 @@ def updateViewRange(self, forceX=False, forceY=False): dy = 0.5 * (tr.width() / viewRatio - tr.height()) if dy != 0: changed[1] = True - self.state['targetRange'][1] = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] - changed[1] = True + viewRange[1] = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] else: ## view range needs to be wider than target dx = 0.5 * (tr.height() * viewRatio - tr.width()) if dx != 0: changed[0] = True - self.state['targetRange'][0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] - changed[0] = True - + viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] - ## need to adjust orthogonal target range to match - #size = [self.width(), self.height()] - #tr1 = self.state['targetRange'][ax] - #tr2 = self.state['targetRange'][1-ax] - #if size[1] == 0 or aspect == 0: - #ratio = 1.0 - #else: - #ratio = (size[0] / float(size[1])) / aspect - #if ax == 0: - #ratio = 1.0 / ratio - #w = (tr1[1]-tr1[0]) * ratio - #d = 0.5 * (w - (tr2[1]-tr2[0])) - #self.state['targetRange'][1-ax] = [tr2[0]-d, tr2[1]+d] - #changed[1-ax] = True - + changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange - # emit range change signals here! + # emit range change signals if changed[0]: self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) if changed[1]: self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) + if any(changed): self.sigRangeChanged.emit(self, self.state['viewRange']) + self.update() - # Inform linked views that the range has changed <> - for ax, range in changes.items(): + # Inform linked views that the range has changed + for ax in [0, 1]: + if not changed[ax]: + continue link = self.linkedView(ax) if link is not None: link.linkedViewChanged(self, ax) @@ -1377,7 +1376,7 @@ def updateMatrix(self, changed=None): #changed = [False, False] #changed = list(changed) #tr = self.targetRect() - #bounds = self.rect() + bounds = self.rect() ## set viewRect, given targetRect and possibly aspect ratio constraint #self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] From 0daae425a39e353fe86df2b4c9db2c0abf54c61a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Nov 2013 19:41:05 -0500 Subject: [PATCH 100/121] A bit more flow reorganization --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index aed0cb8b83..8dbaf9ef77 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -246,7 +246,8 @@ def setState(self, state): del state['linkedViews'] self.state.update(state) - self.updateMatrix() + #self.updateMatrix() + self.updateViewRange() self.sigStateChanged.emit(self) @@ -485,9 +486,11 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru # If ortho axes have auto-visible-only, update them now # Note that aspect ratio constraints and auto-visible probably do not work together.. if changed[0] and self.state['autoVisibleOnly'][1]: - self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? + self._autoRangeNeedsUpdate = True + #self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? elif changed[1] and self.state['autoVisibleOnly'][0]: - self.updateAutoRange() + self._autoRangeNeedsUpdate = True + #self.updateAutoRange() ## Update view matrix only if requested #if update: @@ -907,7 +910,8 @@ def invertY(self, b=True): By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis. """ self.state['yInverted'] = b - self.updateMatrix(changed=(False, True)) + #self.updateMatrix(changed=(False, True)) + self.updateViewRange() self.sigStateChanged.emit(self) def yInverted(self): @@ -933,7 +937,7 @@ def setAspectLocked(self, lock=True, ratio=1): self.state['aspectLocked'] = ratio if ratio != currentRatio: ## If this would change the current range, do that now #self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1]) - self.updateMatrix() + self.updateViewRange() self.sigStateChanged.emit(self) def childTransform(self): From 96a4ff7cd5235441b890433154c72972a7977fc9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 4 Nov 2013 22:07:43 -0500 Subject: [PATCH 101/121] Fixes: - ROI updates on sigTransformChanged - ViewBox is more careful about accepting all auto-range changes up to the point it is disabled, even if the auto-range calculation is deferred. --- examples/ROIExamples.py | 2 +- examples/crosshair.py | 4 ++- pyqtgraph/graphicsItems/ROI.py | 7 ++-- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 38 ++++++++++++---------- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py index a67e279d09..56b15bcf10 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -27,7 +27,7 @@ ## create GUI app = QtGui.QApplication([]) -w = pg.GraphicsWindow(size=(800,800), border=True) +w = pg.GraphicsWindow(size=(1000,800), border=True) w.setWindowTitle('pyqtgraph example: ROI Examples') text = """Data Selection From Image.
\n diff --git a/examples/crosshair.py b/examples/crosshair.py index c41dfff1f2..67d3cc5fc5 100644 --- a/examples/crosshair.py +++ b/examples/crosshair.py @@ -23,7 +23,9 @@ region = pg.LinearRegionItem() region.setZValue(10) -p2.addItem(region) +# Add the LinearRegionItem to the ViewBox, but tell the ViewBox to exclude this +# item when doing auto-range calculations. +p2.addItem(region, ignoreBounds=True) #pg.dbg() p1.setAutoVisible(y=True) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 033aab42a8..f6ce468006 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1320,7 +1320,6 @@ def generateShape(self): ## determine rotation of transform #m = self.sceneTransform() ## Qt bug: do not access sceneTransform() until we know this object has a scene. #mi = m.inverted()[0] - dt = self.deviceTransform() if dt is None: @@ -1339,10 +1338,10 @@ def generateShape(self): return dti.map(tr.map(self.path)) - def viewRangeChanged(self): - GraphicsObject.viewRangeChanged(self) + def viewTransformChanged(self): + GraphicsObject.viewTransformChanged(self) self._shape = None ## invalidate shape, recompute later if requested. - #self.updateShape() + self.update() #def itemChange(self, change, value): #if change == self.ItemScenePositionHasChanged: diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 8dbaf9ef77..f0ea1e57c0 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -213,8 +213,9 @@ def itemChange(self, change, value): return ret def prepareForPaint(self): - autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) - if self._autoRangeNeedsUpdate and autoRangeEnabled: + #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) + # don't check whether auto range is enabled here--only check when setting dirty flag. + if self._autoRangeNeedsUpdate: # and autoRangeEnabled: self.updateAutoRange() if self._matrixNeedsUpdate: self.updateMatrix() @@ -332,22 +333,15 @@ def clear(self): ch.setParentItem(None) def resizeEvent(self, ev): - #print self.name, "ViewBox.resizeEvent", self.size() - #self.setRange(self.range, padding=0) - #x,y = self.targetRange() - #self.setRange(xRange=x, yRange=y, padding=0) self.linkedXChanged() self.linkedYChanged() self.updateAutoRange() self.updateViewRange() - #self.updateMatrix() self.sigStateChanged.emit(self) self.background.setRect(self.rect()) - #self._itemBoundsCache.clear() - #self.linkedXChanged() - #self.linkedYChanged() self.sigResized.emit(self) + def viewRange(self): """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]""" return [x[:] for x in self.state['viewRange']] ## return copy @@ -485,10 +479,10 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru # If ortho axes have auto-visible-only, update them now # Note that aspect ratio constraints and auto-visible probably do not work together.. - if changed[0] and self.state['autoVisibleOnly'][1]: + if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False): self._autoRangeNeedsUpdate = True #self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? - elif changed[1] and self.state['autoVisibleOnly'][0]: + elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False): self._autoRangeNeedsUpdate = True #self.updateAutoRange() @@ -662,11 +656,18 @@ def enableAutoRange(self, axis=None, enable=True, x=None, y=None): for ax in axes: if self.state['autoRange'][ax] != enable: + # If we are disabling, do one last auto-range to make sure that + # previously scheduled auto-range changes are enacted + if enable is False and self._autoRangeNeedsUpdate: + self.updateAutoRange() + self.state['autoRange'][ax] = enable - needAutoRangeUpdate |= (enable is not False) - - if needAutoRangeUpdate: - self.updateAutoRange() + self._autoRangeNeedsUpdate |= (enable is not False) + self.update() + + + #if needAutoRangeUpdate: + # self.updateAutoRange() self.sigStateChanged.emit(self) @@ -901,8 +902,9 @@ def itemsChanged(self): def itemBoundsChanged(self, item): self._itemBoundsCache.pop(item, None) - self._autoRangeNeedsUpdate = True - self.update() + if (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False): + self._autoRangeNeedsUpdate = True + self.update() #self.updateAutoRange() def invertY(self, b=True): From ea8079334fb3b013f3330c78375e1c6f168a8cde Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Nov 2013 15:35:24 -0500 Subject: [PATCH 102/121] Correct ViewBox.translate to use setRange(x, y) when possible rather than making two calls. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index f0ea1e57c0..419b63065b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -608,11 +608,10 @@ def translateBy(self, t=None, x=None, y=None): self.setRange(vr.translated(t), padding=0) else: if x is not None: - x1, x2 = vr.left()+x, vr.right()+x - self.setXRange(x1, x2, padding=0) + x = vr.left()+x, vr.right()+x if y is not None: - y1, y2 = vr.top()+y, vr.bottom()+y - self.setYRange(y1, y2, padding=0) + y = vr.top()+y, vr.bottom()+y + self.setRange(xRange=x, yRange=y, padding=0) From 31928e70a5f5e3a8e5edd9f07da14f6373610721 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Nov 2013 23:14:27 -0500 Subject: [PATCH 103/121] Bugfixes: - GraphicsView.render now correctly invokes GraphicsScene.prepareForPaint - Fixed RemoteGraphicsView renderer to use new PyQt QImage API. - multiprocess.Process now pipes stdout/err directly to console when in debugging mode --- examples/RemoteGraphicsView.py | 3 ++- pyqtgraph/__init__.py | 6 ++++-- pyqtgraph/multiprocess/bootstrap.py | 4 +--- pyqtgraph/multiprocess/processes.py | 15 +++++++++++---- pyqtgraph/widgets/GraphicsView.py | 5 +++++ pyqtgraph/widgets/RemoteGraphicsView.py | 12 ++++++++++-- 6 files changed, 33 insertions(+), 12 deletions(-) diff --git a/examples/RemoteGraphicsView.py b/examples/RemoteGraphicsView.py index a5d869c94d..2b74a8c60c 100644 --- a/examples/RemoteGraphicsView.py +++ b/examples/RemoteGraphicsView.py @@ -13,7 +13,8 @@ app = pg.mkQApp() ## Create the widget -v = RemoteGraphicsView(debug=False) +v = RemoteGraphicsView(debug=False) # setting debug=True causes both processes to print information + # about interprocess communication v.show() v.setWindowTitle('pyqtgraph example: RemoteGraphicsView') diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f6eafb6031..810fecec9a 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -311,13 +311,15 @@ def image(*args, **kargs): return w show = image ## for backward compatibility -def dbg(): +def dbg(*args, **kwds): """ Create a console window and begin watching for exceptions. + + All arguments are passed to :func:`ConsoleWidget.__init__() `. """ mkQApp() from . import console - c = console.ConsoleWidget() + c = console.ConsoleWidget(*args, **kwds) c.catchAllExceptions() c.show() global consoles diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py index 4ecfb7da9d..b82debc27e 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -20,10 +20,8 @@ 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(**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 7d147a1d86..cf8023522d 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -48,9 +48,10 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de it must be picklable (bound methods are not). copySysPath If True, copy the contents of sys.path to the remote process debug If True, print detailed information about communication - with the child process. + with the child process. Note that this option may cause + strange behavior on some systems due to a python bug: + http://bugs.python.org/issue3905 ============ ============================================================= - """ if target is None: target = startEventLoop @@ -81,8 +82,14 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap)) ## note: we need all three streams to have their own PIPE due to this bug: - ## http://bugs.python.org/issue3905 - self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + ## http://bugs.python.org/issue3905 + if debug is True: # when debugging, we need to keep the usual stdout + stdout = sys.stdout + stderr = sys.stderr + else: + stdout = subprocess.PIPE + stderr = subprocess.PIPE + self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=stdout, stderr=stderr) targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to ## set its sys.path properly before unpickling the target diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 0c8921f685..fb53592919 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -147,6 +147,11 @@ def paintEvent(self, ev): #print "GV: paint", ev.rect() return QtGui.QGraphicsView.paintEvent(self, ev) + def render(self, *args, **kwds): + self.scene().prepareForPaint() + return QtGui.QGraphicsView.render(self, *args, **kwds) + + def close(self): self.centralWidget = None self.scene().clear() diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index 80f0fb4b5e..f8bbb6cfc8 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -18,12 +18,15 @@ class RemoteGraphicsView(QtGui.QWidget): """ def __init__(self, parent=None, *args, **kwds): + """ + The keyword arguments 'debug' and 'name', if specified, are passed to QtProcess.__init__(). + """ self._img = None self._imgReq = None 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(debug=kwds.pop('debug', False)) + self._proc = mp.QtProcess(debug=kwds.pop('debug', False), name=kwds.pop('name', None)) self.pg = self._proc._import('pyqtgraph') self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS) rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') @@ -123,6 +126,7 @@ class Renderer(GraphicsView): def __init__(self, *args, **kwds): ## Create shared memory for rendered image + #pg.dbg(namespace={'r': self}) 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 @@ -184,7 +188,11 @@ def renderView(self): self.img = QtGui.QImage(ch, self.width(), self.height(), QtGui.QImage.Format_ARGB32) else: address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0)) - self.img = QtGui.QImage(sip.voidptr(address), self.width(), self.height(), QtGui.QImage.Format_ARGB32) + try: + self.img = QtGui.QImage(sip.voidptr(address), self.width(), self.height(), QtGui.QImage.Format_ARGB32) + except TypeError: + # different versions of pyqt have different requirements here.. + self.img = QtGui.QImage(memoryview(buffer(self.shm)), 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 ccc5e6274a3771cfefa6df5dd2fef9b9e6832804 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 7 Nov 2013 12:05:05 -0500 Subject: [PATCH 104/121] Fixes: - GraphItem reports pixel margins to improve auto-range - ViewBox.setRange is more careful about disabling auto range for axes that are set --- pyqtgraph/graphicsItems/GraphItem.py | 5 +++++ pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 16 +++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphItem.py b/pyqtgraph/graphicsItems/GraphItem.py index 79f8804a63..b1f34baa4a 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -110,6 +110,11 @@ def paint(self, p, *args): def boundingRect(self): return self.scatter.boundingRect() + def dataBounds(self, *args, **kwds): + return self.scatter.dataBounds(*args, **kwds) + + def pixelPadding(self): + return self.scatter.pixelPadding() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 419b63065b..5f59d8bc02 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -394,6 +394,8 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru """ #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding + #import traceback + #traceback.print_stack() changes = {} # axes setRequested = [False, False] @@ -454,15 +456,11 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru lockY = False self.updateViewRange(lockX, lockY) - # Disable auto-range if needed + # Disable auto-range for each axis that was requested to be set if disableAutoRange: - if all(changed): - ax = ViewBox.XYAxes - elif changed[0]: - ax = ViewBox.XAxis - elif changed[1]: - ax = ViewBox.YAxis - self.enableAutoRange(ax, False) + xOff = False if setRequested[0] else None + yOff = False if setRequested[1] else None + self.enableAutoRange(x=xOff, y=yOff) changed.append(True) # If nothing has changed, we are done. @@ -1376,7 +1374,7 @@ def updateViewRange(self, forceX=False, forceY=False): def updateMatrix(self, changed=None): ## Make the childGroup's transform match the requested viewRange. - + #print self.name, "updateMAtrix", self.state['targetRange'] #if changed is None: #changed = [False, False] #changed = list(changed) From 810b90a1e63abed40f2df2037a50ce05db91826e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 10 Nov 2013 23:25:07 -0500 Subject: [PATCH 105/121] Minor fix in ScatterPlotItem handling of per-point data --- pyqtgraph/graphicsItems/AxisItem.py | 5 +++++ pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index bfc8644ad8..36516f8cd9 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -404,19 +404,24 @@ def boundingRect(self): return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) def paint(self, p, opt, widget): + prof = debug.Profiler('AxisItem.paint', disabled=True) if self.picture is None: try: picture = QtGui.QPicture() painter = QtGui.QPainter(picture) specs = self.generateDrawSpecs(painter) + prof.mark('generate specs') if specs is not None: self.drawPicture(painter, *specs) + prof.mark('draw picture') finally: painter.end() self.picture = picture #p.setRenderHint(p.Antialiasing, False) ## Sometimes we get a segfault here ??? #p.setRenderHint(p.TextAntialiasing, True) self.picture.play(p) + prof.finish() + def setTicks(self, ticks): diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 97f5aa8f61..befc23608b 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -517,7 +517,7 @@ def setPointData(self, data, dataSet=None, mask=None): ## Bug: If data is a numpy record array, then items from that array must be copied to dataSet one at a time. ## (otherwise they are converted to tuples and thus lose their field names. - if isinstance(data, np.ndarray) and len(data.dtype.fields) > 1: + if isinstance(data, np.ndarray) and (data.dtype.fields is not None)and len(data.dtype.fields) > 1: for i, rec in enumerate(data): dataSet['data'][i] = rec else: From f8772d179fd6e01988d89b669785f1529227f765 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 14 Nov 2013 12:16:31 -0500 Subject: [PATCH 106/121] removed unused variable --- pyqtgraph/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 14e4e076f3..337dfb677d 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -33,7 +33,6 @@ try: import scipy.ndimage HAVE_SCIPY = True - WEAVE_DEBUG = pg.getConfigOption('weaveDebug') if pg.getConfigOption('useWeave'): try: import scipy.weave From ef2ffdd88cb154d2da44306b9b9e5c3ec1b6cf62 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 14 Nov 2013 14:01:25 -0500 Subject: [PATCH 107/121] Fixed bug: ViewBox context menu elements are no longer deleted when using flowchart + pyside --- pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index bbb40efc33..5242ecdd5d 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -65,8 +65,18 @@ def __init__(self, view): self.leftMenu = QtGui.QMenu("Mouse Mode") group = QtGui.QActionGroup(self) - pan = self.leftMenu.addAction("3 button", self.set3ButtonMode) - zoom = self.leftMenu.addAction("1 button", self.set1ButtonMode) + + # This does not work! QAction _must_ be initialized with a permanent + # object as the parent or else it may be collected prematurely. + #pan = self.leftMenu.addAction("3 button", self.set3ButtonMode) + #zoom = self.leftMenu.addAction("1 button", self.set1ButtonMode) + pan = QtGui.QAction("3 button", self.leftMenu) + zoom = QtGui.QAction("1 button", self.leftMenu) + self.leftMenu.addAction(pan) + self.leftMenu.addAction(zoom) + pan.triggered.connect(self.set3ButtonMode) + zoom.triggered.connect(self.set1ButtonMode) + pan.setCheckable(True) zoom.setCheckable(True) pan.setActionGroup(group) From dac7eb581740e7958dcd8c1086216e77c20f6319 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 15 Nov 2013 11:00:12 -0800 Subject: [PATCH 108/121] Faster import of PyQtGraph. * RawImageWidget (and thus OpenGL) isn't imported by default anymore. * scipy.stats.scoreatpercentile is replaced by numpy.percentile. This commit has not been tested as the example runner is currently broken. --- pyqtgraph/__init__.py | 3 ++- pyqtgraph/graphicsItems/PlotCurveItem.py | 5 ++--- pyqtgraph/graphicsItems/ScatterPlotItem.py | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 810fecec9a..11e281a43d 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -178,7 +178,8 @@ def importAll(path, globals, locals, excludes=()): globals[k] = getattr(mod, k) importAll('graphicsItems', globals(), locals()) -importAll('widgets', globals(), locals(), excludes=['MatplotlibWidget', 'RemoteGraphicsView']) +importAll('widgets', globals(), locals(), + excludes=['MatplotlibWidget', 'RawImageWidget', 'RemoteGraphicsView']) from .imageview import * from .WidgetGroup import * diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 2fea3d33d0..321c6438e4 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -7,7 +7,6 @@ from scipy.fftpack import fft import numpy as np -import scipy.stats from .GraphicsObject import GraphicsObject import pyqtgraph.functions as fn from pyqtgraph import debug @@ -126,8 +125,8 @@ def dataBounds(self, ax, frac=1.0, orthoRange=None): else: mask = np.isfinite(d) d = d[mask] - b = (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) - + b = np.percentile(d, [50 * (1 - frac), 50 * (1 + frac)]) + ## 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'])) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index befc23608b..f1a5201dc9 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -4,7 +4,6 @@ from .GraphicsItem import GraphicsItem from .GraphicsObject import GraphicsObject import numpy as np -import scipy.stats import weakref import pyqtgraph.debug as debug from pyqtgraph.pgcollections import OrderedDict @@ -633,8 +632,8 @@ def dataBounds(self, ax, frac=1.0, orthoRange=None): else: mask = np.isfinite(d) d = d[mask] - return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) - + return np.percentile(d, [50 * (1 - frac), 50 * (1 + frac)]) + def pixelPadding(self): return self._maxSpotPxWidth*0.7072 From 25d666a1dacd406117652be9643e3dcbead77815 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 15 Nov 2013 22:05:09 -0500 Subject: [PATCH 109/121] Avoid calling QGraphicsWidget.itemChange--this causes segfault in python3 + pyqt Fixes #10 --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 5f59d8bc02..b8404ddcbe 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -201,7 +201,8 @@ def implements(self, interface): return interface == 'ViewBox' def itemChange(self, change, value): - ret = GraphicsWidget.itemChange(self, change, value) + # Note: Calling QWidget.itemChange causes segv in python 3 + PyQt + ret = QtGui.QGraphicsItem.itemChange(self, change, value) if change == self.ItemSceneChange: scene = self.scene() if scene is not None: From 8d7ab108fd6e723692f77a393fd145ca298cb7e9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 16 Nov 2013 20:23:41 -0500 Subject: [PATCH 110/121] Fixed PySide issues by removing itemChange methods from GraphicsWidget and ViewBox; Workaround is for ViewBox to see whether its scene has changed every time it paints. Fixes: 12 --- examples/VideoSpeedTest.py | 1 - examples/VideoTemplate.ui | 8 ++-- examples/VideoTemplate_pyqt.py | 55 +++++++++++++--------- examples/VideoTemplate_pyside.py | 10 ++-- pyqtgraph/graphicsItems/GraphicsWidget.py | 21 +++++---- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 50 +++++++++++++++----- 6 files changed, 90 insertions(+), 55 deletions(-) diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index d7a4e1e0e4..1341ec0e6f 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -13,7 +13,6 @@ from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE import numpy as np import pyqtgraph as pg -from pyqtgraph import RawImageWidget import scipy.ndimage as ndi import pyqtgraph.ptime as ptime diff --git a/examples/VideoTemplate.ui b/examples/VideoTemplate.ui index d73b0dc94f..9560a19be4 100644 --- a/examples/VideoTemplate.ui +++ b/examples/VideoTemplate.ui @@ -22,9 +22,6 @@ RawImageWidget - - true -
@@ -32,6 +29,9 @@ GraphicsView + ImageItem + + true + @@ -265,7 +265,7 @@ RawImageWidget QWidget -
pyqtgraph
+
pyqtgraph.widgets.RawImageWidget
1
diff --git a/examples/VideoTemplate_pyqt.py b/examples/VideoTemplate_pyqt.py index f61a5e4658..91fc1b1e1f 100644 --- a/examples/VideoTemplate_pyqt.py +++ b/examples/VideoTemplate_pyqt.py @@ -2,8 +2,8 @@ # Form implementation generated from reading ui file './VideoTemplate.ui' # -# Created: Tue Jul 9 23:38:17 2013 -# by: PyQt4 UI code generator 4.9.3 +# Created: Sat Nov 16 20:07:09 2013 +# by: PyQt4 UI code generator 4.10 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,16 @@ try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_MainWindow(object): def setupUi(self, MainWindow): @@ -25,10 +34,10 @@ def setupUi(self, MainWindow): self.gridLayout = QtGui.QGridLayout() self.gridLayout.setObjectName(_fromUtf8("gridLayout")) self.rawRadio = QtGui.QRadioButton(self.centralwidget) - self.rawRadio.setChecked(True) self.rawRadio.setObjectName(_fromUtf8("rawRadio")) self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1) self.gfxRadio = QtGui.QRadioButton(self.centralwidget) + self.gfxRadio.setChecked(True) self.gfxRadio.setObjectName(_fromUtf8("gfxRadio")) self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1) self.stack = QtGui.QStackedWidget(self.centralwidget) @@ -158,23 +167,23 @@ def setupUi(self, MainWindow): QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8)) - self.rawRadio.setText(QtGui.QApplication.translate("MainWindow", "RawImageWidget", None, QtGui.QApplication.UnicodeUTF8)) - self.gfxRadio.setText(QtGui.QApplication.translate("MainWindow", "GraphicsView + ImageItem", None, QtGui.QApplication.UnicodeUTF8)) - self.rawGLRadio.setText(QtGui.QApplication.translate("MainWindow", "RawGLImageWidget", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("MainWindow", "Data type", None, QtGui.QApplication.UnicodeUTF8)) - self.dtypeCombo.setItemText(0, QtGui.QApplication.translate("MainWindow", "uint8", None, QtGui.QApplication.UnicodeUTF8)) - self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8)) - self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8)) - self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8)) - self.rgbLevelsCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) - self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) - self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) - self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) - self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8)) - self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8)) - self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8)) - self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow", None)) + self.rawRadio.setText(_translate("MainWindow", "RawImageWidget", None)) + self.gfxRadio.setText(_translate("MainWindow", "GraphicsView + ImageItem", None)) + self.rawGLRadio.setText(_translate("MainWindow", "RawGLImageWidget", None)) + self.label.setText(_translate("MainWindow", "Data type", None)) + self.dtypeCombo.setItemText(0, _translate("MainWindow", "uint8", None)) + self.dtypeCombo.setItemText(1, _translate("MainWindow", "uint16", None)) + self.dtypeCombo.setItemText(2, _translate("MainWindow", "float", None)) + self.scaleCheck.setText(_translate("MainWindow", "Scale Data", None)) + self.rgbLevelsCheck.setText(_translate("MainWindow", "RGB", None)) + self.label_2.setText(_translate("MainWindow", "<--->", None)) + self.label_3.setText(_translate("MainWindow", "<--->", None)) + self.label_4.setText(_translate("MainWindow", "<--->", None)) + self.lutCheck.setText(_translate("MainWindow", "Use Lookup Table", None)) + self.alphaCheck.setText(_translate("MainWindow", "alpha", None)) + self.fpsLabel.setText(_translate("MainWindow", "FPS", None)) + self.rgbCheck.setText(_translate("MainWindow", "RGB", None)) -from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget -from pyqtgraph import GradientWidget, SpinBox, GraphicsView, RawImageWidget +from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget, RawImageWidget +from pyqtgraph import GradientWidget, SpinBox, GraphicsView diff --git a/examples/VideoTemplate_pyside.py b/examples/VideoTemplate_pyside.py index d0db5effd9..c1f8bc573b 100644 --- a/examples/VideoTemplate_pyside.py +++ b/examples/VideoTemplate_pyside.py @@ -2,8 +2,8 @@ # Form implementation generated from reading ui file './VideoTemplate.ui' # -# Created: Tue Jul 9 23:38:19 2013 -# by: pyside-uic 0.2.13 running on PySide 1.1.2 +# Created: Sat Nov 16 20:07:10 2013 +# by: pyside-uic 0.2.14 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! @@ -20,10 +20,10 @@ def setupUi(self, MainWindow): self.gridLayout = QtGui.QGridLayout() self.gridLayout.setObjectName("gridLayout") self.rawRadio = QtGui.QRadioButton(self.centralwidget) - self.rawRadio.setChecked(True) self.rawRadio.setObjectName("rawRadio") self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1) self.gfxRadio = QtGui.QRadioButton(self.centralwidget) + self.gfxRadio.setChecked(True) self.gfxRadio.setObjectName("gfxRadio") self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1) self.stack = QtGui.QStackedWidget(self.centralwidget) @@ -171,5 +171,5 @@ def retranslateUi(self, MainWindow): self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8)) self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget -from pyqtgraph import GradientWidget, SpinBox, GraphicsView, RawImageWidget +from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget, RawImageWidget +from pyqtgraph import GradientWidget, SpinBox, GraphicsView diff --git a/pyqtgraph/graphicsItems/GraphicsWidget.py b/pyqtgraph/graphicsItems/GraphicsWidget.py index 8f28d2080f..7650b125e4 100644 --- a/pyqtgraph/graphicsItems/GraphicsWidget.py +++ b/pyqtgraph/graphicsItems/GraphicsWidget.py @@ -20,16 +20,17 @@ def __init__(self, *args, **kargs): ## done by GraphicsItem init #GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() -## Removed because this causes segmentation faults. Don't know why. -# def itemChange(self, change, value): -# ret = QtGui.QGraphicsWidget.itemChange(self, change, value) ## segv occurs here -# if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]: -# self._updateView() -# return ret - - #def getMenu(self): - #pass - + # Removed due to https://bugreports.qt-project.org/browse/PYSIDE-86 + #def itemChange(self, change, value): + ## BEWARE: Calling QGraphicsWidget.itemChange can lead to crashing! + ##ret = QtGui.QGraphicsWidget.itemChange(self, change, value) ## segv occurs here + ## The default behavior is just to return the value argument, so we'll do that + ## without calling the original method. + #ret = value + #if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]: + #self._updateView() + #return ret + def setFixedHeight(self, h): self.setMaximumHeight(h) self.setMinimumHeight(h) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index b8404ddcbe..5ab118f76f 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -93,6 +93,8 @@ def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, #self.showGrid = showGrid self._matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed. + + self._lastScene = None ## stores reference to the last known scene this view was a part of. self.state = { @@ -200,18 +202,40 @@ def close(self): def implements(self, interface): return interface == 'ViewBox' - def itemChange(self, change, value): - # Note: Calling QWidget.itemChange causes segv in python 3 + PyQt - ret = QtGui.QGraphicsItem.itemChange(self, change, value) - if change == self.ItemSceneChange: - scene = self.scene() - if scene is not None: - scene.sigPrepareForPaint.disconnect(self.prepareForPaint) - elif change == self.ItemSceneHasChanged: - scene = self.scene() - if scene is not None: - scene.sigPrepareForPaint.connect(self.prepareForPaint) - return ret + # removed due to https://bugreports.qt-project.org/browse/PYSIDE-86 + #def itemChange(self, change, value): + ## Note: Calling QWidget.itemChange causes segv in python 3 + PyQt + ##ret = QtGui.QGraphicsItem.itemChange(self, change, value) + #ret = GraphicsWidget.itemChange(self, change, value) + #if change == self.ItemSceneChange: + #scene = self.scene() + #if scene is not None and hasattr(scene, 'sigPrepareForPaint'): + #scene.sigPrepareForPaint.disconnect(self.prepareForPaint) + #elif change == self.ItemSceneHasChanged: + #scene = self.scene() + #if scene is not None and hasattr(scene, 'sigPrepareForPaint'): + #scene.sigPrepareForPaint.connect(self.prepareForPaint) + #return ret + + def checkSceneChange(self): + # ViewBox needs to receive sigPrepareForPaint from its scene before + # being painted. However, we have no way of being informed when the + # scene has changed in order to make this connection. The usual way + # to do this is via itemChange(), but bugs prevent this approach + # (see above). Instead, we simply check at every paint to see whether + # (the scene has changed. + scene = self.scene() + if scene == self._lastScene: + return + if self._lastScene is not None and hasattr(self.lastScene, 'sigPrepareForPaint'): + self._lastScene.sigPrepareForPaint.disconnect(self.prepareForPaint) + if scene is not None and hasattr(scene, 'sigPrepareForPaint'): + scene.sigPrepareForPaint.connect(self.prepareForPaint) + self.prepareForPaint() + self._lastScene = scene + + + def prepareForPaint(self): #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) @@ -1446,6 +1470,8 @@ def updateMatrix(self, changed=None): self._matrixNeedsUpdate = False def paint(self, p, opt, widget): + self.checkSceneChange() + if self.border is not None: bounds = self.shape() p.setPen(self.border) From 1e8210498685d243331ffca5c15e8f532ac9e92c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 16 Nov 2013 21:51:55 -0500 Subject: [PATCH 111/121] Fixed running `python examples --test` for python3; needs to be tested under windows. --- examples/__main__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index 4aa23e8ed7..a397cf05fa 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -252,6 +252,7 @@ def testFile(name, f, exe, lib, graphicsSystem=None): else: process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) process.stdin.write(code.encode('UTF-8')) + process.stdin.close() ##? output = '' fail = False while True: @@ -266,8 +267,8 @@ def testFile(name, f, exe, lib, graphicsSystem=None): break time.sleep(1) process.kill() - #process.wait() - res = process.communicate() + #res = process.communicate() + res = (process.stdout.read(), process.stderr.read()) if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): print('.' * (50-len(name)) + 'FAILED') From 08be09ee408f750ff09b61742939b641edff1e68 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 17 Nov 2013 09:27:55 -0700 Subject: [PATCH 112/121] Fixed RemoteGraphicsView on windows - Avoid using authkey on windows; seems to be broken - Included yet another method of accessing shared memory as QImage --- pyqtgraph/multiprocess/processes.py | 22 ++++++++++++++++++---- pyqtgraph/multiprocess/remoteproxy.py | 6 +++--- pyqtgraph/widgets/RemoteGraphicsView.py | 12 +++++++----- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index cf8023522d..16fd6bab6a 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, debug=False): + def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20): """ ============ ============================================================= Arguments: @@ -63,19 +63,23 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de ## random authentication key authkey = os.urandom(20) + + ## Windows seems to have a hard time with hmac + if sys.platform.startswith('win'): + authkey = None + #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: if ex.errno != 98: raise port += 1 - + ## 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')) @@ -111,7 +115,7 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de self.proc.stdin.close() ## open connection for remote process - self.debugMsg('Listening for child process..') + self.debugMsg('Listening for child process on port %d, authkey=%s..' % (port, repr(authkey))) while True: try: conn = l.accept() @@ -140,7 +144,12 @@ def join(self, timeout=10): def startEventLoop(name, port, authkey, ppid, debug=False): + if debug: + import os + print('[%d] connecting to server at port localhost:%d, authkey=%s..' % (os.getpid(), port, repr(authkey))) conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) + if debug: + print('[%d] connected; starting remote proxy.' % os.getpid()) global HANDLER #ppid = 0 if not hasattr(os, 'getppid') else os.getppid() HANDLER = RemoteEventHandler(conn, name, ppid, debug=debug) @@ -380,7 +389,12 @@ def processRequests(self): self.timer.stop() def startQtEventLoop(name, port, authkey, ppid, debug=False): + if debug: + import os + print('[%d] connecting to server at port localhost:%d, authkey=%s..' % (os.getpid(), port, repr(authkey))) conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) + if debug: + print('[%d] connected; starting remote proxy.' % os.getpid()) from pyqtgraph.Qt import QtGui, QtCore #from PyQt4 import QtGui, QtCore app = QtGui.QApplication.instance() diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 702b10bcfd..eba42ef3d6 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -97,7 +97,6 @@ def processRequests(self): 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() @@ -108,7 +107,7 @@ def processRequests(self): self.handleRequest() numProcessed += 1 except ClosedError: - self.debugMsg(' processRequests: got ClosedError from handleRequest; setting exited=True.') + self.debugMsg('processRequests: got ClosedError from handleRequest; setting exited=True.') self.exited = True raise #except IOError as err: ## let handleRequest take care of this. @@ -121,7 +120,8 @@ def processRequests(self): print("Error in process %s" % self.name) sys.excepthook(*sys.exc_info()) - self.debugMsg(' processRequests: finished %d requests' % numProcessed) + if numProcessed > 0: + self.debugMsg('processRequests: finished %d requests' % numProcessed) return numProcessed def handleRequest(self): diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index f8bbb6cfc8..ac29f42610 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -188,11 +188,16 @@ def renderView(self): self.img = QtGui.QImage(ch, self.width(), self.height(), QtGui.QImage.Format_ARGB32) else: address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0)) + + # different versions of pyqt have different requirements here.. try: self.img = QtGui.QImage(sip.voidptr(address), self.width(), self.height(), QtGui.QImage.Format_ARGB32) except TypeError: - # different versions of pyqt have different requirements here.. - self.img = QtGui.QImage(memoryview(buffer(self.shm)), self.width(), self.height(), QtGui.QImage.Format_ARGB32) + try: + self.img = QtGui.QImage(memoryview(buffer(self.shm)), self.width(), self.height(), QtGui.QImage.Format_ARGB32) + except TypeError: + # Works on PyQt 4.9.6 + 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()) @@ -236,6 +241,3 @@ def leaveEvent(self, typ): ev = QtCore.QEvent(QtCore.QEvent.Type(typ)) return GraphicsView.leaveEvent(self, ev) - - - From 1418358bfb8722eef2385ed48c5b0fcffddf9324 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 17 Nov 2013 14:12:00 -0500 Subject: [PATCH 113/121] Fixed RemoteGraphicsView passing mouse events on python3 + pyside --- examples/initExample.py | 7 +++++++ pyqtgraph/multiprocess/processes.py | 5 ++--- pyqtgraph/widgets/RemoteGraphicsView.py | 15 +++++++++------ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/examples/initExample.py b/examples/initExample.py index d8022aba5f..b61b55cc79 100644 --- a/examples/initExample.py +++ b/examples/initExample.py @@ -33,3 +33,10 @@ QtGui.QApplication.setGraphicsSystem(gs) break +## Enable fault handling to give more helpful error messages on crash. +## Only available in python 3.3+ +try: + import faulthandler + faulthandler.enable() +except ImportError: + pass \ No newline at end of file diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 16fd6bab6a..42eb19102b 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -1,7 +1,7 @@ from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy import subprocess, atexit, os, sys, time, random, socket, signal import multiprocessing.connection -from pyqtgraph.Qt import USE_PYSIDE +import pyqtgraph as pg try: import cPickle as pickle except ImportError: @@ -98,7 +98,6 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de 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 send pid to child because windows does not have getppid - pyside = USE_PYSIDE ## Send everything the remote process needs to start correctly data = dict( @@ -108,7 +107,7 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de ppid=pid, targetStr=targetStr, path=sysPath, - pyside=pyside, + pyside=pg.Qt.USE_PYSIDE, debug=debug ) pickle.dump(data, self.proc.stdin) diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index ac29f42610..7270d44929 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -83,17 +83,17 @@ def paintEvent(self, ev): p.end() def mousePressEvent(self, ev): - self._view.mousePressEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + self._view.mousePressEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mousePressEvent(self, ev) def mouseReleaseEvent(self, ev): - self._view.mouseReleaseEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + self._view.mouseReleaseEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mouseReleaseEvent(self, ev) def mouseMoveEvent(self, ev): - self._view.mouseMoveEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + self._view.mouseMoveEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mouseMoveEvent(self, ev) @@ -103,16 +103,16 @@ def wheelEvent(self, ev): return QtGui.QWidget.wheelEvent(self, ev) def keyEvent(self, ev): - if self._view.keyEvent(ev.type(), int(ev.modifiers()), text, autorep, count): + if self._view.keyEvent(int(ev.type()), int(ev.modifiers()), text, autorep, count): ev.accept() return QtGui.QWidget.keyEvent(self, ev) def enterEvent(self, ev): - self._view.enterEvent(ev.type(), _callSync='off') + self._view.enterEvent(int(ev.type()), _callSync='off') return QtGui.QWidget.enterEvent(self, ev) def leaveEvent(self, ev): - self._view.leaveEvent(ev.type(), _callSync='off') + self._view.leaveEvent(int(ev.type()), _callSync='off') return QtGui.QWidget.leaveEvent(self, ev) def remoteProcess(self): @@ -206,18 +206,21 @@ def renderView(self): def mousePressEvent(self, typ, pos, gpos, btn, btns, mods): typ = QtCore.QEvent.Type(typ) + btn = QtCore.Qt.MouseButton(btn) btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) return GraphicsView.mousePressEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) def mouseMoveEvent(self, typ, pos, gpos, btn, btns, mods): typ = QtCore.QEvent.Type(typ) + btn = QtCore.Qt.MouseButton(btn) btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) return GraphicsView.mouseMoveEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) def mouseReleaseEvent(self, typ, pos, gpos, btn, btns, mods): typ = QtCore.QEvent.Type(typ) + btn = QtCore.Qt.MouseButton(btn) btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) return GraphicsView.mouseReleaseEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) From 5b156cd3d39c4546163ab390f224b881b19692e6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 17 Nov 2013 22:32:15 -0500 Subject: [PATCH 114/121] Fixes for multiprocess / RemoteGraphicsView: - Process now optionally wraps stdout/stderr from child process to circumvent a python bug - Added windows error number for port-in-use check - fixed segv caused by lost QImage input in pyside --- pyqtgraph/multiprocess/bootstrap.py | 1 + pyqtgraph/multiprocess/processes.py | 88 +++++++++++++++++++++---- pyqtgraph/widgets/RemoteGraphicsView.py | 29 ++++++-- 3 files changed, 97 insertions(+), 21 deletions(-) diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py index b82debc27e..bb71a7035c 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -20,6 +20,7 @@ if opts.pop('pyside', False): import PySide + targetStr = opts.pop('targetStr') target = pickle.loads(targetStr) ## unpickling the target should import everything we need diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 42eb19102b..4d32c999df 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, debug=False, timeout=20): + def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None): """ ============ ============================================================= Arguments: @@ -48,9 +48,13 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de it must be picklable (bound methods are not). copySysPath If True, copy the contents of sys.path to the remote process debug If True, print detailed information about communication - with the child process. Note that this option may cause - strange behavior on some systems due to a python bug: - http://bugs.python.org/issue3905 + with the child process. + wrapStdout If True (default on windows) then stdout and stderr from the + child process will be caught by the parent process and + forwarded to its stdout/stderr. This provides a workaround + for a python bug: http://bugs.python.org/issue3905 + but has the side effect that child output is significantly + delayed relative to the parent output. ============ ============================================================= """ if target is None: @@ -76,25 +80,32 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey) break except socket.error as ex: - if ex.errno != 98: + if ex.errno != 98 and ex.errno != 10048: # unix=98, win=10048 raise port += 1 + ## 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)) - ## note: we need all three streams to have their own PIPE due to this bug: - ## http://bugs.python.org/issue3905 - if debug is True: # when debugging, we need to keep the usual stdout - stdout = sys.stdout - stderr = sys.stderr - else: + if wrapStdout is None: + wrapStdout = sys.platform.startswith('win') + + if wrapStdout: + ## note: we need all three streams to have their own PIPE due to this bug: + ## http://bugs.python.org/issue3905 stdout = subprocess.PIPE stderr = subprocess.PIPE - self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=stdout, stderr=stderr) - + self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=stdout, stderr=stderr) + ## to circumvent the bug and still make the output visible, we use + ## background threads to pass data from pipes to stdout/stderr + self._stdoutForwarder = FileForwarder(self.proc.stdout, "stdout") + self._stderrForwarder = FileForwarder(self.proc.stderr, "stderr") + else: + 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 send pid to child because windows does not have getppid @@ -129,6 +140,7 @@ def __init__(self, name=None, target=None, executable=None, copySysPath=True, de self.debugMsg('Connected to child process.') atexit.register(self.join) + def join(self, timeout=10): self.debugMsg('Joining child process..') @@ -140,7 +152,16 @@ def join(self, timeout=10): raise Exception('Timed out waiting for remote process to end.') time.sleep(0.05) self.debugMsg('Child process exited. (%d)' % self.proc.returncode) - + + def debugMsg(self, msg): + if hasattr(self, '_stdoutForwarder'): + ## Lock output from subprocess to make sure we do not get line collisions + with self._stdoutForwarder.lock: + with self._stderrForwarder.lock: + RemoteEventHandler.debugMsg(self, msg) + else: + RemoteEventHandler.debugMsg(self, msg) + def startEventLoop(name, port, authkey, ppid, debug=False): if debug: @@ -409,4 +430,43 @@ def startQtEventLoop(name, port, authkey, ppid, debug=False): HANDLER.startEventTimer() app.exec_() +import threading +class FileForwarder(threading.Thread): + """ + Background thread that forwards data from one pipe to another. + This is used to catch data from stdout/stderr of the child process + and print it back out to stdout/stderr. We need this because this + bug: http://bugs.python.org/issue3905 _requires_ us to catch + stdout/stderr. + + *output* may be a file or 'stdout' or 'stderr'. In the latter cases, + sys.stdout/stderr are retrieved once for every line that is output, + which ensures that the correct behavior is achieved even if + sys.stdout/stderr are replaced at runtime. + """ + def __init__(self, input, output): + threading.Thread.__init__(self) + self.input = input + self.output = output + self.lock = threading.Lock() + self.start() + + def run(self): + if self.output == 'stdout': + while True: + line = self.input.readline() + with self.lock: + sys.stdout.write(line) + elif self.output == 'stderr': + while True: + line = self.input.readline() + with self.lock: + sys.stderr.write(line) + else: + while True: + line = self.input.readline() + with self.lock: + self.output.write(line) + + diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index 7270d44929..d44fd1c3eb 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -19,18 +19,26 @@ class RemoteGraphicsView(QtGui.QWidget): """ def __init__(self, parent=None, *args, **kwds): """ - The keyword arguments 'debug' and 'name', if specified, are passed to QtProcess.__init__(). + The keyword arguments 'useOpenGL' and 'backgound', if specified, are passed to the remote + GraphicsView.__init__(). All other keyword arguments are passed to multiprocess.QtProcess.__init__(). """ self._img = None self._imgReq = None 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(debug=kwds.pop('debug', False), name=kwds.pop('name', None)) + + # separate local keyword arguments from remote. + remoteKwds = {} + for kwd in ['useOpenGL', 'background']: + if kwd in kwds: + remoteKwds[kwd] = kwds.pop(kwd) + + self._proc = mp.QtProcess(**kwds) 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 = rpgRemote.Renderer(*args, **remoteKwds) self._view._setProxyOptions(deferGetattr=True) self.setFocusPolicy(QtCore.Qt.StrongFocus) @@ -72,7 +80,9 @@ def remoteSceneChanged(self, data): 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) + data = self.shm.read(w*h*4) + self._img = QtGui.QImage(data, w, h, QtGui.QImage.Format_ARGB32) + self._img.data = data # data must be kept alive or PySide 1.2.1 (and probably earlier) will crash. self.update() def paintEvent(self, ev): @@ -118,7 +128,12 @@ def leaveEvent(self, ev): def remoteProcess(self): """Return the remote process handle. (see multiprocess.remoteproxy.RemoteEventHandler)""" return self._proc - + + def close(self): + """Close the remote process. After this call, the widget will no longer be updated.""" + self._proc.close() + + class Renderer(GraphicsView): ## Created by the remote process to handle render requests @@ -146,9 +161,9 @@ def __init__(self, *args, **kwds): def close(self): self.shm.close() - if sys.platform.startswith('win'): + if not sys.platform.startswith('win'): self.shmFile.close() - + def shmFileName(self): if sys.platform.startswith('win'): return self.shmtag From 901e8ae596f30348fda81a355502939e5695929f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 19 Nov 2013 14:45:57 -0500 Subject: [PATCH 115/121] Fixed unicode handling in AxisItem label --- pyqtgraph/graphicsItems/AxisItem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 36516f8cd9..429ff49ca4 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -202,13 +202,13 @@ def labelString(self): units = asUnicode('(x%g)') % (1.0/self.autoSIPrefixScale) else: #print repr(self.labelUnitPrefix), repr(self.labelUnits) - units = asUnicode('(%s%s)') % (self.labelUnitPrefix, self.labelUnits) + units = asUnicode('(%s%s)') % (asUnicode(self.labelUnitPrefix), asUnicode(self.labelUnits)) - s = asUnicode('%s %s') % (self.labelText, units) + s = asUnicode('%s %s') % (asUnicode(self.labelText), asUnicode(units)) style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle]) - return asUnicode("%s") % (style, s) + return asUnicode("%s") % (style, asUnicode(s)) def _updateMaxTextSize(self, x): ## Informs that the maximum tick size orthogonal to the axis has From a972114b4fa8df133330021f10522d8ba65f05ee Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 21 Nov 2013 07:56:30 -0500 Subject: [PATCH 116/121] Fixed ViewBox not updating immediately after call to setAspectLocked --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 58 ++++------------------ 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 5ab118f76f..6ca2090c85 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -933,6 +933,9 @@ def invertY(self, b=True): """ By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis. """ + if self.state['yInverted'] == b: + return + self.state['yInverted'] = b #self.updateMatrix(changed=(False, True)) self.updateViewRange() @@ -947,7 +950,10 @@ def setAspectLocked(self, lock=True, ratio=1): By default, the ratio is set to 1; x and y both have the same scaling. This ratio can be overridden (xScale/yScale), or use None to lock in the current ratio. """ + if not lock: + if self.state['aspectLocked'] == False: + return self.state['aspectLocked'] = False else: rect = self.rect() @@ -958,10 +964,15 @@ def setAspectLocked(self, lock=True, ratio=1): currentRatio = (rect.width()/float(rect.height())) / (vr.width()/vr.height()) if ratio is None: ratio = currentRatio + if self.state['aspectLocked'] == ratio: # nothing to change + return self.state['aspectLocked'] = ratio if ratio != currentRatio: ## If this would change the current range, do that now #self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1]) self.updateViewRange() + + self.updateAutoRange() + self.updateViewRange() self.sigStateChanged.emit(self) def childTransform(self): @@ -1332,7 +1343,6 @@ def updateViewRange(self, forceX=False, forceY=False): ## Update viewRange to match targetRange as closely as possible, given ## aspect ratio constraints. The *force* arguments are used to indicate ## which axis (if any) should be unchanged when applying constraints. - viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] changed = [False, False] @@ -1399,46 +1409,8 @@ def updateViewRange(self, forceX=False, forceY=False): def updateMatrix(self, changed=None): ## Make the childGroup's transform match the requested viewRange. - #print self.name, "updateMAtrix", self.state['targetRange'] - #if changed is None: - #changed = [False, False] - #changed = list(changed) - #tr = self.targetRect() bounds = self.rect() - ## set viewRect, given targetRect and possibly aspect ratio constraint - #self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] - - #aspect = self.state['aspectLocked'] - #if aspect is False or bounds.height() == 0: - #self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] - #else: - ### aspect is (widget w/h) / (view range w/h) - - ### This is the view range aspect ratio we have requested - #targetRatio = tr.width() / tr.height() - ### This is the view range aspect ratio we need to obey aspect constraint - #viewRatio = (bounds.width() / bounds.height()) / aspect - - #if targetRatio > viewRatio: - ### view range needs to be taller than target - #dy = 0.5 * (tr.width() / viewRatio - tr.height()) - #if dy != 0: - #changed[1] = True - #self.state['viewRange'] = [ - #self.state['targetRange'][0][:], - #[self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] - #] - #else: - ### view range needs to be wider than target - #dx = 0.5 * (tr.height() * viewRatio - tr.width()) - #if dx != 0: - #changed[0] = True - #self.state['viewRange'] = [ - #[self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], - #self.state['targetRange'][1][:] - #] - vr = self.viewRect() if vr.height() == 0 or vr.width() == 0: return @@ -1458,14 +1430,6 @@ def updateMatrix(self, changed=None): self.childGroup.setTransform(m) - # moved to viewRangeChanged - #if changed[0]: - #self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) - #if changed[1]: - #self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) - #if any(changed): - #self.sigRangeChanged.emit(self, self.state['viewRange']) - self.sigTransformChanged.emit(self) ## segfaults here: 1 self._matrixNeedsUpdate = False From f05c10a80f12a461cd0f5dcb9ccbcea60688c6da Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 21 Nov 2013 09:57:56 -0500 Subject: [PATCH 117/121] removed unnecessary scipy import --- pyqtgraph/graphicsItems/PlotCurveItem.py | 2 -- pyqtgraph/graphicsItems/PlotDataItem.py | 28 ------------------------ 2 files changed, 30 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 321c6438e4..282145522f 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -5,7 +5,6 @@ except: HAVE_OPENGL = False -from scipy.fftpack import fft import numpy as np from .GraphicsObject import GraphicsObject import pyqtgraph.functions as fn @@ -26,7 +25,6 @@ class PlotCurveItem(GraphicsObject): Features: - Fast data update - - FFT display mode (accessed via PlotItem context menu) - Fill under curve - Mouse interaction diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 1e525f8372..87b47227f2 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -4,7 +4,6 @@ from .PlotCurveItem import PlotCurveItem from .ScatterPlotItem import ScatterPlotItem import numpy as np -import scipy import pyqtgraph.functions as fn import pyqtgraph.debug as debug import pyqtgraph as pg @@ -597,33 +596,6 @@ def dataBounds(self, ax, frac=1.0, orthoRange=None): 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 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 def pixelPadding(self): """ From 8deaf0866f8ffe54efb3e0f7412108261d4ec75a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 21 Nov 2013 13:37:01 -0500 Subject: [PATCH 118/121] avoid division by zero when ViewBox has size or aspect = 0 --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 6ca2090c85..3cbb1ea23b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1352,7 +1352,7 @@ def updateViewRange(self, forceX=False, forceY=False): aspect = self.state['aspectLocked'] # size ratio / view ratio tr = self.targetRect() bounds = self.rect() - if aspect is not False and tr.width() != 0 and bounds.width() != 0: + if aspect is not False and aspect != 0 and tr.height() != 0 and bounds.height() != 0: ## This is the view range aspect ratio we have requested targetRatio = tr.width() / tr.height() From 52c89bf202320b8c901e4a786214fda74cb82059 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 23 Nov 2013 20:27:14 -0500 Subject: [PATCH 119/121] added CHANGELOG --- CHANGELOG | 290 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 CHANGELOG diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000000..6b1579fdf4 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,290 @@ +pyqtgraph-0.9.8 + + API / behavior changes: + - ViewBox will auto-range when ImageItem changes shape + - AxisItem: + - Smarter about deciding which ticks get text + - AxisItem.setScale(float) has the usual behavior, but .setScale(None) + is deprecated. Instead use: + AxisItem.enableAutoSIPrefix(bool) to enable/disable SI prefix scaling + - Removed inf/nan checking from PlotDataItem and PlotCurveItem; improved + performance + + New Features: + - Support for dynamic downsampling and view clipping in PlotDataItem and + PlotItem + - Added 'connect' option to PlotDataItem and PlotCurveItem to affect which + line segments are drawn + - Support for FFT with non-uniform time sampling + - Added BarGraphItem + - OpenGL: + - Added export methods to GLViewWidget + - Wireframe meshes + - GLLinePLotItem gets antialiasing, accepts array of colors + - GLMeshItem accepts ShaderProgram or name of predefined program + - Added GLBarGraphItem + - LegendItem: + - User-draggable + - Allow custom ItemSamples + - Symbol support + - Support for removing items + - ScatterPlotWidget, ColorMapWidget, and DataFilterWidget are stable + - TableWidget: + - Made numerically sortable + - Added setEditable method + - AxisItem ability to truncate axis lines at the last tick + - arrayToQPath() added 'finite' connection mode which omits non-finite + values from connections + - pg.plot() and pg.PlotWidget() now accept background argument + - Allow QtProcess without local QApplication + - Support for dashing in mkPen() + - Added Dock.close() + - Added style options to flowchart connection lines + - Added parentChanged and viewChanged hooks to GraphicsItem + - Bidirectional pseudoScatter for beeswarm plots + - Added exit() function for working around PyQt exit crashes + - Added PolylineROI.getArrayRegion() + + Bugfixes: + - Many Python 3 compatibility fixes + - AxisItem: + - Correctly handles scaling with values that are not power of 10 + - Did not update grid line length when plot stretches + - Fixed unicode handling in AxisItem label + - ViewBox: + - Overhauled to fix issues with aspect locking + - ViewBox context menu elements are no longer deleted when using + flowchart with pyside + - Fixed view linking with inverted y axis + - Prevent auto-range disabling when dragging with one mouse axis diabled + - Ignore inf and nan when auto-ranging + - ParameterTree: + - fixed TextParameter editor disappearing after focus lost + - ListParameter: allow unhashable types as parameter values. + - Exporting: + - ImageExporter correctly handles QBrush with style=NoBrush + - SVGExporter text, gradients working correctly + - SVGExporter correctly handles coordinate corrections for groups with + mixed elements + - ImageView: + - Fixed auto-levelling when normalization options change + - Added autoHistogramRange argument to setImage + - ScatterPlotItem: + - Fixed crashes caused by ScatterPlotItem + - Fixed antialiasing + - arrayToQPath performance improved for python 3 + - Fixed makeQImage on many platforms (notably, on newer PyQt APIs) + - Removed unnecessary scipy imports for faster import + - GraphItem reports pixel margins to improve auto-range + - Add backport ordereddict to repository; old OrderedDict class is removed + - Corrected behavior of GraphicsView.setBackground + - Fixed PySide bug listing image formats + - Fixed QString -> str conversions in flowchart + - Unicode file name support when exporting + - Fixed MatplotlibWidget + PySide + - Fixed 3D view updating after every scene change + - Fixed handling of non-native dtypes when optimizing with weave + - RemoteGraphicsView fixed for PyQt 4.10, Python 3 + - Fixed GLLinePlotItem line width option + - HistogramLUTWidget obeys default background color + - ScaleBar complete rewrite + - GraphItem obeys antialiasing flag + - Workaround for PySide/QByteArray memory leak + - Fixed example --test on windows, python3 + - Luke finished dissertation + + +pyqtgraph-0.9.7 + + Bugfixes: + - ArrowItem auto range now works correctly + - Dock drag/drop fixed on PySide + - Made padding behavior consistent across ViewBox methods + - Fixed MeshData / python2.6 incompatibility + - Fixed ScatterPlotItem.setSize and .setPointData + - Workaround for PySide bug; GradientEditor fixed + - Prefer initially selecting PlotItem rather then ViewBox when exporting + - Fixed python3 import error with flowcharts + + Cleaned up examples, made code editable from example loader + Minor documentation updates + Features: + - Added GraphItem class for displaying networks/trees + - Added ColorMap class for mapping linear gradients and generating lookup + tables + (Provides gradient editor functionality without the GUI) + - Added ColorMapWidget for complex user-defined color mapping + - Added ScatterPlotWidget for exploring relationships in multi-column + tables + - Added ErrorBarItem + - SVG and image exporters can now copy to clipboard + - PlotItem gets new methods: addLine, setLabels, and listDataItems + - AxisItem gets setTickFont method + - Added functions.arrayToQPath, shared between GraphItem and PlotCurveItem + - Added gradient editors to parametertree + - Expanded documentation, added beginning of Qt crash course + + Bugfixes: + - Fixed auto-ranging bugs: ViewBox now properly handles pixel-padding + around data items + - ViewBox ignores bounds of zoom-rect when auto ranging + - Fixed AxisItem artifacts + - Fixed GraphicsItem.pixelVector caching bugs and simplified workaround for + fp-precision errors + - LinearRegionItem.hoverEvent obeys 'movable' flag + + + - Fixed PlotDataItem nan masking bugs + + + - Workaround for segmentation fault in QPainter.drawPixmapFragments + + + - multiprocess and RemoteGraphicsView work correctly in Windows. + + + - Expanded python 3 support + + + - Silenced weave errors by default + + + - Fixed " 'win' in sys.platform " occurrences matching 'darwin' (duh) + - Workaround for change in QImage API (PyQt 4.9.6) + - Fixed axis ordering bug in GLScatterPlotItem + +pyqtgraph-0.9.6 + + Features: + - Added GraphItem class for displaying networks/trees + - Added ColorMap class for mapping linear gradients and generating lookup + tables + (Provides gradient editor functionality without the GUI) + - Added ColorMapWidget for complex user-defined color mapping + - Added ScatterPlotWidget for exploring relationships in multi-column + tables + - Added ErrorBarItem + - SVG and image exporters can now copy to clipboard + - PlotItem gets new methods: addLine, setLabels, and listDataItems + - AxisItem gets setTickFont method + - Added functions.arrayToQPath, shared between GraphItem and PlotCurveItem + - Added gradient editors to parametertree + - Expanded documentation, added beginning of Qt crash course + + Bugfixes: + - Fixed auto-ranging bugs: ViewBox now properly handles pixel-padding + around data items + - ViewBox ignores bounds of zoom-rect when auto ranging + - Fixed AxisItem artifacts + - Fixed GraphicsItem.pixelVector caching bugs and simplified workaround for + fp-precision errors + - LinearRegionItem.hoverEvent obeys 'movable' flag + + + - Fixed PlotDataItem nan masking bugs + + + - Workaround for segmentation fault in QPainter.drawPixmapFragments + + + - multiprocess and RemoteGraphicsView work correctly in Windows. + + + - Expanded python 3 support + + + - Silenced weave errors by default + + + - Fixed " 'win' in sys.platform " occurrences matching 'darwin' (duh) + - Workaround for change in QImage API (PyQt 4.9.6) + - Fixed axis ordering bug in GLScatterPlotItem + Plotting performance improvements: + - AxisItem shows fewer tick levels in some cases. + - Lots of boundingRect and dataBounds caching + (improves ViewBox auto-range performance, especially with multiple plots) + - GraphicsScene avoids testing for hover intersections with non-hoverable + items + (much less slowdown when moving mouse over plots) + + Improved performance for remote plotting: + - reduced cost of transferring arrays between processes (pickle is too + slow) + - avoid unnecessary synchronous calls + + Added RemoteSpeedTest example + + +pyqtgraph-0.9.5 + + Plotting performance improvements: + - AxisItem shows fewer tick levels in some cases. + - Lots of boundingRect and dataBounds caching + (improves ViewBox auto-range performance, especially with multiple plots) + - GraphicsScene avoids testing for hover intersections with non-hoverable + items + (much less slowdown when moving mouse over plots) + + Improved performance for remote plotting: + - reduced cost of transferring arrays between processes (pickle is too + slow) + - avoid unnecessary synchronous calls + + Added RemoteSpeedTest example + Documentation: + - Added documentation on export system + - Added flowchart documentation and custom node example + + Bugfixes: + - prevent PlotCurveItem drawing shadow when unnecessary + - deprecated flowchart.Node.__getattr__ -- causes too many problems. + +pyqtgraph-0.9.4 + + Documentation: + - Added documentation on export system + - Added flowchart documentation and custom node example + + Bugfixes: + - prevent PlotCurveItem drawing shadow when unnecessary + - deprecated flowchart.Node.__getattr__ -- causes too many problems. + Bugfix: prevent adding invalid entry to sys.path when running examples + +pyqtgraph-0.9.3 + + Bugfix: prevent adding invalid entry to sys.path when running examples + Bugfixes: + - SVG export text elements use generic font-family as backup, corrected item + transformation issues + - Fixed RuntimeError caused when clearing item hierarchies from ViewBox + - Fixed example execution bug + + Packaging maintenance: + - Added missing files to MANIFEST.in, fixed setup.py package detection + - Added debian control files for building source packages + - Fixed version numbering in doc, __init__.py + +pyqtgraph-0.9.2 + + Bugfixes: + - SVG export text elements use generic font-family as backup, corrected item + transformation issues + - Fixed RuntimeError caused when clearing item hierarchies from ViewBox + - Fixed example execution bug + + Packaging maintenance: + - Added missing files to MANIFEST.in, fixed setup.py package detection + - Added debian control files for building source packages + - Fixed version numbering in doc, __init__.py + +pyqtgraph-0.9.1 + + Removed incorrect version numbers + Correction to setup.py - use install_requires to inform pip of dependencies. + Fixed doc version (again) + Added debian control files + bugfixes for new package structure + +pyqtgraph-0.9.0 + + * Initial release. From 51c16150590f7ce568685f961ba658ffdd2eb276 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 24 Nov 2013 10:16:45 -0500 Subject: [PATCH 120/121] added dates to changelog --- CHANGELOG | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6b1579fdf4..9fa1098425 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -pyqtgraph-0.9.8 +pyqtgraph-0.9.8 2013-11-24 API / behavior changes: - ViewBox will auto-range when ImageItem changes shape @@ -94,7 +94,7 @@ pyqtgraph-0.9.8 - Luke finished dissertation -pyqtgraph-0.9.7 +pyqtgraph-0.9.7 2013-02-25 Bugfixes: - ArrowItem auto range now works correctly @@ -153,7 +153,7 @@ pyqtgraph-0.9.7 - Workaround for change in QImage API (PyQt 4.9.6) - Fixed axis ordering bug in GLScatterPlotItem -pyqtgraph-0.9.6 +pyqtgraph-0.9.6 2013-02-14 Features: - Added GraphItem class for displaying networks/trees @@ -215,7 +215,7 @@ pyqtgraph-0.9.6 Added RemoteSpeedTest example -pyqtgraph-0.9.5 +pyqtgraph-0.9.5 2013-01-11 Plotting performance improvements: - AxisItem shows fewer tick levels in some cases. @@ -239,7 +239,7 @@ pyqtgraph-0.9.5 - prevent PlotCurveItem drawing shadow when unnecessary - deprecated flowchart.Node.__getattr__ -- causes too many problems. -pyqtgraph-0.9.4 +pyqtgraph-0.9.4 2013-01-07 Documentation: - Added documentation on export system @@ -250,7 +250,7 @@ pyqtgraph-0.9.4 - deprecated flowchart.Node.__getattr__ -- causes too many problems. Bugfix: prevent adding invalid entry to sys.path when running examples -pyqtgraph-0.9.3 +pyqtgraph-0.9.3 2012-12-29 Bugfix: prevent adding invalid entry to sys.path when running examples Bugfixes: @@ -264,7 +264,7 @@ pyqtgraph-0.9.3 - Added debian control files for building source packages - Fixed version numbering in doc, __init__.py -pyqtgraph-0.9.2 +pyqtgraph-0.9.2 2012-12-29 Bugfixes: - SVG export text elements use generic font-family as backup, corrected item @@ -277,7 +277,7 @@ pyqtgraph-0.9.2 - Added debian control files for building source packages - Fixed version numbering in doc, __init__.py -pyqtgraph-0.9.1 +pyqtgraph-0.9.1 2012-12-27 Removed incorrect version numbers Correction to setup.py - use install_requires to inform pip of dependencies. @@ -285,6 +285,6 @@ pyqtgraph-0.9.1 Added debian control files bugfixes for new package structure -pyqtgraph-0.9.0 +pyqtgraph-0.9.0 2012-12-27 * Initial release. From 08a19f56161cc0de97b051a7b74d0aac92f3d809 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 24 Nov 2013 11:06:53 -0500 Subject: [PATCH 121/121] Line-wrapped setup.py description --- setup.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8128a8517a..055b74e8bf 100644 --- a/setup.py +++ b/setup.py @@ -18,9 +18,13 @@ version='', description='Scientific Graphics and GUI Library for Python', long_description="""\ -PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and numpy. +PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and +numpy. -It is intended for use in mathematics / scientific / engineering applications. Despite being written entirely in python, the library is very fast due to its heavy leverage of numpy for number crunching, Qt's GraphicsView framework for 2D display, and OpenGL for 3D display. +It is intended for use in mathematics / scientific / engineering applications. +Despite being written entirely in python, the library is very fast due to its +heavy leverage of numpy for number crunching, Qt's GraphicsView framework for +2D display, and OpenGL for 3D display. """, license='MIT', url='http://www.pyqtgraph.org',