diff --git a/CHANGELOG b/CHANGELOG index dcc21eb5..81e384ee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ pyqtgraph-0.9.9 [unreleased] - ArrowItem.setStyle now updates style options rather than replacing them New Features: + - Added ViewBox.setLimits() method - New HDF5 example for working with very large datasets - Added Qt.loadUiType function for PySide - Simplified Profilers; can be activated with environmental variables @@ -48,6 +49,11 @@ pyqtgraph-0.9.9 [unreleased] - PlotCurveItem ignores clip-to-view when auto range is enabled - FillBetweenItem now forces PlotCurveItem to generate path - Fixed import errors and py3 issues in MultiPlotWidget + - Isosurface works for arrays with shapes > 255 + - Fixed ImageItem exception building histogram when image has only one value + - Fixed MeshData exception caused when vertexes have no matching faces + - Fixed GLViewWidget exception handler + pyqtgraph-0.9.8 2013-11-24 diff --git a/examples/SimplePlot.py b/examples/SimplePlot.py index 92106661..03ee2204 100644 --- a/examples/SimplePlot.py +++ b/examples/SimplePlot.py @@ -1,9 +1,11 @@ import initExample ## Add path to library (just for examples; you do not need this) import pyqtgraph as pg +import pyqtgraph.exporters import numpy as np plt = pg.plot(np.random.normal(size=100), title="Simplest possible plotting example") +## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': import sys if sys.flags.interactive != 1 or not hasattr(pg.QtCore, 'PYQT_VERSION'): diff --git a/examples/ViewLimits.py b/examples/ViewLimits.py new file mode 100644 index 00000000..c8f0dd21 --- /dev/null +++ b/examples/ViewLimits.py @@ -0,0 +1,15 @@ +import initExample ## Add path to library (just for examples; you do not need this) + +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +import numpy as np + +plt = pg.plot(np.random.normal(size=100), title="View limit example") +plt.centralWidget.vb.setLimits(xMin=-20, xMax=120, minXRange=5, maxXRange=100) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'): + pg.QtGui.QApplication.exec_() diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 8a4875ab..579589f5 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1790,7 +1790,7 @@ def isosurface(data, level): [1, 1, 0, 2], [0, 1, 0, 2], #[9, 9, 9, 9] ## fake - ], dtype=np.ubyte) + ], dtype=np.uint16) # don't use ubyte here! This value gets added to cell index later; will need the extra precision. nTableFaces = np.array([len(f)/3 for f in triTable], dtype=np.ubyte) faceShiftTables = [None] for i in range(1,6): @@ -1889,7 +1889,6 @@ def isosurface(data, level): #profiler() if cells.shape[0] == 0: continue - #cellInds = index[(cells*ins[np.newaxis,:]).sum(axis=1)] cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round #profiler() @@ -1901,9 +1900,7 @@ def isosurface(data, level): #profiler() ### expensive: - #print verts.shape verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2) - #vertInds = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want. vertInds = cutEdges[verts] #profiler() nv = vertInds.shape[0] diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index a4016522..b8325736 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -31,6 +31,15 @@ class GraphicsLayout(GraphicsWidget): #ret = GraphicsWidget.resizeEvent(self, ev) #print self.pos(), self.mapToDevice(self.rect().topLeft()) #return ret + + def setBorder(self, *args, **kwds): + """ + Set the pen used to draw border between cells. + + See :func:`mkPen ` for arguments. + """ + self.border = fn.mkPen(*args, **kwds) + self.update() def nextRow(self): """Advance to next row for automatic item placement""" diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 7c80859d..6da8aedc 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -322,6 +322,8 @@ class ImageItem(GraphicsObject): mx = stepData.max() step = np.ceil((mx-mn) / 500.) bins = np.arange(mn, mx+1.01*step, step, dtype=np.int) + if len(bins) == 0: + bins = [mn, mx] else: bins = 500 diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 138244a5..aab546b2 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -69,6 +69,7 @@ class PlotItem(GraphicsWidget): :func:`setYLink `, :func:`setAutoPan `, :func:`setAutoVisible `, + :func:`setLimits `, :func:`viewRect `, :func:`viewRange `, :func:`setMouseEnabled `, @@ -195,7 +196,7 @@ class PlotItem(GraphicsWidget): ## Wrap a few methods from viewBox for m in [ 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', - 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', + 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY', 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. setattr(self, m, getattr(self.vb, m)) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 15434e53..59cabfa7 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -118,6 +118,15 @@ class ViewBox(GraphicsWidget): 'wheelScaleFactor': -1.0 / 8.0, 'background': None, + + # Limits + 'limits': { + 'xLimits': [None, None], # Maximum and minimum visible X values + 'yLimits': [None, None], # Maximum and minimum visible Y values + 'xRange': [None, None], # Maximum and minimum X range + 'yRange': [None, None], # Maximum and minimum Y range + } + } self._updatingRange = False ## Used to break recursive loops. See updateAutoRange. self._itemBoundsCache = weakref.WeakKeyDictionary() @@ -398,6 +407,13 @@ class ViewBox(GraphicsWidget): print("make qrectf failed:", self.state['targetRange']) raise + def _resetTarget(self): + # Reset target range to exactly match current view range. + # This is used during mouse interaction to prevent unpredictable + # behavior (because the user is unaware of targetRange). + if self.state['aspectLocked'] is False: # (interferes with aspect locking) + self.state['targetRange'] = [self.state['viewRange'][0][:], self.state['viewRange'][1][:]] + def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True): """ Set the visible range of the ViewBox. @@ -571,6 +587,53 @@ class ViewBox(GraphicsWidget): else: padding = 0.02 return padding + + def setLimits(self, **kwds): + """ + Set limits that constrain the possible view ranges. + + **Panning limits**. The following arguments define the region within the + viewbox coordinate system that may be accessed by panning the view. + =========== ============================================================ + xMin Minimum allowed x-axis value + xMax Maximum allowed x-axis value + yMin Minimum allowed y-axis value + yMax Maximum allowed y-axis value + =========== ============================================================ + + **Scaling limits**. These arguments prevent the view being zoomed in or + out too far. + =========== ============================================================ + minXRange Minimum allowed left-to-right span across the view. + maxXRange Maximum allowed left-to-right span across the view. + minYRange Minimum allowed top-to-bottom span across the view. + maxYRange Maximum allowed top-to-bottom span across the view. + =========== ============================================================ + """ + update = False + + #for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']: + #if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: + #self.state['limits'][kwd] = kwds[kwd] + #update = True + for axis in [0,1]: + for mnmx in [0,1]: + kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx] + lname = ['xLimits', 'yLimits'][axis] + if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]: + self.state['limits'][lname][mnmx] = kwds[kwd] + update = True + kwd = [['minXRange', 'maxXRange'], ['minYRange', 'maxYRange']][axis][mnmx] + lname = ['xRange', 'yRange'][axis] + if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]: + self.state['limits'][lname][mnmx] = kwds[kwd] + update = True + + if update: + self.updateViewRange() + + + def scaleBy(self, s=None, center=None, x=None, y=None): """ @@ -1056,6 +1119,7 @@ class ViewBox(GraphicsWidget): center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) #center = ev.pos() + self._resetTarget() self.scaleBy(s, center) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) ev.accept() @@ -1113,6 +1177,7 @@ class ViewBox(GraphicsWidget): x = tr.x() if mask[0] == 1 else None y = tr.y() if mask[1] == 1 else None + self._resetTarget() self.translateBy(x=x, y=y) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) elif ev.button() & QtCore.Qt.RightButton: @@ -1132,6 +1197,7 @@ class ViewBox(GraphicsWidget): y = s[1] if mouseEnabled[1] == 1 else None center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton))) + self._resetTarget() self.scaleBy(x=x, y=y, center=center) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) @@ -1327,9 +1393,9 @@ class ViewBox(GraphicsWidget): viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] changed = [False, False] - # Make correction for aspect ratio constraint + #-------- Make correction for aspect ratio constraint ---------- - ## aspect is (widget w/h) / (view range w/h) + # aspect is (widget w/h) / (view range w/h) aspect = self.state['aspectLocked'] # size ratio / view ratio tr = self.targetRect() bounds = self.rect() @@ -1351,7 +1417,6 @@ class ViewBox(GraphicsWidget): # then make the entire target range visible ax = 0 if targetRatio > viewRatio else 1 - #### these should affect viewRange, not targetRange! if ax == 0: ## view range needs to be taller than target dy = 0.5 * (tr.width() / viewRatio - tr.height()) @@ -1364,8 +1429,59 @@ class ViewBox(GraphicsWidget): if dx != 0: changed[0] = True viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] + + # ----------- Make corrections for view limits ----------- + + limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits']) + minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]] + maxRng = [self.state['limits']['xRange'][1], self.state['limits']['yRange'][1]] + + for axis in [0, 1]: + if limits[axis][0] is None and limits[axis][1] is None and minRng[axis] is None and maxRng[axis] is None: + continue - changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] + # max range cannot be larger than bounds, if they are given + if limits[axis][0] is not None and limits[axis][1] is not None: + if maxRng[axis] is not None: + maxRng[axis] = min(maxRng[axis], limits[axis][1]-limits[axis][0]) + else: + maxRng[axis] = limits[axis][1]-limits[axis][0] + + #print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, limits[axis], minRng[axis], maxRng[axis]) + #print "Starting range:", viewRange[axis] + + # Apply xRange, yRange + diff = viewRange[axis][1] - viewRange[axis][0] + if maxRng[axis] is not None and diff > maxRng[axis]: + delta = maxRng[axis] - diff + changed[axis] = True + elif minRng[axis] is not None and diff < minRng[axis]: + delta = minRng[axis] - diff + changed[axis] = True + else: + delta = 0 + + viewRange[axis][0] -= delta/2. + viewRange[axis][1] += delta/2. + + #print "after applying min/max:", viewRange[axis] + + # Apply xLimits, yLimits + mn, mx = limits[axis] + if mn is not None and viewRange[axis][0] < mn: + delta = mn - viewRange[axis][0] + viewRange[axis][0] += delta + viewRange[axis][1] += delta + changed[axis] = True + elif mx is not None and viewRange[axis][1] > mx: + delta = mx - viewRange[axis][1] + viewRange[axis][0] += delta + viewRange[axis][1] += delta + changed[axis] = True + + #print "after applying edge limits:", viewRange[axis] + + changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange # emit range change signals diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index d74a13ce..0516bf08 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -180,7 +180,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): i.paint() except: from .. import debug - pyqtgraph.debug.printExc() + debug.printExc() msg = "Error while drawing item %s." % str(item) ver = glGetString(GL_VERSION) if ver is not None: diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index 3046459d..e6888c16 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -266,7 +266,11 @@ class MeshData(object): vertFaces = self.vertexFaces() self._vertexNormals = np.empty(self._vertexes.shape, dtype=float) for vindex in xrange(self._vertexes.shape[0]): - norms = faceNorms[vertFaces[vindex]] ## get all face normals + faces = vertFaces[vindex] + if len(faces) == 0: + self._vertexNormals[vindex] = (0,0,0) + continue + norms = faceNorms[faces] ## get all face normals norm = norms.sum(axis=0) ## sum normals norm /= (norm**2).sum()**0.5 ## and re-normalize self._vertexNormals[vindex] = norm @@ -403,12 +407,10 @@ class MeshData(object): Return list mapping each vertex index to a list of face indexes that use the vertex. """ if self._vertexFaces is None: - self._vertexFaces = [None] * len(self.vertexes()) + self._vertexFaces = [[] for i in xrange(len(self.vertexes()))] for i in xrange(self._faces.shape[0]): face = self._faces[i] for ind in face: - if self._vertexFaces[ind] is None: - self._vertexFaces[ind] = [] ## need a unique/empty list to fill self._vertexFaces[ind].append(i) return self._vertexFaces diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index f9b544f5..12176c74 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -33,6 +33,7 @@ class PlotWidget(GraphicsView): :func:`enableAutoRange `, :func:`disableAutoRange `, :func:`setAspectLocked `, + :func:`setLimits `, :func:`register `, :func:`unregister ` @@ -52,7 +53,10 @@ class PlotWidget(GraphicsView): self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem ## NOTE: If you change this list, update the documentation above as well. - for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', 'register', 'unregister', 'viewRect']: + for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', + 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', + 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', + 'setLimits', 'register', 'unregister', 'viewRect']: setattr(self, m, getattr(self.plotItem, m)) #QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged)