diff --git a/graphicsItems/InfiniteLine.py b/graphicsItems/InfiniteLine.py index 071d6750..70c3b07a 100644 --- a/graphicsItems/InfiniteLine.py +++ b/graphicsItems/InfiniteLine.py @@ -191,7 +191,7 @@ class InfiniteLine(UIGraphicsItem): p.drawLine(Point(br.right(), 0), Point(br.left(), 0)) #p.drawRect(self.boundingRect()) - def dataBounds(self, axis, frac=1.0): + def dataBounds(self, axis, frac=1.0, orthoRange=None): if axis == 0: return None ## x axis should never be auto-scaled else: diff --git a/graphicsItems/LinearRegionItem.py b/graphicsItems/LinearRegionItem.py index c8250a34..1c81584e 100644 --- a/graphicsItems/LinearRegionItem.py +++ b/graphicsItems/LinearRegionItem.py @@ -146,7 +146,7 @@ class LinearRegionItem(UIGraphicsItem): p.drawRect(self.boundingRect()) #prof.finish() - def dataBounds(self, axis, frac=1.0): + def dataBounds(self, axis, frac=1.0, orthoRange=None): if axis == self.orientation: return self.getRegion() else: diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index 8840e339..f280e408 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -82,15 +82,23 @@ class PlotCurveItem(GraphicsObject): def getData(self): return self.xData, self.yData - def dataBounds(self, ax, frac=1.0): + def dataBounds(self, ax, frac=1.0, orthoRange=None): (x, y) = self.getData() if x is None or len(x) == 0: return (0, 0) 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 frac >= 1.0: return (d.min(), d.max()) diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index 8c10512b..f287f9f3 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -421,15 +421,39 @@ class PlotDataItem(GraphicsObject): #print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() return self.xDisp, self.yDisp - def dataBounds(self, ax, frac=1.0): + def dataBounds(self, ax, frac=1.0, orthoRange=None): + """ + Returns the range occupied by the data (along a specific axis) in this item. + Tis method is called by ViewBox when auto-scaling. + =============== ============================================================= + **Arguments:** + ax (0 or 1) the axis for which to return this item's data range + frac (float 0.0-1.0) Specifies what fraction of the total data + range to return. By default, the entire range is returned. + This allows the ViewBox to ignore large spikes in the data + when auto-scaling. + orthoRange ([min,max] or None) Specifies that only the data within the + given range (orthogonal to *ax*) should me measured when + returning the data range. (For example, a ViewBox might ask + what is the y-range of all data with x-values between min + and max) + =============== ============================================================= + """ (x, y) = self.getData() if x is None or len(x) == 0: return (0, 0) 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 frac >= 1.0: return (np.min(d), np.max(d)) diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index 2bbbc39f..4b40f34d 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -70,6 +70,8 @@ class PlotItem(GraphicsWidget): :func:`autoRange `, :func:`setXLink `, :func:`setYLink `, + :func:`setAutoPan `, + :func:`setAutoVisible `, :func:`viewRect `, :func:`viewRange `, :func:`setMouseEnabled `, @@ -188,7 +190,7 @@ class PlotItem(GraphicsWidget): ## Wrap a few methods from viewBox for m in [ - 'setXRange', 'setYRange', 'setXLink', 'setYLink', + 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index 0ae9f14c..4f9c61ec 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -341,7 +341,7 @@ class ScatterPlotItem(GraphicsObject): self.bounds = [None, None] - def dataBounds(self, ax, frac=1.0): + def dataBounds(self, ax, frac=1.0, orthoRange=None): if frac >= 1.0 and self.bounds[ax] is not None: return self.bounds[ax] @@ -350,8 +350,15 @@ class ScatterPlotItem(GraphicsObject): if ax == 0: d = self.data['x'] + d2 = self.data['y'] elif ax == 1: d = self.data['y'] + d2 = self.data['x'] + + if orthoRange is not None: + mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) + d = d[mask] + d2 = d2[mask] if frac >= 1.0: minIndex = np.argmin(d) diff --git a/graphicsItems/UIGraphicsItem.py b/graphicsItems/UIGraphicsItem.py index e1cce7ed..3bef29e3 100644 --- a/graphicsItems/UIGraphicsItem.py +++ b/graphicsItems/UIGraphicsItem.py @@ -85,7 +85,7 @@ class UIGraphicsItem(GraphicsObject): self._boundingRect = br return QtCore.QRectF(self._boundingRect) - def dataBounds(self, axis, frac=1.0): + def dataBounds(self, axis, frac=1.0, orthoRange=None): """Called by ViewBox for determining the auto-range bounds. By default, UIGraphicsItems are excluded from autoRange.""" return None diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index cf88508f..04fda9e3 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -97,6 +97,8 @@ class ViewBox(GraphicsWidget): 'aspectLocked': False, ## False if aspect is unlocked, otherwise float specifies the locked ratio. 'autoRange': [True, True], ## False if auto range is disabled, ## otherwise float gives the fraction of data that is visible + 'autoPan': [False, False], ## whether to only pan (do not change scaling) when auto-range is enabled + 'autoVisibleOnly': [False, False], ## whether to auto-range only to the visible portion of a plot 'linkedViews': [None, None], 'mouseEnabled': [enableMouse, enableMouse], @@ -368,8 +370,11 @@ class ViewBox(GraphicsWidget): link = self.state['linkedViews'][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=0.02, update=True): """ @@ -466,8 +471,29 @@ class ViewBox(GraphicsWidget): def autoRangeEnabled(self): return self.state['autoRange'][:] + def setAutoPan(self, x=None, y=None): + if x is not None: + self.state['autoPan'][0] = x + if y is not None: + self.state['autoPan'][1] = y + if None not in [x,y]: + self.updateAutoRange() + + def setAutoVisible(self, x=None, y=None): + if x is not None: + self.state['autoVisibleOnly'][0] = x + if x is True: + self.state['autoVisibleOnly'][1] = False + if y is not None: + self.state['autoVisibleOnly'][1] = y + if y is True: + self.state['autoVisibleOnly'][0] = False + + if x is not None or y is not None: + self.updateAutoRange() + def updateAutoRange(self): - tr = self.viewRect() + targetRect = self.viewRange() if not any(self.state['autoRange']): return @@ -475,19 +501,53 @@ class ViewBox(GraphicsWidget): for i in [0,1]: if type(fractionVisible[i]) is bool: fractionVisible[i] = 1.0 - cr = self.childrenBoundingRect(frac=fractionVisible) - wp = cr.width() * 0.02 - hp = cr.height() * 0.02 - cr = cr.adjusted(-wp, -hp, wp, hp) - - if self.state['autoRange'][0] is not False: - tr.setLeft(cr.left()) - tr.setRight(cr.right()) - if self.state['autoRange'][1] is not False: - tr.setTop(cr.top()) - tr.setBottom(cr.bottom()) + + childRect = None + + order = [0,1] + if self.state['autoVisibleOnly'][0] is True: + order = [1,0] + + for ax in order: + if self.state['autoRange'][ax] is False: + continue + if self.state['autoVisibleOnly'][ax]: + oRange = [None, None] + oRange[ax] = targetRect[1-ax] + childRect = self.childrenBoundingRect(frac=fractionVisible, orthoRange=oRange) + + else: + if childRect is None: + childRect = self.childrenBoundingRect(frac=fractionVisible) + + if ax == 0: + ## Make corrections to X range + if self.state['autoPan'][0]: + x = childRect.center().x() + w2 = (targetRect[0][1]-targetRect[0][0]) / 2. + childRect.setLeft(x-w2) + childRect.setRight(x+w2) + else: + wp = childRect.width() * 0.02 + childRect = childRect.adjusted(-wp, 0, wp, 0) + + targetRect[0][0] = childRect.left() + targetRect[0][1] = childRect.right() + else: + ## Make corrections to Y range + if self.state['autoPan'][1]: + y = childRect.center().y() + h2 = (targetRect[1][1]-targetRect[1][0]) / 2. + childRect.setTop(y-h2) + childRect.setBottom(y+h2) + else: + hp = childRect.height() * 0.02 + childRect = childRect.adjusted(0, -hp, 0, hp) + + targetRect[1][0] = childRect.top() + targetRect[1][1] = childRect.bottom() - self.setRange(tr, padding=0, disableAutoRange=False) + self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False) def setXLink(self, view): """Link this view's X axis to another view. (see LinkView)""" @@ -855,7 +915,7 @@ class ViewBox(GraphicsWidget): - def childrenBoundingRect(self, frac=None): + def childrenBoundingRect(self, frac=None, orthoRange=(None,None)): """Return the bounding range of all children. [[xmin, xmax], [ymin, ymax]] Values may be None if there are no specific bounds for an axis. @@ -880,8 +940,8 @@ class ViewBox(GraphicsWidget): if hasattr(item, 'dataBounds'): if frac is None: frac = (1.0, 1.0) - xr = item.dataBounds(0, frac=frac[0]) - yr = item.dataBounds(1, frac=frac[1]) + xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) + yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) if xr is None or xr == (None, None): useX = False xr = (0,0) diff --git a/graphicsItems/ViewBox/ViewBoxMenu.py b/graphicsItems/ViewBox/ViewBoxMenu.py index 9d0b1667..cc0a9bbf 100644 --- a/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/graphicsItems/ViewBox/ViewBoxMenu.py @@ -110,6 +110,8 @@ class ViewBoxMenu(QtGui.QMenu): self.ctrl[i].maxText.setText("%0.5g" % tr[1]) if state['autoRange'][i] is not False: self.ctrl[i].autoRadio.setChecked(True) + if state['autoRange'][i] is not True: + self.ctrl[i].autoPercentSpin.setValue(state['autoRange'][i]*100) else: self.ctrl[i].manualRadio.setChecked(True) self.ctrl[i].mouseCheck.setChecked(state['mouseEnabled'][i]) @@ -132,6 +134,8 @@ class ViewBoxMenu(QtGui.QMenu): finally: c.blockSignals(False) + self.ctrl[i].autoPanCheck.setChecked(state['autoPan'][i]) + self.ctrl[i].visibleOnlyCheck.setChecked(state['autoVisibleOnly'][i]) self.valid = True @@ -165,10 +169,10 @@ class ViewBoxMenu(QtGui.QMenu): self.view.setXLink(str(self.ctrl[0].linkCombo.currentText())) def xAutoPanToggled(self, b): - pass + self.view.setAutoPan(x=b) def xVisibleOnlyToggled(self, b): - pass + self.view.setAutoVisible(x=b) def yMouseToggled(self, b): @@ -197,10 +201,10 @@ class ViewBoxMenu(QtGui.QMenu): self.view.setYLink(str(self.ctrl[1].linkCombo.currentText())) def yAutoPanToggled(self, b): - pass + self.view.setAutoPan(y=b) def yVisibleOnlyToggled(self, b): - pass + self.view.setAutoVisible(y=b)