- Fixes to ScatterPlotItem bounding rect calculation

- Moved some functionality from UIGraphicsItem upstream to GraphicsItem
This commit is contained in:
Luke Campagnola 2012-05-14 22:05:53 -04:00
parent 8a9557cff1
commit 841006b79c
7 changed files with 154 additions and 74 deletions

View File

@ -13,6 +13,7 @@ class GraphicsItem(object):
def __init__(self, register=True): def __init__(self, register=True):
self._viewWidget = None self._viewWidget = None
self._viewBox = None self._viewBox = None
self._connectedView = None
if register: if register:
GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
@ -132,12 +133,19 @@ class GraphicsItem(object):
return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig
def pixelLength(self, direction): def pixelLength(self, direction):
"""Return the length of one pixel in the direction indicated (in local coordinates)""" """
Return the length of one pixel in the direction indicated (in local coordinates)
If the result would be infinite (this happens if the device transform is not properly configured yet),
then return None instead.
"""
dt = self.deviceTransform() dt = self.deviceTransform()
if dt is None: if dt is None:
return None return None
viewDir = Point(dt.map(direction) - dt.map(Point(0,0))) viewDir = Point(dt.map(direction) - dt.map(Point(0,0)))
norm = viewDir.norm() try:
norm = viewDir.norm()
except ZeroDivisionError:
return None
dti = dt.inverted()[0] dti = dt.inverted()[0]
return Point(dti.map(norm)-dti.map(Point(0,0))).length() return Point(dti.map(norm)-dti.map(Point(0,0))).length()
@ -235,23 +243,6 @@ class GraphicsItem(object):
def viewPos(self): def viewPos(self):
return self.mapToView(self.mapFromParent(self.pos())) return self.mapToView(self.mapFromParent(self.pos()))
#def itemChange(self, change, value):
#ret = QtGui.QGraphicsObject.itemChange(self, change, value)
#if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged:
#print "Item scene changed:", self
#self.setChildScene(self) ## This is bizarre.
#return ret
#def setChildScene(self, ch):
#scene = self.scene()
#for ch2 in ch.childItems():
#if ch2.scene() is not scene:
#print "item", ch2, "has different scene:", ch2.scene(), scene
#scene.addItem(ch2)
#QtGui.QApplication.processEvents()
#print " --> ", ch2.scene()
#self.setChildScene(ch2)
def parentItem(self): def parentItem(self):
## PyQt bug -- some items are returned incorrectly. ## PyQt bug -- some items are returned incorrectly.
return GraphicsScene.translateGraphicsItem(QtGui.QGraphicsObject.parentItem(self)) return GraphicsScene.translateGraphicsItem(QtGui.QGraphicsObject.parentItem(self))
@ -287,6 +278,57 @@ class GraphicsItem(object):
return Point(vec).angle(Point(1,0)) return Point(vec).angle(Point(1,0))
#def itemChange(self, change, value):
#ret = QtGui.QGraphicsObject.itemChange(self, change, value)
#if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged:
#print "Item scene changed:", self
#self.setChildScene(self) ## This is bizarre.
#return ret
#def setChildScene(self, ch):
#scene = self.scene()
#for ch2 in ch.childItems():
#if ch2.scene() is not scene:
#print "item", ch2, "has different scene:", ch2.scene(), scene
#scene.addItem(ch2)
#QtGui.QApplication.processEvents()
#print " --> ", ch2.scene()
#self.setChildScene(ch2)
def _updateView(self):
## called to see whether this item has a new view to connect to
## NOTE: This is called from GraphicsObject.itemChange or GraphicsWidget.itemChange.
## It is possible this item has moved to a different ViewBox or widget;
## clear out previously determined references to these.
self.forgetViewBox()
self.forgetViewWidget()
## check for this item's current viewbox or view widget
view = self.getViewBox()
if view is None:
#print " no view"
return
if self._connectedView is not None and view is self._connectedView():
#print " already have view", view
return
## disconnect from previous view
if self._connectedView is not None:
cv = self._connectedView()
if cv is not None:
#print "disconnect:", self
cv.sigRangeChanged.disconnect(self.viewRangeChanged)
## connect to new view
#print "connect:", self
view.sigRangeChanged.connect(self.viewRangeChanged)
self._connectedView = weakref.ref(view)
self.viewRangeChanged()
def viewRangeChanged(self):
"""
Called whenever the view coordinates of the ViewBox containing this item have changed.
"""
pass

View File

@ -12,4 +12,10 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
QtGui.QGraphicsObject.__init__(self, *args) QtGui.QGraphicsObject.__init__(self, *args)
GraphicsItem.__init__(self) GraphicsItem.__init__(self)
def itemChange(self, change, value):
ret = QtGui.QGraphicsObject.itemChange(self, change, value)
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
self._updateView()
return ret

View File

@ -16,6 +16,12 @@ class GraphicsWidget(GraphicsItem, QtGui.QGraphicsWidget):
GraphicsItem.__init__(self) GraphicsItem.__init__(self)
GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
def itemChange(self, change, value):
ret = QtGui.QGraphicsWidget.itemChange(self, change, value)
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
self._updateView()
return ret
#def getMenu(self): #def getMenu(self):
#pass #pass

