- 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):
self._viewWidget = None
self._viewBox = None
self._connectedView = None
if register:
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
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()
if dt is None:
return None
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]
return Point(dti.map(norm)-dti.map(Point(0,0))).length()
@ -235,23 +243,6 @@ class GraphicsItem(object):
def viewPos(self):
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):
## PyQt bug -- some items are returned incorrectly.
return GraphicsScene.translateGraphicsItem(QtGui.QGraphicsObject.parentItem(self))
@ -287,6 +278,57 @@ class GraphicsItem(object):
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)
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)
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):
#pass

View File

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

View File

@ -1068,7 +1068,8 @@ class Handle(UIGraphicsItem):
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.updateShape()

View File

@ -35,11 +35,12 @@ for k, c in coords.items():
def makeSymbolPixmap(size, pen, brush, symbol):
## 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)
p = QtGui.QPainter(image)
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.setPen(pen)
p.setBrush(brush)
@ -79,19 +80,16 @@ class ScatterPlotItem(GraphicsObject):
GraphicsObject.__init__(self)
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.spots = []
#self.fragments = None
self.bounds = [None, None]
self.opts = {'pxMode': True}
#self.spotsValid = False
#self.itemsValid = False
self.bounds = [None, None] ## caches data bounds
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
self._spotPixmap = None
self.opts = {'pxMode': True}
self.setPen(200,200,200, update=False)
self.setBrush(100,100,150, update=False)
self.setSymbol('o', update=False)
self.setSize(7, update=False)
#self.setIdentical(False, update=False)
prof.mark('1')
self.setData(*args, **kargs)
prof.mark('setData')
@ -228,6 +226,7 @@ class ScatterPlotItem(GraphicsObject):
self.setPointData(kargs['data'], dataSet=newData)
#self.updateSpots()
self.bounds = [None, None]
self.generateSpotItems()
self.sigPlotChanged.emit(self)
@ -345,9 +344,30 @@ class ScatterPlotItem(GraphicsObject):
def updateSpots(self, dataSet=None):
if dataSet is None:
dataSet = self.data
self._maxSpotWidth = 0
self._maxSpotPxWidth = 0
for spot in dataSet['item']:
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):
"""Remove all spots from the scatter plot"""
self.clearItems()
@ -384,14 +404,16 @@ class ScatterPlotItem(GraphicsObject):
d2 = d2[mask]
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)
maxIndex = np.argmax(d)
minVal = d[minIndex]
maxVal = d[maxIndex]
if not self.opts['pxMode']:
minVal -= self.data[minIndex]['size']
maxVal += self.data[maxIndex]['size']
self.bounds[ax] = (minVal, maxVal)
spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth)
self.bounds[ax] = (minVal-spotSize, maxVal+spotSize)
return self.bounds[ax]
elif frac <= 0.0:
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:
if rec['item'] is None:
rec['item'] = PathSpotItem(rec, self)
self.measureSpotSizes(self.data)
self.sigPlotChanged.emit(self)
def defaultSpotPixmap(self):
@ -430,6 +453,17 @@ class ScatterPlotItem(GraphicsObject):
ymn = 0
ymx = 0
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):
return self.data['item']

View File

@ -28,7 +28,6 @@ class UIGraphicsItem(GraphicsObject):
"""
GraphicsObject.__init__(self, parent)
self.setFlag(self.ItemSendsScenePositionChanges)
self._connectedView = None
if bounds is None:
self._bounds = QtCore.QRectF(0, 0, 1, 1)
@ -36,7 +35,7 @@ class UIGraphicsItem(GraphicsObject):
self._bounds = bounds
self._boundingRect = None
self.updateView()
self._updateView()
def paint(self, *args):
## check for a new view object every time we paint.
@ -45,39 +44,39 @@ class UIGraphicsItem(GraphicsObject):
def itemChange(self, change, value):
ret = GraphicsObject.itemChange(self, change, value)
if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged:
#print "caught parent/scene change:", self.parentItem(), self.scene()
self.updateView()
elif change == self.ItemScenePositionHasChanged:
#if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged: ## handled by GraphicsItem now.
##print "caught parent/scene change:", self.parentItem(), self.scene()
#self.updateView()
if change == self.ItemScenePositionHasChanged:
self.setNewBounds()
return ret
def updateView(self):
## called to see whether this item has a new view to connect to
#def updateView(self):
### called to see whether this item has a new view to connect to
## check for this item's current viewbox or view widget
view = self.getViewBox()
if view is None:
#print " no view"
return
### 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
#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)
### 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.setNewBounds()
### connect to new view
##print "connect:", self
#view.sigRangeChanged.connect(self.viewRangeChanged)
#self._connectedView = weakref.ref(view)
#self.setNewBounds()
def boundingRect(self):
if self._boundingRect is None:
br = self.viewRect()
@ -101,15 +100,6 @@ class UIGraphicsItem(GraphicsObject):
"""Update the item's bounding rect to match the viewport"""
self._boundingRect = None ## invalidate bounding rect, regenerate later if needed.
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):