Added ViewBox auto-ranging options:

- auto pan (center of view changes, but total range does not)
  - scale only to visible data (data outside of viewbox is ignored when auto-ranging)
This commit is contained in:
Luke Campagnola 2012-05-08 17:56:55 -04:00
parent 9107eed243
commit f21c3986d5
9 changed files with 133 additions and 28 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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())

View File

@ -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))

View File

@ -70,6 +70,8 @@ class PlotItem(GraphicsWidget):
:func:`autoRange <pyqtgraph.ViewBox.autoRange>`,
:func:`setXLink <pyqtgraph.ViewBox.setXLink>`,
:func:`setYLink <pyqtgraph.ViewBox.setYLink>`,
:func:`setAutoPan <pyqtgraph.ViewBox.setAutoPan>`,
:func:`setAutoVisible <pyqtgraph.ViewBox.setAutoVisible>`,
:func:`viewRect <pyqtgraph.ViewBox.viewRect>`,
:func:`viewRange <pyqtgraph.ViewBox.viewRange>`,
:func:`setMouseEnabled <pyqtgraph.ViewBox.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.

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)