View File

@ -21,7 +21,8 @@ class GridItem(UIGraphicsItem):
self.picture = None self.picture = None
def viewChangedEvent(self): def viewRangeChanged(self):
GraphicsObject.viewRangeChanged(self)
self.picture = None self.picture = None
#UIGraphicsItem.viewRangeChanged(self) #UIGraphicsItem.viewRangeChanged(self)
#self.update() #self.update()

View File

@ -1068,7 +1068,8 @@ class Handle(UIGraphicsItem):
return dti.map(tr.map(self.path)) return dti.map(tr.map(self.path))
def viewChangedEvent(self): def viewRangeChanged(self):
GraphicsObject.viewRangeChanged(self)
self._shape = None ## invalidate shape, recompute later if requested. self._shape = None ## invalidate shape, recompute later if requested.
#self.updateShape() #self.updateShape()

View File

@ -35,11 +35,12 @@ for k, c in coords.items():
def makeSymbolPixmap(size, pen, brush, symbol): def makeSymbolPixmap(size, pen, brush, symbol):
## Render a spot with the given parameters to a pixmap ## Render a spot with the given parameters to a pixmap
image = QtGui.QImage(size+2, size+2, QtGui.QImage.Format_ARGB32_Premultiplied) penPxWidth = np.ceil(pen.width())
image = QtGui.QImage(size+penPxWidth, size+penPxWidth, QtGui.QImage.Format_ARGB32_Premultiplied)
image.fill(0) image.fill(0)
p = QtGui.QPainter(image) p = QtGui.QPainter(image)
p.setRenderHint(p.Antialiasing) p.setRenderHint(p.Antialiasing)
p.translate(size*0.5+1, size*0.5+1) p.translate(image.width()*0.5, image.height()*0.5)
p.scale(size, size) p.scale(size, size)
p.setPen(pen) p.setPen(pen)
p.setBrush(brush) p.setBrush(brush)
@ -79,19 +80,16 @@ class ScatterPlotItem(GraphicsObject):
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
self.setFlag(self.ItemHasNoContents, True) self.setFlag(self.ItemHasNoContents, True)
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('item', object), ('data', object)]) self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('item', object), ('data', object)])
#self.spots = [] self.bounds = [None, None] ## caches data bounds
#self.fragments = None self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
self.bounds = [None, None] self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
self.opts = {'pxMode': True}
#self.spotsValid = False
#self.itemsValid = False
self._spotPixmap = None self._spotPixmap = None
self.opts = {'pxMode': True}
self.setPen(200,200,200, update=False) self.setPen(200,200,200, update=False)
self.setBrush(100,100,150, update=False) self.setBrush(100,100,150, update=False)
self.setSymbol('o', update=False) self.setSymbol('o', update=False)
self.setSize(7, update=False) self.setSize(7, update=False)
#self.setIdentical(False, update=False)
prof.mark('1') prof.mark('1')
self.setData(*args, **kargs) self.setData(*args, **kargs)
prof.mark('setData') prof.mark('setData')
@ -228,6 +226,7 @@ class ScatterPlotItem(GraphicsObject):
self.setPointData(kargs['data'], dataSet=newData) self.setPointData(kargs['data'], dataSet=newData)
#self.updateSpots() #self.updateSpots()
self.bounds = [None, None]
self.generateSpotItems() self.generateSpotItems()
self.sigPlotChanged.emit(self) self.sigPlotChanged.emit(self)
@ -345,9 +344,30 @@ class ScatterPlotItem(GraphicsObject):
def updateSpots(self, dataSet=None): def updateSpots(self, dataSet=None):
if dataSet is None: if dataSet is None:
dataSet = self.data dataSet = self.data
self._maxSpotWidth = 0
self._maxSpotPxWidth = 0
for spot in dataSet['item']: for spot in dataSet['item']:
spot.updateItem() spot.updateItem()
self.measureSpotSizes(dataSet)
def measureSpotSizes(self, dataSet):
for spot in dataSet['item']:
## keep track of the maximum spot size and pixel size
width = 0
pxWidth = 0
if self.opts['pxMode']:
pxWidth += spot.size()
else:
width += spot.size()
pen = spot.pen()
if pen.isCosmetic():
pxWidth += pen.width() * 2
else:
width += pen.width() * 2
self._maxSpotWidth = max(self._maxSpotWidth, width)
self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth)
def clear(self): def clear(self):
"""Remove all spots from the scatter plot""" """Remove all spots from the scatter plot"""
self.clearItems() self.clearItems()
@ -384,14 +404,16 @@ class ScatterPlotItem(GraphicsObject):
d2 = d2[mask] d2 = d2[mask]
if frac >= 1.0: if frac >= 1.0:
## increase size of bounds based on spot size and pen width
px = self.pixelLength(Point(1, 0) if ax == 0 else Point(0, 1)) ## determine length of pixel along this axis
if px is None:
px = 0
minIndex = np.argmin(d) minIndex = np.argmin(d)
maxIndex = np.argmax(d) maxIndex = np.argmax(d)
minVal = d[minIndex] minVal = d[minIndex]
maxVal = d[maxIndex] maxVal = d[maxIndex]
if not self.opts['pxMode']: spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth)
minVal -= self.data[minIndex]['size'] self.bounds[ax] = (minVal-spotSize, maxVal+spotSize)
maxVal += self.data[maxIndex]['size']
self.bounds[ax] = (minVal, maxVal)
return self.bounds[ax] return self.bounds[ax]
elif frac <= 0.0: elif frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
@ -412,6 +434,7 @@ class ScatterPlotItem(GraphicsObject):
for rec in self.data: for rec in self.data:
if rec['item'] is None: if rec['item'] is None:
rec['item'] = PathSpotItem(rec, self) rec['item'] = PathSpotItem(rec, self)
self.measureSpotSizes(self.data)
self.sigPlotChanged.emit(self) self.sigPlotChanged.emit(self)
def defaultSpotPixmap(self): def defaultSpotPixmap(self):
@ -430,6 +453,17 @@ class ScatterPlotItem(GraphicsObject):
ymn = 0 ymn = 0
ymx = 0 ymx = 0
return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
def viewRangeChanged(self):
GraphicsObject.viewRangeChanged(self)
self.bounds = [None, None]
def paint(self, p, *args):
## NOTE: self.paint is disabled by this line in __init__:
## self.setFlag(self.ItemHasNoContents, True)
p.setPen(fn.mkPen('r'))
p.drawRect(self.boundingRect())
def points(self): def points(self):
return self.data['item'] return self.data['item']

