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 f885920f..bdf10523 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -235,9 +235,13 @@ except: """ % (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 62f69cb1..84a5c573 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 9ef64763..9d1684bd 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -309,10 +309,18 @@ class AxisItem(GraphicsWidget): 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 87b687bd..cf204007 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 @@ class ViewBox(GraphicsWidget): #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 623d1a28..3066223d 100644 --- a/pyqtgraph/flowchart/Terminal.py +++ b/pyqtgraph/flowchart/Terminal.py @@ -521,6 +521,8 @@ class ConnectionItem(GraphicsObject): 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 @@ class ConnectionItem(GraphicsObject): 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 @@ class ConnectionItem(GraphicsObject): 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 @@ class ConnectionItem(GraphicsObject): 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 d3aefa83..ed9e3357 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -54,6 +54,7 @@ CONFIG_OPTIONS = { '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 35a38ae7..c5a8ec3f 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 @@ class PlotCurveItem(GraphicsObject): 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 @@ class PlotCurveItem(GraphicsObject): self.path = self.generatePath(x,y) self.fillPath = None - path = self.path prof.mark('generate path') @@ -440,6 +444,65 @@ class PlotCurveItem(GraphicsObject): #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 a69131ef..84c05478 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -683,6 +683,9 @@ class ScatterPlotItem(GraphicsObject): 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 cf204007..654a12c9 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1048,10 +1048,10 @@ class ViewBox(GraphicsWidget): 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 dc4f875b..4c6a77ce 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -6,7 +6,7 @@ import numpy as np try: import metaarray HAVE_METAARRAY = True -except: +except ImportError: HAVE_METAARRAY = False __all__ = ['TableWidget'] @@ -60,26 +60,19 @@ class TableWidget(QtGui.QTableWidget): 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 @@ class TableWidget(QtGui.QTableWidget): 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 @@ class TableWidget(QtGui.QTableWidget): 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 @@ class TableWidget(QtGui.QTableWidget): 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 @@ class TableWidget(QtGui.QTableWidget): """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 4c6a77ce..3fa02d59 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -26,6 +26,7 @@ class TableWidget(QtGui.QTableWidget): 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 @@ class TableWidget(QtGui.QTableWidget): def setData(self, data): self.clear() self.appendData(data) + self.setSortingEnabled(True) + self.resizeColumnsToContents() def appendData(self, data): """Types allowed: @@ -135,9 +138,23 @@ class TableWidget(QtGui.QTableWidget): 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 bdf10523..57caa7e0 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -96,6 +96,7 @@ class ExampleLoader(QtGui.QMainWindow): self.codeBtn.hide() global examples + self.itemCache = [] self.populateTree(self.ui.exampleTree.invisibleRootItem(), examples) self.ui.exampleTree.expandAll() @@ -122,6 +123,9 @@ class ExampleLoader(QtGui.QMainWindow): 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 3fa02d59..5b49b86f 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -27,6 +27,7 @@ class TableWidget(QtGui.QTableWidget): 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 @@ class TableWidget(QtGui.QTableWidget): 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 00000000..cfeac399 --- /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 57caa7e0..c46d7065 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -64,7 +64,7 @@ examples = OrderedDict([ ('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 5b49b86f..8ffe7291 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -12,23 +12,20 @@ except ImportError: __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 @@ class TableWidget(QtGui.QTableWidget): 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 @@ class TableWidget(QtGui.QTableWidget): 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 @@ class TableWidget(QtGui.QTableWidget): 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 @@ class TableWidget(QtGui.QTableWidget): 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 @@ class TableWidget(QtGui.QTableWidget): 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 966fd926..556c5be0 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 d3aefa83..71880fbd 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -54,6 +54,7 @@ CONFIG_OPTIONS = { '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 @@ from .SignalProxy import * from .colormap import * from .ptime import time +############################################################## +## PyQt and PySide both are prone to crashing on exit. +## There are two general approaches to dealing with this: +## 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 6fbe44a7..982c2424 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -169,7 +169,7 @@ class ConsoleWidget(QtGui.QWidget): 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 @@ class ConsoleWidget(QtGui.QWidget): return False return True - \ No newline at end of file + diff --git a/pyqtgraph/flowchart/Terminal.py b/pyqtgraph/flowchart/Terminal.py index 623d1a28..3066223d 100644 --- a/pyqtgraph/flowchart/Terminal.py +++ b/pyqtgraph/flowchart/Terminal.py @@ -521,6 +521,8 @@ class ConnectionItem(GraphicsObject): 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 @@ class ConnectionItem(GraphicsObject): 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 @@ class ConnectionItem(GraphicsObject): 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 @@ class ConnectionItem(GraphicsObject): 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 b562132c..21d74efd 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1046,10 +1046,10 @@ class ViewBox(GraphicsWidget): 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 619d639a..c82ecc15 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -169,6 +169,13 @@ class EnumColorMapItem(ptree.types.GroupParameter): 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 @@ class EnumColorMapItem(ptree.types.GroupParameter): 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 a2e1a7b8..65796a15 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -92,14 +92,18 @@ class RangeFilterItem(ptree.types.SimpleParameter): 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 @@ class EnumFilterItem(ptree.types.SimpleParameter): 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 56d6b13c..a67e279d 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -56,7 +56,7 @@ rois.append(pg.MultiRectROI([[20, 90], [50, 60], [60, 90]], width=5, pen=(2,9))) 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 123612b8..fad88bee 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -249,7 +249,7 @@ class ImageItem(GraphicsObject): 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 @@ class ImageItem(GraphicsObject): 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 4da8fa4a..9cdc8c29 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -802,7 +802,11 @@ class ROI(GraphicsObject): 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 @@ class ROI(GraphicsObject): #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 @@ class PolyLineROI(ROI): 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 @@ class SpiralROI(ROI): #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 c5a8ec3f..d707a347 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -112,7 +112,10 @@ class PlotCurveItem(GraphicsObject): 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 654a12c9..b7785a9d 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -467,12 +467,32 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 00000000..75e0c680 --- /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 9d1684bd..7081f0ba 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -314,10 +314,13 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): ## 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 00000000..e766e456 --- /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 3066223d..45805cd8 100644 --- a/pyqtgraph/flowchart/Terminal.py +++ b/pyqtgraph/flowchart/Terminal.py @@ -523,6 +523,15 @@ class ConnectionItem(GraphicsObject): 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 @@ class ConnectionItem(GraphicsObject): 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 @@ class ConnectionItem(GraphicsObject): 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 @@ class ConnectionItem(GraphicsObject): 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 c0d5f2f3..76b74359 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -84,13 +84,24 @@ class PlotDataItem(GraphicsObject): **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 84c05478..8fdbe0f9 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -677,15 +677,12 @@ class ScatterPlotItem(GraphicsObject): pts[1] = self.data['y'] pts = fn.transformCoordinates(tr, pts) self.fragments = [] - pts = np.clip(pts, -2**31, 2**31) ## prevent Qt segmentation fault. + 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 65796a15..93c5f24f 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -104,6 +104,10 @@ class EnumFilterItem(ptree.types.SimpleParameter): 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 @@ class EnumFilterItem(ptree.types.SimpleParameter): 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 2e1c1918..5760fac6 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -48,13 +48,15 @@ class ScatterPlotWidget(QtGui.QSplitter): 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 @@ class ScatterPlotWidget(QtGui.QSplitter): :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 @@ class ScatterPlotWidget(QtGui.QSplitter): 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 @@ class ScatterPlotWidget(QtGui.QSplitter): 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 b7785a9d..8769ed92 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -524,12 +524,13 @@ class ViewBox(GraphicsWidget): 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 00000000..165240b2 --- /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 8769ed92..3bbb9fe8 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -950,7 +950,8 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 7081f0ba..c4e0138c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -380,7 +380,7 @@ class AxisItem(GraphicsWidget): 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 93a109ed..2b345e8b 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -79,7 +79,11 @@ class Process(RemoteEventHandler): sysPath = sys.path if copySysPath else None bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py')) self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap)) - 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 3bbb9fe8..338cdde4 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1322,6 +1322,8 @@ class ViewBox(GraphicsWidget): 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 dd49ab7d..6ddfe930 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -181,8 +181,9 @@ class GraphicsView(QtGui.QGraphicsView): 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 @@ class GraphicsView(QtGui.QGraphicsView): 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 | 118 ++++++++++---- 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, 522 insertions(+), 123 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 effa6b0b..c6362295 100644 --- a/examples/GraphItem.py +++ b/examples/GraphItem.py @@ -10,6 +10,9 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np +# Enable antialiasing for prettier plots +pg.setConfigOptions(antialias=True) + w = pg.GraphicsWindow() w.setWindowTitle('pyqtgraph example: GraphItem') v = w.addViewBox() diff --git a/examples/Plotting.py b/examples/Plotting.py index 6a3a1d11..6578fb2b 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -21,7 +21,8 @@ win = pg.GraphicsWindow(title="Basic plotting examples") 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 00000000..5f9675e4 --- /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 e766e456..563667bd 100644 --- a/examples/ScatterPlotWidget.py +++ b/examples/ScatterPlotWidget.py @@ -16,8 +16,22 @@ data = np.array([ (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 ec40cf16..f572743a 100644 --- a/examples/SimplePlot.py +++ b/examples/SimplePlot.py @@ -3,9 +3,12 @@ import initExample ## Add path to library (just for examples; you do not need th from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import 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 00000000..48ee4236 --- /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 b284db89..672897ab 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 84a5c573..5f820a9a 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 00000000..0527e9f1 --- /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 be6138ce..79f8804a 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -103,6 +103,8 @@ class GraphItem(GraphicsObject): 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 3a63afa7..40ff6bc5 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -446,6 +446,14 @@ class GraphicsItem(object): #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 @@ class GraphicsItem(object): ## 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 121a67ea..e4c5cd81 100644 --- a/pyqtgraph/graphicsItems/GraphicsObject.py +++ b/pyqtgraph/graphicsItems/GraphicsObject.py @@ -19,7 +19,7 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): 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 9770b661..3174e6e0 100644 --- a/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py +++ b/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py @@ -5,7 +5,9 @@ from ..Point import Point 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 17301fb3..6101c4bc 100644 --- a/pyqtgraph/graphicsItems/LabelItem.py +++ b/pyqtgraph/graphicsItems/LabelItem.py @@ -2,11 +2,12 @@ from pyqtgraph.Qt import QtGui, QtCore 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 d707a347..fc8fe4c2 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -402,7 +402,6 @@ class PlotCurveItem(GraphicsObject): 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 961f07d7..768f6978 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) + self.bar = QtGui.QGraphicsRectItem() + self.bar.setPen(self.pen) + self.bar.setBrush(self.brush) + self.bar.setParentItem(self) - 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 + 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() - 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 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 + #def setSize(self, s): + #self.size = s diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 8fdbe0f9..29bfeaac 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -740,6 +740,7 @@ class ScatterPlotItem(GraphicsObject): 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 3516c9f6..8769ed92 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -524,12 +524,13 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 c82ecc15..26539d7e 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -72,7 +72,8 @@ class ColorMapParameter(ptree.types.GroupParameter): (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 93c5f24f..c94f6c68 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -2,6 +2,7 @@ from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph.parametertree as ptree import numpy as np from pyqtgraph.pgcollections import OrderedDict +import pyqtgraph as pg __all__ = ['DataFilterWidget'] @@ -22,6 +23,7 @@ class DataFilterWidget(ptree.ParameterTree): 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 @@ class DataFilterParameter(ptree.types.GroupParameter): 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 @@ class RangeFilterItem(ptree.types.SimpleParameter): 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 @@ class EnumFilterItem(ptree.types.SimpleParameter): 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 @@ class EnumFilterItem(ptree.types.SimpleParameter): 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 5760fac6..fe785e04 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -6,6 +6,7 @@ import pyqtgraph.parametertree as ptree import pyqtgraph.functions as fn import numpy as np from pyqtgraph.pgcollections import OrderedDict +import pyqtgraph as pg __all__ = ['ScatterPlotWidget'] @@ -47,6 +48,12 @@ class ScatterPlotWidget(QtGui.QSplitter): 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 @@ class ScatterPlotWidget(QtGui.QSplitter): 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 @@ class ScatterPlotWidget(QtGui.QSplitter): 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 | 211 ++++++++++++++----- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 1 + 2 files changed, 160 insertions(+), 52 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index c4e0138c..bf3c8743 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -39,19 +39,19 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): """Set the width of this axis reserved for ticks and tick labels. The width of the axis label is automatically added.""" if w is None: - 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): ## 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): if tickPositions[i][j] is None: strings[j] = None - textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, 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 @@ class AxisItem(GraphicsWidget): 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 3100087a..c226b9c4 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -1079,6 +1079,7 @@ class PlotItem(GraphicsWidget): ============= ================================================================= """ 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 bf3c8743..e31030df 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -383,7 +383,8 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): ## 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 338cdde4..0a625d48 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -87,6 +87,7 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 bc041595..cbe8eb61 100644 --- a/pyqtgraph/widgets/HistogramLUTWidget.py +++ b/pyqtgraph/widgets/HistogramLUTWidget.py @@ -13,7 +13,7 @@ __all__ = ['HistogramLUTWidget'] 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 ab2fd75b..1de07cff 100644 --- a/examples/GLLinePlotItem.py +++ b/examples/GLLinePlotItem.py @@ -40,7 +40,7 @@ for i in range(n): 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 1586d70a..9dbcd443 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 @@ class GLAxisItem(GLGraphicsItem): 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 630b2aba..01a2b178 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 @@ class GLGridItem(GLGraphicsItem): 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 ef747d17..9ef34cab 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -32,13 +32,14 @@ class GLLinePlotItem(GLGraphicsItem): 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 @@ class GLLinePlotItem(GLGraphicsItem): 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 8b52ac09..dfdaad0c 100644 --- a/examples/GLImageItem.py +++ b/examples/GLImageItem.py @@ -25,11 +25,14 @@ shape = (100,100,70) data = ndi.gaussian_filter(np.random.normal(size=shape), (4,4,4)) data += ndi.gaussian_filter(np.random.normal(size=shape), (15,15,15))*15 -## 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 9babec3a..f73b0a7a 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -116,11 +116,11 @@ class GLGraphicsItem(QtCore.QObject): 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 @@ class GLGraphicsItem(QtCore.QObject): 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 9dbcd443..860ac497 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 @@ class GLAxisItem(GLGraphicsItem): 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 af888e91..bc25afd1 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 @@ class GLBoxItem(GLGraphicsItem): 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 @@ class GLBoxItem(GLGraphicsItem): 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 b292a7b7..aca68f3d 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 @@ class GLImageItem(GLGraphicsItem): self.smooth = smooth self.data = data GLGraphicsItem.__init__(self) + self.setGLOptions(glOptions) def initializeGL(self): glEnable(GL_TEXTURE_2D) @@ -66,11 +67,13 @@ class GLImageItem(GLGraphicsItem): 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 4ff3d175..eac70f51 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 5b4e7ef4..a5d869c9 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 cb36ba62..d1a21e97 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 @@ class RemoteGraphicsView(QtGui.QWidget): self._sizeHint = (640,480) ## no clue why this is needed, but it seems to be the default sizeHint for GraphicsView. ## without it, the widget will not compete for space against another GraphicsView. QtGui.QWidget.__init__(self) - self._proc = mp.QtProcess(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 @@ class Renderer(GraphicsView): 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 @@ class Renderer(GraphicsView): #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 23b7cd58..76a71e14 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 c46d7065..e7b89716 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 67eb712e..d83e0ec0 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 9cdc8c29..97669fe0 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1771,7 +1771,7 @@ class PolyLineROI(ROI): 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 9925a573..e96692e2 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -129,7 +129,7 @@ class Parallelize(object): 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 6cd65f6e..974e1e95 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -803,7 +803,7 @@ class ObjectProxy(object): 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 e31030df..d8c49390 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -731,6 +731,7 @@ class AxisItem(GraphicsWidget): 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 e7b89716..2ecc810d 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -3,6 +3,7 @@ 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) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index d8c49390..846f48ac 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -281,7 +281,7 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): self.setLabel(unitPrefix=prefix) else: scale = 1.0 + self.autoScale = True else: self.setLabel(unitPrefix='') self.autoScale = False @@ -499,6 +500,10 @@ class AxisItem(GraphicsWidget): """ 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 @@ class AxisItem(GraphicsWidget): ## 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 c41feb95..3f4d5fa1 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -73,6 +73,36 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): 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 c226b9c4..52a1429b 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -295,19 +295,21 @@ class PlotItem(GraphicsWidget): - 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 f0c13a60..cb72241a 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -209,7 +209,7 @@ class ImageView(QtGui.QWidget): *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 @@ class ImageView(QtGui.QWidget): 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 376e900d..46499fd3 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -157,3 +157,9 @@ class ParameterItem(QtGui.QTreeWidgetItem): ## 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 71695f4a..57e4f1ed 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -313,7 +313,7 @@ class SpinBox(QtGui.QAbstractSpinBox): 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 5f820a9a..6c52e775 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 19ebc76e..414980ac 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -209,6 +209,13 @@ class Dock(QtGui.QWidget, DockDrop): 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 6c52e775..836ae433 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -23,7 +23,7 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' 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 @@ import sys, struct 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 97669fe0..bdfc8508 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 f73b0a7a..59bc4449 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -240,7 +240,7 @@ class GLGraphicsItem(QtCore.QObject): 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 3f4d5fa1..e2484ecf 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -4,6 +4,7 @@ from ..Qt import QtGui, QtCore 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 @@ class ItemSample(GraphicsWidget): 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 @@ class ItemSample(GraphicsWidget): 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 e2484ecf..a2fc0e04 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -63,21 +63,23 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): =========== ======================================================== 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 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): 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 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): 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 b51b9aa3..d209ef01 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 752cf3b6..882b29a3 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -40,11 +40,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop): 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 @@ class DockArea(Container, QtGui.QWidget, DockDrop): DockDrop.dropEvent(self, *args) - \ No newline at end of file + diff --git a/pyqtgraph/widgets/MatplotlibWidget.py b/pyqtgraph/widgets/MatplotlibWidget.py index 25e058f9..6a22c973 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 3174e6e0..251bc0c8 100644 --- a/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py +++ b/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py @@ -47,7 +47,52 @@ class GraphicsWidgetAnchor(object): 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 a2fc0e04..6c42fb4c 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -101,7 +101,6 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): label.close() self.updateSize() # redraq box - def updateSize(self): if self.size is not None: return @@ -115,15 +114,22 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): #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 a2fc0e04..1fc88662 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -126,6 +126,11 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): 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 @@ class ItemSample(GraphicsWidget): 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 d1c1d090..40bd853e 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -168,6 +168,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 9056fbd6..5ef8eb51 100644 --- a/examples/GLMeshItem.py +++ b/examples/GLMeshItem.py @@ -83,6 +83,16 @@ m3 = gl.GLMeshItem(meshdata=md, smooth=False)#, shader='balloon') 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 170074b9..12a9b83b 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -44,7 +44,7 @@ class MeshData(object): ## 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 @@ class MeshData(object): 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 @@ class MeshData(object): #""" #pass + def _computeEdges(self): + ## generate self._edges from self._faces + #print self._faces + nf = len(self._faces) + edges = np.empty(nf*3, dtype=[('i', np.uint, 2)]) + edges['i'][0:nf] = self._faces[:,:2] + edges['i'][nf:2*nf] = self._faces[:,1:3] + edges['i'][-nf:,0] = self._faces[:,2] + edges['i'][-nf:,1] = self._faces[:,0] + + # sort per-edge + mask = edges['i'][:,0] > edges['i'][:,1] + edges['i'][mask] = edges['i'][mask][:,::-1] + + # remove duplicate entries + self._edges = np.unique(edges)['i'] + #print self._edges + + 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 4222c96b..66d54361 100644 --- a/pyqtgraph/opengl/items/GLMeshItem.py +++ b/pyqtgraph/opengl/items/GLMeshItem.py @@ -22,9 +22,15 @@ class GLMeshItem(GLGraphicsItem): 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 @@ class GLMeshItem(GLGraphicsItem): 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 @@ class GLMeshItem(GLGraphicsItem): self.faces = None self.normals = None self.colors = None + self.edges = None + self.edgeColors = None self.update() def parseMeshData(self): @@ -137,6 +148,9 @@ class GLMeshItem(GLGraphicsItem): 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 @@ class GLMeshItem(GLGraphicsItem): 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 @@ class GLMeshItem(GLGraphicsItem): 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 | 125 ++++++++----------------------- 1 file changed, 32 insertions(+), 93 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index cb72241a..77f34419 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -90,14 +90,6 @@ class ImageView(QtGui.QWidget): 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 @@ class ImageView(QtGui.QWidget): 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 @@ class ImageView(QtGui.QWidget): 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 @@ class ImageView(QtGui.QWidget): 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 @@ class ImageView(QtGui.QWidget): 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 @@ class ImageView(QtGui.QWidget): 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}; + {'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*. - ============== ======================================================================= + 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 @@ class ImageView(QtGui.QWidget): 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 @@ class ImageView(QtGui.QWidget): 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 @@ class ImageView(QtGui.QWidget): 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 @@ class ImageView(QtGui.QWidget): 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 @@ class ImageView(QtGui.QWidget): """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 @@ class ImageView(QtGui.QWidget): 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 @@ class ImageView(QtGui.QWidget): if not self.ui.normOffRadio.isChecked(): self.imageDisp = None self.updateImage() + self.autoLevels() self.roiChanged() self.sigProcessingChanged.emit(self) @@ -634,22 +592,19 @@ class ImageView(QtGui.QWidget): #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 @@ class ImageView(QtGui.QWidget): ## 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 00000000..17d1ef32 --- /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 236cb807..893f79f5 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -18,6 +18,7 @@ import sys, os # 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 @@ sys.path.insert(0, os.path.join(path, '..', '..')) # 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 @@ man_pages = [ ('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 f5a93088..43a8c330 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 @@ class Exporter(object): 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 00000000..490ba298 --- /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 255f550b..d025a4c7 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 9ef34cab..bb5ce2f6 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 @@ class GLLinePlotItem(GLGraphicsItem): 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 29bfeaac..bec6a318 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 a68cf542..be0d86e5 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -560,6 +560,7 @@ class Flowchart(Node): 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 @@ class FlowchartCtrlWidget(QtGui.QWidget): #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 @@ class FlowchartCtrlWidget(QtGui.QWidget): #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 40bd853e..493d2523 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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 493d2523..83034887 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -34,6 +34,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): '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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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))) - ## 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] + 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) + 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 83034887..afab475c 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -350,7 +350,8 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 afab475c..12984c86 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -346,7 +346,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 0a625d48..9edca06f 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -295,7 +295,7 @@ class ViewBox(GraphicsWidget): 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 fad88bee..530db7fb 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -196,10 +196,12 @@ class ImageItem(GraphicsObject): 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 86973b04..69ddffea 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -92,7 +92,6 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): # 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 bec6a318..c8324901 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -35,6 +35,8 @@ for k, c in coords.items(): 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 836ae433..a9cf2693 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 d707a347..ebcd0d38 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -312,10 +312,10 @@ class PlotCurveItem(GraphicsObject): 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 17d1ef32..75c848fc 100644 --- a/doc/extensions/qt_doc.py +++ b/doc/extensions/qt_doc.py @@ -11,6 +11,12 @@ Extension for building Qt-like documentation. 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 893f79f5..5475fc60 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -27,7 +27,7 @@ sys.path.insert(0, os.path.join(path, '..', 'extensions')) # 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 23a561b9..f117bb7f 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 bdfc8508..a5e25a2f 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1621,7 +1621,7 @@ class PolyLineROI(ROI): 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 @@ class PolyLineROI(ROI): 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 @@ class PolyLineROI(ROI): 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 @@ class PolyLineROI(ROI): 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 | 90 +++++++-- .../PlotItem/plotConfigTemplate.ui | 153 ++++++++++----- .../PlotItem/plotConfigTemplate_pyqt.py | 81 +++++--- .../PlotItem/plotConfigTemplate_pyside.py | 81 +++++--- 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, 613 insertions(+), 204 deletions(-) diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index ea35d119..682f19f7 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -80,6 +80,12 @@ class Point(QtCore.QPointF): 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 a861f940..efb24f60 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -130,11 +130,14 @@ class SRTTransform(QtGui.QTransform): 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 77583b5a..7d87dcb8 100644 --- a/pyqtgraph/SRTTransform3D.py +++ b/pyqtgraph/SRTTransform3D.py @@ -123,7 +123,6 @@ class SRTTransform3D(pg.Transform3D): 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 @@ class SRTTransform3D(pg.Transform3D): 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 ae2b21ac..a175be9c 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -28,6 +28,15 @@ def ftrace(func): 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 412af573..579d2cd2 100644 --- a/pyqtgraph/flowchart/library/Operators.py +++ b/pyqtgraph/flowchart/library/Operators.py @@ -24,7 +24,15 @@ class BinOpNode(Node): }) 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 5f820a9a..4168836e 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 e31030df..97f0ef1c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -42,12 +42,18 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): ## 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): - 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 @@ class AxisItem(GraphicsWidget): #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 @@ class AxisItem(GraphicsWidget): 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 40ff6bc5..a129436e 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -533,6 +533,7 @@ class GraphicsItem(object): 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 d707a347..4c66bf72 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -375,6 +375,7 @@ class PlotCurveItem(GraphicsObject): 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 76b74359..1ae528ba 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -84,24 +84,28 @@ class PlotDataItem(GraphicsObject): **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 @@ class PlotDataItem(GraphicsObject): self.opts = { 'fftMode': False, 'logMode': [False, False], - 'downsample': False, 'alphaHint': 1.0, 'alphaMode': False, @@ -149,6 +152,11 @@ class PlotDataItem(GraphicsObject): 'antialias': pg.getConfigOption('antialias'), 'pointMode': None, + 'downsample': 1, + 'autoDownsample': False, + 'downsampleMethod': 'peak', + 'clipToView': False, + 'data': None, } self.setData(*args, **kargs) @@ -175,6 +183,7 @@ class PlotDataItem(GraphicsObject): return self.opts['fftMode'] = mode self.xDisp = self.yDisp = None + self.xClean = self.yClean = None self.updateItems() self.informViewBoundsChanged() @@ -183,6 +192,7 @@ class PlotDataItem(GraphicsObject): return self.opts['logMode'] = [xMode, yMode] self.xDisp = self.yDisp = None + self.xClean = self.yClean = None self.updateItems() self.informViewBoundsChanged() @@ -269,13 +279,51 @@ class PlotDataItem(GraphicsObject): #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 @@ class PlotDataItem(GraphicsObject): 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 @@ class PlotDataItem(GraphicsObject): 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 @@ class PlotDataItem(GraphicsObject): 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 @@ class PlotDataItem(GraphicsObject): 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 @@ class PlotDataItem(GraphicsObject): #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 @@ class PlotDataItem(GraphicsObject): 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 c226b9c4..ff3dc7b3 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -256,6 +256,11 @@ class PlotItem(GraphicsWidget): 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 @@ class PlotItem(GraphicsWidget): (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 @@ class PlotItem(GraphicsWidget): :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 @@ class PlotItem(GraphicsWidget): 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 516ec721..dffc62d0 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,40 +62,17 @@ 0 - - + + + + Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced. + - Manual - - - true + Clip to View - - - - 1 - - - 100000 - - - 1 - - - - - - - Auto - - - false - - - - + If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed. @@ -111,14 +82,34 @@ - + + + + Downsample + + + + + + + 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. + + + 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 d34cd297..5335ee76 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 @@ except AttributeError: 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 @@ class Ui_Form(object): 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.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, 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, 8, 2, 1, 1) + self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.forgetTracesCheck.setObjectName(_fromUtf8("forgetTracesCheck")) + 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, 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.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) - self.maxTracesCheck.setObjectName(_fromUtf8("maxTracesCheck")) - self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1) - self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) - self.maxTracesSpin.setObjectName(_fromUtf8("maxTracesSpin")) - self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 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.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 @@ class Ui_Form(object): 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 85b563a7..b8e0b19e 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 @@ from PySide import QtCore, QtGui 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 @@ class Ui_Form(object): 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.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, 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, 8, 2, 1, 1) + self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.forgetTracesCheck.setObjectName("forgetTracesCheck") + 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, 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.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) - self.maxTracesCheck.setObjectName("maxTracesCheck") - self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1) - self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) - self.maxTracesSpin.setObjectName("maxTracesSpin") - self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 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.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 @@ class Ui_Form(object): 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 29bfeaac..a6a46bf5 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -15,7 +15,7 @@ __all__ = ['ScatterPlotItem', 'SpotItem'] ## 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 k, c in coords.items(): 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 @@ class ScatterPlotItem(GraphicsObject): 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 338cdde4..29bd6d23 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -141,6 +141,12 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): """ 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): self.sigStateChanged.emit(self) + self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) + if update: self.updateMatrix(changed) @@ -494,7 +523,7 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): else: if self.autoRangeEnabled()[axis] is False: slot() + self.sigStateChanged.emit(self) @@ -807,13 +837,17 @@ class ViewBox(GraphicsWidget): """ 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 0797c75e..f55c60dc 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -328,6 +328,9 @@ class MetaArray(object): 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 6cd65f6e..d0d75c1e 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -887,6 +887,12 @@ class ObjectProxy(object): 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 @@ class ObjectProxy(object): 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 @@ class ObjectProxy(object): 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 @@ class ObjectProxy(object): 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 @@ class ObjectProxy(object): 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 fe785e04..e9e24dd7 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -190,10 +190,15 @@ class ScatterPlotWidget(QtGui.QSplitter): 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 2ecc810d..4aa23e8e 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 682f19f7..4d04f01c 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -152,4 +152,4 @@ class Point(QtCore.QPointF): 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 5a78616d..1c179995 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -5,6 +5,7 @@ Copyright 2010 Luke Campagnola 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 f33ebc83..7622b6e7 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -932,9 +932,9 @@ class ObjectProxy(object): 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 12984c86..1cd3a047 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -139,7 +139,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 199c372c..5345e187 100644 --- a/pyqtgraph/opengl/__init__.py +++ b/pyqtgraph/opengl/__init__.py @@ -23,8 +23,8 @@ from pyqtgraph import importAll 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 bb5ce2f6..888af664 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -83,7 +83,7 @@ class GLLinePlotItem(GLGraphicsItem): 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 e9bbde64..b02a9dda 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -146,7 +146,7 @@ class GLScatterPlotItem(GLGraphicsItem): 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 46c54fc2..88d50fac 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 @@ class GLSurfacePlotItem(GLMeshItem): 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 b1652850..e8ca28d9 100644 --- a/pyqtgraph/opengl/shaders.py +++ b/pyqtgraph/opengl/shaders.py @@ -354,7 +354,7 @@ class ShaderProgram(object): 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 @@ class HeightColorShader(ShaderProgram): ## 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 d1a21e97..80f0fb4b 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -128,7 +128,7 @@ class Renderer(GraphicsView): 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 5a78616d..2fc1a8a2 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 12984c86..d8f70055 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -80,23 +80,26 @@ class GLViewWidget(QtOpenGL.QGLWidget): #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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 dd392189..d7a4e1e0 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 3dddb928..d73b0dc9 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 c3430e2d..f61a5e46 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 @@ except AttributeError: 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 @@ class Ui_MainWindow(object): 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 @@ class Ui_MainWindow(object): 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 d19e0f23..d0db5eff 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 @@ from PySide import QtCore, QtGui 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 @@ class Ui_MainWindow(object): 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 @@ class Ui_MainWindow(object): 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 4c5d7275..c600d1be 100644 --- a/examples/parametertree.py +++ b/examples/parametertree.py @@ -139,14 +139,19 @@ p.param('Save/Restore functionality', 'Restore State').sigActivated.connect(rest ## 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 d83e0ec0..b1a05835 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 a5e25a2f..033aab42 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 28e1e618..51f0be64 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -619,9 +619,15 @@ class TextParameterItem(WidgetParameterItem): 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 ea5c98a0..a780f463 100644 --- a/pyqtgraph/widgets/RawImageWidget.py +++ b/pyqtgraph/widgets/RawImageWidget.py @@ -11,8 +11,8 @@ import numpy as np 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 @@ class RawImageWidget(QtGui.QWidget): 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 ea04bb16..7657a6bd 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -139,6 +139,7 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 c600d1be..c0eb50db 100644 --- a/examples/parametertree.py +++ b/examples/parametertree.py @@ -67,7 +67,7 @@ params = [ {'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 51f0be64..c3a9420e 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -476,32 +476,16 @@ class ListParameterItem(WidgetParameterItem): 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 @@ class ListParameter(Parameter): 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 b1652850..5ef20776 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 @@ class Shader(object): 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 d209ef01..85e2b24a 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 bb5ce2f6..75d48c86 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -30,8 +30,9 @@ class GLLinePlotItem(GLGraphicsItem): 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 @@ class GLLinePlotItem(GLGraphicsItem): 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 @@ class GLLinePlotItem(GLGraphicsItem): 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 2b345e8b..7d147a1d 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 @@ class QtProcess(Process): 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 881dcf2d..742c73ef 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -261,6 +261,9 @@ class PlotCurveItem(GraphicsObject): 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 1ae528ba..f76a8b74 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -475,7 +475,7 @@ class PlotDataItem(GraphicsObject): 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 f76a8b74..f9f2febe 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -495,10 +495,7 @@ class PlotDataItem(GraphicsObject): ##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 @@ class PlotDataItem(GraphicsObject): 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 e57430ea..866875e5 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 @@ class ParameterTree(TreeWidget): 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 43a8c330..6371a3b9 100644 --- a/pyqtgraph/exporters/Exporter.py +++ b/pyqtgraph/exporters/Exporter.py @@ -119,7 +119,7 @@ class Exporter(object): 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 bdb8b9be..d1d78e7d 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -42,7 +42,7 @@ class ImageExporter(Exporter): 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 be0d86e5..81f9e163 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -376,10 +376,10 @@ class Flowchart(Node): #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 @@ class Flowchart(Node): 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 d8f70055..fe52065a 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -167,7 +167,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): 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 d1d78e7d..b14ed513 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 @@ class ImageExporter(Exporter): 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 6ddfe930..0c8921f6 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -82,6 +82,7 @@ class GraphicsView(QtGui.QGraphicsView): ## 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 @@ class GraphicsView(QtGui.QGraphicsView): 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 b1a05835..c1b62041 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 b14ed513..a9b44ab4 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -17,7 +17,11 @@ class ImageExporter(Exporter): 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 1fa07f2a..7b3c685c 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 c3a9420e..3300171f 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -616,6 +616,7 @@ registerParameterType('action', ActionParameter, override=True) 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 515de33a..8f0d6e1b 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 @@ class Shader(object): 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 @@ class Shader(object): 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 @@ class ShaderProgram(object): 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 00000000..d14eba87 --- /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 0bf13ca8..e5faadda 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 f33ebc83..e4c3f34f 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -205,7 +205,11 @@ class RemoteEventHandler(object): 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 59bc4449..9680fba7 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -40,6 +40,7 @@ class GLGraphicsItem(QtCore.QObject): 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 @@ class GLGraphicsItem(QtCore.QObject): 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 @@ class GLGraphicsItem(QtCore.QObject): 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 @@ class GLGraphicsItem(QtCore.QObject): 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 @@ class GLGraphicsItem(QtCore.QObject): 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 @@ class GLGraphicsItem(QtCore.QObject): 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 12a9b83b..71e566c9 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -247,9 +247,9 @@ class MeshData(object): 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 66d54361..5b245e64 100644 --- a/pyqtgraph/opengl/items/GLMeshItem.py +++ b/pyqtgraph/opengl/items/GLMeshItem.py @@ -69,7 +69,11 @@ class GLMeshItem(GLGraphicsItem): 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 e9c109d8..4b4fb02f 100644 --- a/pyqtgraph/Vector.py +++ b/pyqtgraph/Vector.py @@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola 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 @@ class Vector(QtGui.QVector3D): 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 672897ab..7c48c8a9 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 @@ class SVGExporter(Exporter): 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 7c48c8a9..821427a4 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 821427a4..62b49d30 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -92,8 +92,8 @@ class SVGExporter(Exporter): 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 00000000..28ed45aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +build +*.pyc +*.swp diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000..7bd3e10e --- /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 7bd3e10e..025cf940 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 00000000..f1ae9f7d --- /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 d03c6c77..00000000 --- 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 f1ae9f7d..23f47ea7 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 3f0c6a3e..14e4e076 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 742c73ef..2fea3d33 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -71,7 +71,8 @@ class PlotCurveItem(GraphicsObject): '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 @@ class PlotCurveItem(GraphicsObject): ## 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 @@ class PlotCurveItem(GraphicsObject): 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 @@ class PlotCurveItem(GraphicsObject): 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 @@ class PlotCurveItem(GraphicsObject): 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 f9f2febe..1e525f83 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -58,6 +58,8 @@ class PlotDataItem(GraphicsObject): **Line style keyword arguments:** ========== ================================================ + connect Specifies how / whether vertexes should be connected. + See :func:`arrayToQPath() ` pen Pen to use for drawing line between points. 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 @@ class PlotDataItem(GraphicsObject): 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 @@ class PlotDataItem(GraphicsObject): #self.clear() self.opts = { + 'connect': 'all', + 'fftMode': False, 'logMode': [False, False], 'alphaHint': 1.0, @@ -386,6 +390,8 @@ class PlotDataItem(GraphicsObject): 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 @@ class PlotDataItem(GraphicsObject): 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 @@ class PlotDataItem(GraphicsObject): 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 @@ class PlotDataItem(GraphicsObject): 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 @@ class PlotDataItem(GraphicsObject): 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 @@ class PlotDataItem(GraphicsObject): #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 3070d15a..97f5aa8f 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -626,11 +626,13 @@ class ScatterPlotItem(GraphicsObject): 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 7657a6bd..d7fd49e5 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -183,7 +183,7 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 00000000..b3060dc9 --- /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 a9b44ab4..9fb77e2a 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -64,6 +64,9 @@ class ImageExporter(Exporter): #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 | 228 +++++++++++++------------------------ 2 files changed, 208 insertions(+), 147 deletions(-) create mode 100644 pyqtgraph/ordereddict.py diff --git a/pyqtgraph/ordereddict.py b/pyqtgraph/ordereddict.py new file mode 100644 index 00000000..5b0303f5 --- /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 b0198526..4a225915 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 @@ import threading, sys, copy, collections 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) + from ordereddict import OrderedDict - 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()]) - - 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 @@ class ReverseDict(dict): 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 @@ class ReverseDict(dict): 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 @@ class BiDict(dict): 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 @@ class ThreadsafeDict(dict): dict.__setitem__(self, attr, val) finally: self.unlock() - + def __contains__(self, attr): self.lock() try: @@ -188,19 +122,19 @@ class ThreadsafeDict(dict): 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 @@ class ThreadsafeList(list): list.__setitem__(self, attr, val) finally: self.unlock() - + def __contains__(self, attr): self.lock() try: @@ -238,17 +172,17 @@ class ThreadsafeList(list): 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 @@ class CaselessDict(OrderedDict): 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 @@ class CaselessDict(OrderedDict): 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 @@ class CaselessDict(OrderedDict): 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 @@ class ProtectedDict(dict): 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 @@ class ProtectedList(collections.Sequence): 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 @@ class ProtectedList(collections.Sequence): 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 @@ class ProtectedList(collections.Sequence): 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 @@ class ProtectedTuple(collections.Sequence): 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 | 137 +++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 66 deletions(-) diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py index b0198526..4de1b7d0 100644 --- a/pyqtgraph/pgcollections.py +++ b/pyqtgraph/pgcollections.py @@ -15,74 +15,79 @@ import threading, sys, copy, collections 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) - - 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()) +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 itervalues(self): - for k in self.order: - yield self[k] + 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 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()]) + 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 d82f5d41..16cf4652 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -69,7 +69,8 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): ## 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 16cf4652..531e3e9a 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -287,32 +287,12 @@ class AxisItem(GraphicsWidget): 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 @@ class AxisItem(GraphicsWidget): 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 531e3e9a..93efb45c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -293,6 +293,11 @@ class AxisItem(GraphicsWidget): 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 93efb45c..bfc8644a 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -320,6 +320,7 @@ class AxisItem(GraphicsWidget): (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 d7fd49e5..e57ea1ee 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -308,7 +308,10 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): ============= ===================================================================== """ + #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding changes = {} @@ -770,6 +774,7 @@ class ViewBox(GraphicsWidget): 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 12a4f90f..f6eafb60 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 00000000..a04e9a29 --- /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 e4c5cd81..d8f55d27 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 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): 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 e57ea1ee..5241489c 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -17,6 +17,10 @@ __all__ = ['ViewBox'] 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 any(changed) and disableAutoRange: + # 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): ax = ViewBox.XYAxes elif changed[0]: @@ -436,26 +477,26 @@ class ViewBox(GraphicsWidget): ax = ViewBox.YAxis self.enableAutoRange(ax, False) - self.sigStateChanged.emit(self) - self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) + # Update target rect for debugging + if self.target.isVisible(): + self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) - if update and (any(changed) or self.matrixNeedsUpdate): - self.updateMatrix(changed) - - 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 @@ class ViewBox(GraphicsWidget): - 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 @@ class ViewBox(GraphicsWidget): #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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 a04e9a29..91d9b617 100644 --- a/pyqtgraph/graphicsItems/tests/ViewBox.py +++ b/pyqtgraph/graphicsItems/tests/ViewBox.py @@ -15,41 +15,58 @@ ViewBox test cases: 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 @@ print "add items views match:", 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 5241489c..aed0cb8b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -91,7 +91,8 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): ================== ===================================================================== """ - 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): elif changed[1]: ax = ViewBox.YAxis self.enableAutoRange(ax, False) + 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())) - 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 @@ class ViewBox(GraphicsWidget): args['disableAutoRange'] = False self.setRange(**args) finally: + self._autoRangeNeedsUpdate = False self._updatingRange = False def setXLink(self, view): @@ -891,7 +898,9 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): #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 aed0cb8b..8dbaf9ef 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -246,7 +246,8 @@ class ViewBox(GraphicsWidget): del state['linkedViews'] self.state.update(state) - self.updateMatrix() + #self.updateMatrix() + self.updateViewRange() self.sigStateChanged.emit(self) @@ -485,9 +486,11 @@ class ViewBox(GraphicsWidget): # 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 a67e279d..56b15bcf 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -27,7 +27,7 @@ arr += np.random.normal(size=(100,100)) ## 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 c41dfff1..67d3cc5f 100644 --- a/examples/crosshair.py +++ b/examples/crosshair.py @@ -23,7 +23,9 @@ p2 = win.addPlot(row=2, col=0) 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 033aab42..f6ce4680 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1320,7 +1320,6 @@ class Handle(UIGraphicsItem): ## 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 @@ class Handle(UIGraphicsItem): 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 8dbaf9ef..f0ea1e57 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -213,8 +213,9 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): # 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 f0ea1e57..419b6306 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -608,11 +608,10 @@ class ViewBox(GraphicsWidget): 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 a5d869c9..2b74a8c6 100644 --- a/examples/RemoteGraphicsView.py +++ b/examples/RemoteGraphicsView.py @@ -13,7 +13,8 @@ from pyqtgraph.widgets.RemoteGraphicsView import RemoteGraphicsView 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 f6eafb60..810fecec 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 4ecfb7da..b82debc2 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -20,10 +20,8 @@ if __name__ == '__main__': 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 7d147a1d..cf802352 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -48,9 +48,10 @@ class Process(RemoteEventHandler): 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 @@ class Process(RemoteEventHandler): 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 0c8921f6..fb535929 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -147,6 +147,11 @@ class GraphicsView(QtGui.QGraphicsView): #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 80f0fb4b..f8bbb6cf 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 @@ class Renderer(GraphicsView): 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 79f8804a..b1f34baa 100644 --- a/pyqtgraph/graphicsItems/GraphItem.py +++ b/pyqtgraph/graphicsItems/GraphItem.py @@ -110,6 +110,11 @@ class GraphItem(GraphicsObject): 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 419b6306..5f59d8bc 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -394,6 +394,8 @@ class ViewBox(GraphicsWidget): """ #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding + #import traceback + #traceback.print_stack() changes = {} # axes setRequested = [False, False] @@ -454,15 +456,11 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 bfc8644a..36516f8c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -404,19 +404,24 @@ class AxisItem(GraphicsWidget): return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) def paint(self, p, opt, widget): + prof = debug.Profiler('AxisItem.paint', disabled=True) 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 97f5aa8f..befc2360 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -517,7 +517,7 @@ class ScatterPlotItem(GraphicsObject): ## 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 14e4e076..337dfb67 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -33,7 +33,6 @@ import sys, struct 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 bbb40efc..5242ecdd 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -65,8 +65,18 @@ class ViewBoxMenu(QtGui.QMenu): 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 810fecec..11e281a4 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 2fea3d33..321c6438 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -7,7 +7,6 @@ except: 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 @@ class PlotCurveItem(GraphicsObject): 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 befc2360..f1a5201d 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -4,7 +4,6 @@ import pyqtgraph.functions as fn 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 @@ class ScatterPlotItem(GraphicsObject): 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 5f59d8bc..b8404ddc 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -201,7 +201,8 @@ class ViewBox(GraphicsWidget): 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 | 19 ++++---- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 50 +++++++++++++++----- 6 files changed, 89 insertions(+), 54 deletions(-) diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index d7a4e1e0..1341ec0e 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -13,7 +13,6 @@ import initExample ## Add path to library (just for examples; you do not need th from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE import numpy as np import pyqtgraph as pg -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 d73b0dc9..9560a19b 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 f61a5e46..91fc1b1e 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 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_MainWindow(object): def setupUi(self, MainWindow): @@ -25,10 +34,10 @@ class Ui_MainWindow(object): 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 @@ class Ui_MainWindow(object): 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 d0db5eff..c1f8bc57 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 @@ class Ui_MainWindow(object): 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 @@ class Ui_MainWindow(object): 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 8f28d208..7650b125 100644 --- a/pyqtgraph/graphicsItems/GraphicsWidget.py +++ b/pyqtgraph/graphicsItems/GraphicsWidget.py @@ -20,16 +20,17 @@ class GraphicsWidget(GraphicsItem, QtGui.QGraphicsWidget): ## 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 + # 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 getMenu(self): - #pass - 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 b8404ddc..5ab118f7 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -93,6 +93,8 @@ class ViewBox(GraphicsWidget): #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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 4aa23e8e..a397cf05 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -252,6 +252,7 @@ except: 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 @@ except: 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 cf802352..16fd6bab 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 @@ class Process(RemoteEventHandler): ## 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 @@ class Process(RemoteEventHandler): 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 @@ class Process(RemoteEventHandler): def startEventLoop(name, port, authkey, ppid, debug=False): + if debug: + import os + print('[%d] connecting to server at port localhost:%d, authkey=%s..' % (os.getpid(), port, repr(authkey))) 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 @@ class QtProcess(Process): 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 702b10bc..eba42ef3 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -97,7 +97,6 @@ class RemoteEventHandler(object): after no more events are immediately available. (non-blocking) Returns the number of events processed. """ - self.debugMsg('processRequests:') if self.exited: self.debugMsg(' processRequests: exited already; raise ClosedError.') raise ClosedError() @@ -108,7 +107,7 @@ class RemoteEventHandler(object): 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 @@ class RemoteEventHandler(object): 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 f8bbb6cf..ac29f426 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -188,11 +188,16 @@ class Renderer(GraphicsView): 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 @@ class Renderer(GraphicsView): 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 d8022aba..b61b55cc 100644 --- a/examples/initExample.py +++ b/examples/initExample.py @@ -33,3 +33,10 @@ for gs in ['raster', 'native', 'opengl']: 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 16fd6bab..42eb1910 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 @@ class Process(RemoteEventHandler): targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to ## set its sys.path properly before unpickling the target pid = os.getpid() # we must 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 @@ class Process(RemoteEventHandler): 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 ac29f426..7270d449 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -83,17 +83,17 @@ class RemoteGraphicsView(QtGui.QWidget): 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 @@ class RemoteGraphicsView(QtGui.QWidget): 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 @@ class Renderer(GraphicsView): 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 b82debc2..bb71a703 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -20,6 +20,7 @@ if __name__ == '__main__': 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 42eb1910..4d32c999 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 @@ class Process(RemoteEventHandler): 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 @@ class Process(RemoteEventHandler): 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 @@ class Process(RemoteEventHandler): self.debugMsg('Connected to child process.') atexit.register(self.join) + def join(self, timeout=10): self.debugMsg('Joining child process..') @@ -140,7 +152,16 @@ class Process(RemoteEventHandler): 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 7270d449..d44fd1c3 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 @@ class RemoteGraphicsView(QtGui.QWidget): 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 @@ class RemoteGraphicsView(QtGui.QWidget): 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 @@ class Renderer(GraphicsView): 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 36516f8c..429ff49c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -202,13 +202,13 @@ class AxisItem(GraphicsWidget): 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 5ab118f7..6ca2090c 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -933,6 +933,9 @@ class ViewBox(GraphicsWidget): """ 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): ## 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 @@ class ViewBox(GraphicsWidget): 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 @@ class ViewBox(GraphicsWidget): 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 321c6438..28214552 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -5,7 +5,6 @@ try: 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 1e525f83..87b47227 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -4,7 +4,6 @@ from .GraphicsObject import GraphicsObject 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 @@ class PlotDataItem(GraphicsObject): 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 6ca2090c..3cbb1ea2 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1352,7 +1352,7 @@ class ViewBox(GraphicsWidget): 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 00000000..6b1579fd --- /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 6b1579fd..9fa10984 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 8128a851..055b74e8 100644 --- a/setup.py +++ b/setup.py @@ -18,9 +18,13 @@ setup(name='pyqtgraph', 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',