Mid-way through overhaul. Proposed code path looks like:

setRange -> updateViewRange -> matrix dirty
                            -> sigRangeChanged

    ... -> prepareForPaint -> updateAutoRange, updateMatrix if dirty
This commit is contained in:
Luke Campagnola 2013-11-03 17:10:59 -05:00
parent ab1b1c6adf
commit a4103dd152
3 changed files with 280 additions and 120 deletions

View File

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

View File

@ -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 <<This should be moved>>
#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 <<This should be moved>>
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:

View File

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