View File

@ -28,7 +28,6 @@ class UIGraphicsItem(GraphicsObject):
""" """
GraphicsObject.__init__(self, parent) GraphicsObject.__init__(self, parent)
self.setFlag(self.ItemSendsScenePositionChanges) self.setFlag(self.ItemSendsScenePositionChanges)
self._connectedView = None
if bounds is None: if bounds is None:
self._bounds = QtCore.QRectF(0, 0, 1, 1) self._bounds = QtCore.QRectF(0, 0, 1, 1)
@ -36,7 +35,7 @@ class UIGraphicsItem(GraphicsObject):
self._bounds = bounds self._bounds = bounds
self._boundingRect = None self._boundingRect = None
self.updateView() self._updateView()
def paint(self, *args): def paint(self, *args):
## check for a new view object every time we paint. ## check for a new view object every time we paint.
@ -45,39 +44,39 @@ class UIGraphicsItem(GraphicsObject):
def itemChange(self, change, value): def itemChange(self, change, value):
ret = GraphicsObject.itemChange(self, change, value) ret = GraphicsObject.itemChange(self, change, value)
if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged: #if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged: ## handled by GraphicsItem now.
#print "caught parent/scene change:", self.parentItem(), self.scene() ##print "caught parent/scene change:", self.parentItem(), self.scene()
self.updateView() #self.updateView()
elif change == self.ItemScenePositionHasChanged: if change == self.ItemScenePositionHasChanged:
self.setNewBounds() self.setNewBounds()
return ret return ret
def updateView(self): #def updateView(self):
## called to see whether this item has a new view to connect to ### called to see whether this item has a new view to connect to
## check for this item's current viewbox or view widget ### check for this item's current viewbox or view widget
view = self.getViewBox() #view = self.getViewBox()
if view is None: #if view is None:
#print " no view" ##print " no view"
return #return
if self._connectedView is not None and view is self._connectedView(): #if self._connectedView is not None and view is self._connectedView():
#print " already have view", view ##print " already have view", view
return #return
## disconnect from previous view ### disconnect from previous view
if self._connectedView is not None: #if self._connectedView is not None:
cv = self._connectedView() #cv = self._connectedView()
if cv is not None: #if cv is not None:
#print "disconnect:", self ##print "disconnect:", self
cv.sigRangeChanged.disconnect(self.viewRangeChanged) #cv.sigRangeChanged.disconnect(self.viewRangeChanged)
## connect to new view ### connect to new view
#print "connect:", self ##print "connect:", self
view.sigRangeChanged.connect(self.viewRangeChanged) #view.sigRangeChanged.connect(self.viewRangeChanged)
self._connectedView = weakref.ref(view) #self._connectedView = weakref.ref(view)
self.setNewBounds() #self.setNewBounds()
def boundingRect(self): def boundingRect(self):
if self._boundingRect is None: if self._boundingRect is None:
br = self.viewRect() br = self.viewRect()
@ -101,15 +100,6 @@ class UIGraphicsItem(GraphicsObject):
"""Update the item's bounding rect to match the viewport""" """Update the item's bounding rect to match the viewport"""
self._boundingRect = None ## invalidate bounding rect, regenerate later if needed. self._boundingRect = None ## invalidate bounding rect, regenerate later if needed.
self.prepareGeometryChange() self.prepareGeometryChange()
self.viewChangedEvent()
def viewChangedEvent(self):
"""
Called whenever the view coordinates have changed.
This is a good method to override if you want to respond to change of coordinates.
"""
pass
def setPos(self, *args): def setPos(self, *args):