From 797a8c0f08362d9d0c18a9d72a9ae3503bebacb3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 8 Dec 2013 09:38:43 -0500 Subject: [PATCH 1/4] Implementing user-defined limits for ViewBox --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 82 +++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 70012ec4..845efe43 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -118,6 +118,15 @@ class ViewBox(GraphicsWidget): 'wheelScaleFactor': -1.0 / 8.0, 'background': None, + + # Limits + 'limits': { + 'xRange': [None, None], # Maximum and minimum visible X values + 'yRange': [None, None], # Maximum and minimum visible Y values + 'minRange': [None, None], # Minimum allowed range for both axes + 'maxRange': [None, None], # Maximum allowed range for both axes + } + } self._updatingRange = False ## Used to break recursive loops. See updateAutoRange. self._itemBoundsCache = weakref.WeakKeyDictionary() @@ -571,6 +580,40 @@ class ViewBox(GraphicsWidget): else: padding = 0.02 return padding + + def setLimits(self, **kwds): + """ + Set limits that constrain the possible view ranges. + + =========== ============================================================ + Arguments + xRange (min, max) limits for x-axis range + yRange (min, max) limits for y-axis range + minRange (x, y) minimum allowed span + maxRange (x, y) maximum allowed span + xMin Minimum allowed x-axis range + xMax Maximum allowed x-axis range + yMin Minimum allowed y-axis range + yMax Maximum allowed y-axis range + =========== ============================================================ + """ + for kwd in ['xRange', 'yRange', 'minRange', 'maxRange']: + if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: + self.state['limits'][kwd] = kwds[kwd] + update = True + for axis in [0,1]: + for mnmx in [0,1]: + kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx] + lname = ['xRange', 'yRange'][axis] + if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]: + self.state['limits'][lname][mnmx] = kwds[kwd] + update = True + + if update: + self.updateViewRange() + + + def scaleBy(self, s=None, center=None, x=None, y=None): """ @@ -1351,7 +1394,6 @@ class ViewBox(GraphicsWidget): # then make the entire target range visible ax = 0 if targetRatio > viewRatio else 1 - #### these should affect viewRange, not targetRange! if ax == 0: ## view range needs to be taller than target dy = 0.5 * (tr.width() / viewRatio - tr.height()) @@ -1364,6 +1406,44 @@ class ViewBox(GraphicsWidget): if dx != 0: changed[0] = True viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] + + # check for any requested limits + rng = (self.state['limits']['xRange'], self.state['limits']['yRange']) + minRng = self.state['limits']['minRange'] + maxRng = self.state['limits']['maxRange'] + + for axis in [0, 1]: + # max range cannot be larger than bounds, if they are given + if rng[axis][0] is not None and rng[axis][1] is not None: + maxRng[axis] = min(maxRng[axis], rng[axis][1]-rng[axis][0]) + + diff = viewRange[axis][1] - viewRange[axis][0] + print axis, diff, maxRng[axis] + if maxRng[axis] is not None and diff > maxRng[axis]: + delta = maxRng[axis] - diff + changed[axis] = True + elif minRng[axis] is not None and diff < minRng[axis]: + delta = minRng[axis] - diff + changed[axis] = True + else: + delta = 0 + + viewRange[axis][0] -= diff/2. + viewRange[axis][1] += diff/2. + print viewRange + + mn, mx = rng[axis] + if mn is not None and viewRange[axis][0] < mn: + delta = mn - viewRange[axis][0] + viewRange[axis][0] += delta + viewRange[axis][1] += delta + elif mx is not None and viewRange[axis][1] > mx: + delta = mx - viewRange[axis][1] + viewRange[axis][0] += delta + viewRange[axis][1] += delta + + + changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange From b0cafce3b4129a72125be13d12fef5387d2d3949 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 30 Jan 2014 10:50:07 -0500 Subject: [PATCH 2/4] Basic view limits appear to be working. --- examples/SimplePlot.py | 6 +++-- examples/ViewLimits.py | 15 ++++++++++++ pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 28 +++++++++++++++------- 3 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 examples/ViewLimits.py diff --git a/examples/SimplePlot.py b/examples/SimplePlot.py index f572743a..cacdbbdc 100644 --- a/examples/SimplePlot.py +++ b/examples/SimplePlot.py @@ -2,13 +2,15 @@ import initExample ## Add path to library (just for examples; you do not need th from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg +import pyqtgraph.exporters import numpy as np plt = pg.plot(np.random.normal(size=100), title="Simplest possible plotting example") plt.getAxis('bottom').setTicks([[(x*20, str(x*20)) for x in range(6)]]) -## Start Qt event loop unless running in interactive mode or using pyside. -ex = pg.exporters.SVGExporter.SVGExporter(plt.plotItem.scene()) + +ex = pg.exporters.SVGExporter(plt.plotItem.scene()) ex.export('/home/luke/tmp/test.svg') +## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': import sys if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'): diff --git a/examples/ViewLimits.py b/examples/ViewLimits.py new file mode 100644 index 00000000..c08bf77c --- /dev/null +++ b/examples/ViewLimits.py @@ -0,0 +1,15 @@ +import initExample ## Add path to library (just for examples; you do not need this) + +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +import numpy as np + +plt = pg.plot(np.random.normal(size=100), title="View limit example") +plt.centralWidget.vb.setLimits(xRange=[-100, 100], minRange=[0.1, None], maxRange=[50, None]) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'): + pg.QtGui.QApplication.exec_() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 845efe43..d4467286 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -597,6 +597,8 @@ class ViewBox(GraphicsWidget): yMax Maximum allowed y-axis range =========== ============================================================ """ + update = False + for kwd in ['xRange', 'yRange', 'minRange', 'maxRange']: if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: self.state['limits'][kwd] = kwds[kwd] @@ -1409,16 +1411,24 @@ class ViewBox(GraphicsWidget): # check for any requested limits rng = (self.state['limits']['xRange'], self.state['limits']['yRange']) - minRng = self.state['limits']['minRange'] - maxRng = self.state['limits']['maxRange'] + minRng = self.state['limits']['minRange'][:] + maxRng = self.state['limits']['maxRange'][:] for axis in [0, 1]: + if rng[axis][0] is None and rng[axis][1] is None and minRng[axis] is None and maxRng[axis] is None: + continue + # max range cannot be larger than bounds, if they are given if rng[axis][0] is not None and rng[axis][1] is not None: - maxRng[axis] = min(maxRng[axis], rng[axis][1]-rng[axis][0]) + if maxRng[axis] is not None: + maxRng[axis] = min(maxRng[axis], rng[axis][1]-rng[axis][0]) + else: + maxRng[axis] = rng[axis][1]-rng[axis][0] + + #print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, rng[axis], minRng[axis], maxRng[axis]) + #print "Starting range:", viewRange[axis] diff = viewRange[axis][1] - viewRange[axis][0] - print axis, diff, maxRng[axis] if maxRng[axis] is not None and diff > maxRng[axis]: delta = maxRng[axis] - diff changed[axis] = True @@ -1428,9 +1438,10 @@ class ViewBox(GraphicsWidget): else: delta = 0 - viewRange[axis][0] -= diff/2. - viewRange[axis][1] += diff/2. - print viewRange + viewRange[axis][0] -= delta/2. + viewRange[axis][1] += delta/2. + + #print "after applying min/max:", viewRange[axis] mn, mx = rng[axis] if mn is not None and viewRange[axis][0] < mn: @@ -1442,8 +1453,7 @@ class ViewBox(GraphicsWidget): viewRange[axis][0] += delta viewRange[axis][1] += delta - - + #print "after applying edge limits:", viewRange[axis] changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange From d0ed3ba2457d5cb10cc80befbf1cd0734b89c441 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 31 Jan 2014 13:04:47 -0500 Subject: [PATCH 3/4] Removed duplicate limit-setting arguments Renamed args for clarity, improved documentation Fixed interaction bugs - zooming works correctly when view is against limit - no more phantom target range; target is reset during mouse interaction. --- examples/ViewLimits.py | 2 +- pyqtgraph/graphicsItems/GraphicsLayout.py | 9 +++ pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 86 ++++++++++++++-------- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/examples/ViewLimits.py b/examples/ViewLimits.py index c08bf77c..c8f0dd21 100644 --- a/examples/ViewLimits.py +++ b/examples/ViewLimits.py @@ -5,7 +5,7 @@ import pyqtgraph as pg import numpy as np plt = pg.plot(np.random.normal(size=100), title="View limit example") -plt.centralWidget.vb.setLimits(xRange=[-100, 100], minRange=[0.1, None], maxRange=[50, None]) +plt.centralWidget.vb.setLimits(xMin=-20, xMax=120, minXRange=5, maxXRange=100) ## Start Qt event loop unless running in interactive mode or using pyside. diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index a4016522..b8325736 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -31,6 +31,15 @@ class GraphicsLayout(GraphicsWidget): #ret = GraphicsWidget.resizeEvent(self, ev) #print self.pos(), self.mapToDevice(self.rect().topLeft()) #return ret + + def setBorder(self, *args, **kwds): + """ + Set the pen used to draw border between cells. + + See :func:`mkPen ` for arguments. + """ + self.border = fn.mkPen(*args, **kwds) + self.update() def nextRow(self): """Advance to next row for automatic item placement""" diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index d4467286..0fe6cd53 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -121,10 +121,10 @@ class ViewBox(GraphicsWidget): # Limits 'limits': { - 'xRange': [None, None], # Maximum and minimum visible X values - 'yRange': [None, None], # Maximum and minimum visible Y values - 'minRange': [None, None], # Minimum allowed range for both axes - 'maxRange': [None, None], # Maximum allowed range for both axes + 'xLimits': [None, None], # Maximum and minimum visible X values + 'yLimits': [None, None], # Maximum and minimum visible Y values + 'xRange': [None, None], # Maximum and minimum X range + 'yRange': [None, None], # Maximum and minimum Y range } } @@ -407,6 +407,13 @@ class ViewBox(GraphicsWidget): print("make qrectf failed:", self.state['targetRange']) raise + def _resetTarget(self): + # Reset target range to exactly match current view range. + # This is used during mouse interaction to prevent unpredictable + # behavior (because the user is unaware of targetRange). + if self.state['aspectLocked'] is False: # (interferes with aspect locking) + self.state['targetRange'] = [self.state['viewRange'][0][:], self.state['viewRange'][1][:]] + def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True): """ Set the visible range of the ViewBox. @@ -585,27 +592,38 @@ class ViewBox(GraphicsWidget): """ Set limits that constrain the possible view ranges. + **Panning limits**. The following arguments define the region within the + viewbox coordinate system that may be accessed by panning the view. =========== ============================================================ - Arguments - xRange (min, max) limits for x-axis range - yRange (min, max) limits for y-axis range - minRange (x, y) minimum allowed span - maxRange (x, y) maximum allowed span - xMin Minimum allowed x-axis range - xMax Maximum allowed x-axis range - yMin Minimum allowed y-axis range - yMax Maximum allowed y-axis range + xMin Minimum allowed x-axis value + xMax Maximum allowed x-axis value + yMin Minimum allowed y-axis value + yMax Maximum allowed y-axis value + =========== ============================================================ + + **Scaling limits**. These arguments prevent the view being zoomed in or + out too far. =========== ============================================================ + minXRange Minimum allowed left-to-right span across the view. + maxXRange Maximum allowed left-to-right span across the view. + minYRange Minimum allowed top-to-bottom span across the view. + maxYRange Maximum allowed top-to-bottom span across the view. + =========== ============================================================ """ update = False - for kwd in ['xRange', 'yRange', 'minRange', 'maxRange']: - if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: - self.state['limits'][kwd] = kwds[kwd] - update = True + #for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']: + #if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: + #self.state['limits'][kwd] = kwds[kwd] + #update = True for axis in [0,1]: for mnmx in [0,1]: kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx] + lname = ['xLimits', 'yLimits'][axis] + if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]: + self.state['limits'][lname][mnmx] = kwds[kwd] + update = True + kwd = [['minXRange', 'maxXRange'], ['minYRange', 'maxYRange']][axis][mnmx] lname = ['xRange', 'yRange'][axis] if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]: self.state['limits'][lname][mnmx] = kwds[kwd] @@ -1101,6 +1119,7 @@ class ViewBox(GraphicsWidget): center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) #center = ev.pos() + self._resetTarget() self.scaleBy(s, center) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) ev.accept() @@ -1158,6 +1177,7 @@ class ViewBox(GraphicsWidget): x = tr.x() if mask[0] == 1 else None y = tr.y() if mask[1] == 1 else None + self._resetTarget() self.translateBy(x=x, y=y) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) elif ev.button() & QtCore.Qt.RightButton: @@ -1177,6 +1197,7 @@ class ViewBox(GraphicsWidget): y = s[1] if mouseEnabled[1] == 1 else None center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton))) + self._resetTarget() self.scaleBy(x=x, y=y, center=center) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) @@ -1372,9 +1393,9 @@ class ViewBox(GraphicsWidget): viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] changed = [False, False] - # Make correction for aspect ratio constraint + #-------- Make correction for aspect ratio constraint ---------- - ## aspect is (widget w/h) / (view range w/h) + # aspect is (widget w/h) / (view range w/h) aspect = self.state['aspectLocked'] # size ratio / view ratio tr = self.targetRect() bounds = self.rect() @@ -1409,25 +1430,27 @@ class ViewBox(GraphicsWidget): changed[0] = True viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] - # check for any requested limits - rng = (self.state['limits']['xRange'], self.state['limits']['yRange']) - minRng = self.state['limits']['minRange'][:] - maxRng = self.state['limits']['maxRange'][:] + # ----------- Make corrections for view limits ----------- + + limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits']) + minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]] + maxRng = [self.state['limits']['xRange'][1], self.state['limits']['yRange'][1]] for axis in [0, 1]: - if rng[axis][0] is None and rng[axis][1] is None and minRng[axis] is None and maxRng[axis] is None: + if limits[axis][0] is None and limits[axis][1] is None and minRng[axis] is None and maxRng[axis] is None: continue # max range cannot be larger than bounds, if they are given - if rng[axis][0] is not None and rng[axis][1] is not None: + if limits[axis][0] is not None and limits[axis][1] is not None: if maxRng[axis] is not None: - maxRng[axis] = min(maxRng[axis], rng[axis][1]-rng[axis][0]) + maxRng[axis] = min(maxRng[axis], limits[axis][1]-limits[axis][0]) else: - maxRng[axis] = rng[axis][1]-rng[axis][0] + maxRng[axis] = limits[axis][1]-limits[axis][0] - #print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, rng[axis], minRng[axis], maxRng[axis]) + #print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, limits[axis], minRng[axis], maxRng[axis]) #print "Starting range:", viewRange[axis] + # Apply xRange, yRange diff = viewRange[axis][1] - viewRange[axis][0] if maxRng[axis] is not None and diff > maxRng[axis]: delta = maxRng[axis] - diff @@ -1443,19 +1466,22 @@ class ViewBox(GraphicsWidget): #print "after applying min/max:", viewRange[axis] - mn, mx = rng[axis] + # Apply xLimits, yLimits + mn, mx = limits[axis] if mn is not None and viewRange[axis][0] < mn: delta = mn - viewRange[axis][0] viewRange[axis][0] += delta viewRange[axis][1] += delta + changed[axis] = True elif mx is not None and viewRange[axis][1] > mx: delta = mx - viewRange[axis][1] viewRange[axis][0] += delta viewRange[axis][1] += delta + changed[axis] = True #print "after applying edge limits:", viewRange[axis] - changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] + changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange # emit range change signals From 9093282a2a9ae0e9019dbdb46a1fccd8a5db7d06 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 4 Feb 2014 20:28:23 -0500 Subject: [PATCH 4/4] Wrap setLimits in PlotItem and PlotWidget --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 3 ++- pyqtgraph/widgets/PlotWidget.py | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 575a1599..77413fc2 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -69,6 +69,7 @@ class PlotItem(GraphicsWidget): :func:`setYLink `, :func:`setAutoPan `, :func:`setAutoVisible `, + :func:`setLimits `, :func:`viewRect `, :func:`viewRange `, :func:`setMouseEnabled `, @@ -195,7 +196,7 @@ class PlotItem(GraphicsWidget): ## Wrap a few methods from viewBox for m in [ 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', - 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', + 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY', 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. setattr(self, m, getattr(self.vb, m)) diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index f9b544f5..12176c74 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -33,6 +33,7 @@ class PlotWidget(GraphicsView): :func:`enableAutoRange `, :func:`disableAutoRange `, :func:`setAspectLocked `, + :func:`setLimits `, :func:`register `, :func:`unregister ` @@ -52,7 +53,10 @@ class PlotWidget(GraphicsView): self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem ## NOTE: If you change this list, update the documentation above as well. - for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', 'register', 'unregister', 'viewRect']: + for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', + 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', + 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', + 'setLimits', 'register', 'unregister', 'viewRect']: setattr(self, m, getattr(self.plotItem, m)) #QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged)