Performance improvements:

- AxisItem shows 2 tick levels instead of 3
  - Lots of boundingRect and dataBounds caching
    (improves ViewBox auto-range performance, especially with multiple plots)
  - GraphicsScene avoids testing for hover intersections with non-hoverable items
    (much less slowdown when moving mouse over plots)
These are deep changes; need good testing before we release them.
This commit is contained in:
Luke Campagnola 2013-01-09 22:21:32 -05:00
parent fa9660e381
commit 01b8968a0a
9 changed files with 234 additions and 200 deletions

View File

@ -75,6 +75,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
sigMouseMoved = QtCore.Signal(object) ## emits position of mouse on every move
sigMouseClicked = QtCore.Signal(object) ## emitted when mouse is clicked. Check for event.isAccepted() to see whether the event has already been acted on.
sigPrepareForPaint = QtCore.Signal() ## emitted immediately before the scene is about to be rendered
_addressCache = weakref.WeakValueDictionary()
ExportDirectory = None
@ -98,6 +100,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
self.clickEvents = []
self.dragButtons = []
self.prepItems = weakref.WeakKeyDictionary() ## set of items with prepareForPaintMethods
self.mouseGrabber = None
self.dragItem = None
self.lastDrag = None
@ -112,6 +115,17 @@ class GraphicsScene(QtGui.QGraphicsScene):
self.exportDialog = None
def render(self, *args):
self.prepareForPaint()
return QGraphicsScene.render(self, *args)
def prepareForPaint(self):
"""Called before every render. This method will inform items that the scene is about to
be rendered by emitting sigPrepareForPaint.
This allows items to delay expensive processing until they know a paint will be required."""
self.sigPrepareForPaint.emit()
def setClickRadius(self, r):
"""
@ -224,7 +238,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
else:
acceptable = int(ev.buttons()) == 0 ## if we are in mid-drag, do not allow items to accept the hover event.
event = HoverEvent(ev, acceptable)
items = self.itemsNearEvent(event)
items = self.itemsNearEvent(event, hoverable=True)
self.sigMouseHover.emit(items)
prevItems = list(self.hoverItems.keys())
@ -402,7 +416,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
#return item
return self.translateGraphicsItem(item)
def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder):
def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder, hoverable=False):
"""
Return an iterator that iterates first through the items that directly intersect point (in Z order)
followed by any other items that are within the scene's click radius.
@ -429,6 +443,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
## remove items whose shape does not contain point (scene.items() apparently sucks at this)
items2 = []
for item in items:
if hoverable and not hasattr(item, 'hoverEvent'):
continue
shape = item.shape()
if shape is None:
continue

View File

@ -356,8 +356,14 @@ class GarbageWatcher(object):
return self.objs[item]
class Profiler(object):
class Profiler:
"""Simple profiler allowing measurement of multiple time intervals.
Arguments:
msg: message to print at start and finish of profiling
disabled: If true, profiler does nothing (so you can leave it in place)
delayed: If true, all messages are printed after call to finish()
(this can result in more accurate time step measurements)
globalDelay: if True, all nested profilers delay printing until the top level finishes
Example:
prof = Profiler('Function')
@ -368,34 +374,65 @@ class Profiler(object):
prof.finish()
"""
depth = 0
msgs = []
def __init__(self, msg="Profiler", disabled=False):
self.depth = Profiler.depth
Profiler.depth += 1
def __init__(self, msg="Profiler", disabled=False, delayed=True, globalDelay=True):
self.disabled = disabled
if disabled:
return
self.markCount = 0
self.finished = False
self.depth = Profiler.depth
Profiler.depth += 1
if not globalDelay:
self.msgs = []
self.delayed = delayed
self.msg = " "*self.depth + msg
msg2 = self.msg + " >>> Started"
if self.delayed:
self.msgs.append(msg2)
else:
print msg2
self.t0 = ptime.time()
self.t1 = self.t0
self.msg = " "*self.depth + msg
print(self.msg, ">>> Started")
def mark(self, msg=''):
def mark(self, msg=None):
if self.disabled:
return
t1 = ptime.time()
print(" "+self.msg, msg, "%gms" % ((t1-self.t1)*1000))
self.t1 = t1
def finish(self):
if self.disabled:
if msg is None:
msg = str(self.markCount)
self.markCount += 1
t1 = ptime.time()
msg2 = " "+self.msg+" "+msg+" "+"%gms" % ((t1-self.t1)*1000)
if self.delayed:
self.msgs.append(msg2)
else:
print msg2
self.t1 = ptime.time() ## don't measure time it took to print
def finish(self, msg=None):
if self.disabled or self.finished:
return
t1 = ptime.time()
print(self.msg, '<<< Finished, total time:', "%gms" % ((t1-self.t0)*1000))
def __del__(self):
Profiler.depth -= 1
if msg is not None:
self.mark(msg)
t1 = ptime.time()
msg = self.msg + ' <<< Finished, total time: %gms' % ((t1-self.t0)*1000)
if self.delayed:
self.msgs.append(msg)
if self.depth == 0:
for line in self.msgs:
print line
Profiler.msgs = []
else:
print msg
Profiler.depth = self.depth
self.finished = True
def profile(code, name='profile_run', sort='cumulative', num=30):

View File

@ -369,7 +369,7 @@ class AxisItem(GraphicsWidget):
return [
(intervals[minorIndex+2], 0),
(intervals[minorIndex+1], 0),
(intervals[minorIndex], 0)
#(intervals[minorIndex], 0) ## Pretty, but eats up CPU
]
##### This does not work -- switching between 2/5 confuses the automatic text-level-selection

View File

@ -3,8 +3,30 @@ from pyqtgraph.GraphicsScene import GraphicsScene
from pyqtgraph.Point import Point
import pyqtgraph.functions as fn
import weakref
from pyqtgraph.pgcollections import OrderedDict
import operator
class FiniteCache(OrderedDict):
"""Caches a finite number of objects, removing
least-frequently used items."""
def __init__(self, length):
self._length = length
OrderedDict.__init__(self)
def __setitem__(self, item, val):
self.pop(item, None) # make sure item is added to end
OrderedDict.__setitem__(self, item, val)
while len(self) > self._length:
del self[self.keys()[0]]
def __getitem__(self, item):
val = dict.__getitem__(self, item)
del self[item]
self[item] = val ## promote this key
return val
class GraphicsItem(object):
"""
**Bases:** :class:`object`
@ -16,6 +38,8 @@ class GraphicsItem(object):
The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task.
"""
_pixelVectorGlobalCache = FiniteCache(100)
def __init__(self, register=True):
if not hasattr(self, '_qtBaseClass'):
for b in self.__class__.__bases__:
@ -25,6 +49,7 @@ class GraphicsItem(object):
if not hasattr(self, '_qtBaseClass'):
raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self))
self._pixelVectorCache = [None, None]
self._viewWidget = None
self._viewBox = None
self._connectedView = None
@ -155,7 +180,6 @@ class GraphicsItem(object):
def pixelVectors(self, direction=None):
"""Return vectors in local coordinates representing the width and height of a view pixel.
If direction is specified, then return vectors parallel and orthogonal to it.
@ -163,13 +187,28 @@ class GraphicsItem(object):
Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed)
or if pixel size is below floating-point precision limit.
"""
## This is an expensive function that gets called very frequently.
## We have two levels of cache to try speeding things up.
dt = self.deviceTransform()
if dt is None:
return None, None
## check local cache
if direction is None and dt == self._pixelVectorCache[0]:
return self._pixelVectorCache[1]
## check global cache
key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
pv = self._pixelVectorGlobalCache.get(key, None)
if pv is not None:
self._pixelVectorCache = [dt, pv]
return pv
if direction is None:
direction = Point(1, 0)
direction = QtCore.QPointF(1, 0)
if direction.manhattanLength() == 0:
raise Exception("Cannot compute pixel length for 0-length vector.")
@ -184,28 +223,33 @@ class GraphicsItem(object):
r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5
directionr = direction * r
viewDir = Point(dt.map(directionr) - dt.map(Point(0,0)))
if viewDir.manhattanLength() == 0:
## map direction vector onto device
#viewDir = Point(dt.map(directionr) - dt.map(Point(0,0)))
#mdirection = dt.map(directionr)
dirLine = QtCore.QLineF(QtCore.QPointF(0,0), directionr)
viewDir = dt.map(dirLine)
if viewDir.length() == 0:
return None, None ## pixel size cannot be represented on this scale
orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
## get unit vector and orthogonal vector (length of pixel)
#orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
try:
normView = viewDir.norm() ## direction of one pixel orthogonal to line
normOrtho = orthoDir.norm()
normView = viewDir.unitVector()
#normView = viewDir.norm() ## direction of one pixel orthogonal to line
normOrtho = normView.normalVector()
#normOrtho = orthoDir.norm()
except:
raise Exception("Invalid direction %s" %directionr)
## map back to item
dti = fn.invertQTransform(dt)
return Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0)))
#pv = Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0)))
pv = Point(dti.map(normView).p2()), Point(dti.map(normOrtho).p2())
self._pixelVectorCache[1] = pv
self._pixelVectorCache[0] = dt
self._pixelVectorGlobalCache[key] = pv
return self._pixelVectorCache[1]
#vt = self.deviceTransform()
#if vt is None:
#return None
#vt = vt.inverted()[0]
#orig = vt.map(QtCore.QPointF(0, 0))
#return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig
def pixelLength(self, direction, ortho=False):
"""Return the length of one pixel in the direction indicated (in local coordinates)
@ -220,7 +264,6 @@ class GraphicsItem(object):
return orthoV.length()
return normV.length()
def pixelSize(self):
## deprecated
@ -235,7 +278,7 @@ class GraphicsItem(object):
if vt is None:
return 0
vt = fn.invertQTransform(vt)
return Point(vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).length()
return vt.map(QtCore.QLineF(0, 0, 1, 0)).length()
def pixelHeight(self):
## deprecated
@ -243,7 +286,8 @@ class GraphicsItem(object):
if vt is None:
return 0
vt = fn.invertQTransform(vt)
return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length()
return vt.map(QtCore.QLineF(0, 0, 0, 1)).length()
#return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length()
def mapToDevice(self, obj):
@ -357,10 +401,11 @@ class GraphicsItem(object):
tr = self.itemTransform(relativeItem)
if isinstance(tr, tuple): ## difference between pyside and pyqt
tr = tr[0]
vec = tr.map(Point(1,0)) - tr.map(Point(0,0))
return Point(vec).angle(Point(1,0))
tr = tr[0]
#vec = tr.map(Point(1,0)) - tr.map(Point(0,0))
vec = tr.map(QtCore.QLineF(0,0,1,0))
#return Point(vec).angle(Point(1,0))
return vec.angleTo(QtCore.QLineF(vec.p1(), vec.p1()+QtCore.QPointF(1,0)))
#def itemChange(self, change, value):
#ret = self._qtBaseClass.itemChange(self, change, value)
@ -500,3 +545,6 @@ class GraphicsItem(object):
else:
self._exportOpts = False
#def update(self):
#self._qtBaseClass.update(self)
#print "Update:", self

View File

@ -52,7 +52,7 @@ class PlotCurveItem(GraphicsObject):
self.clear()
self.path = None
self.fillPath = None
self._boundsCache = [None, None]
## this is disastrous for performance.
#self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
@ -85,6 +85,12 @@ class PlotCurveItem(GraphicsObject):
return self.xData, self.yData
def dataBounds(self, ax, frac=1.0, orthoRange=None):
## Need this to run as fast as possible.
## check cache first:
cache = self._boundsCache[ax]
if cache is not None and cache[0] == (frac, orthoRange):
return cache[1]
(x, y) = self.getData()
if x is None or len(x) == 0:
return (0, 0)
@ -103,15 +109,22 @@ class PlotCurveItem(GraphicsObject):
if frac >= 1.0:
return (d.min(), d.max())
b = (d.min(), d.max())
elif frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
else:
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
b = (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
self._boundsCache[ax] = [(frac, orthoRange), b]
return b
def invalidateBounds(self):
self._boundingRect = None
self._boundsCache = [None, None]
def setPen(self, *args, **kargs):
"""Set the pen used to draw the curve."""
self.opts['pen'] = fn.mkPen(*args, **kargs)
self.invalidateBounds()
self.update()
def setShadowPen(self, *args, **kargs):
@ -120,17 +133,20 @@ class PlotCurveItem(GraphicsObject):
pen to be visible.
"""
self.opts['shadowPen'] = fn.mkPen(*args, **kargs)
self.invalidateBounds()
self.update()
def setBrush(self, *args, **kargs):
"""Set the brush used when filling the area under the curve"""
self.opts['brush'] = fn.mkBrush(*args, **kargs)
self.invalidateBounds()
self.update()
def setFillLevel(self, level):
"""Set the level filled to when filling under the curve"""
self.opts['fillLevel'] = level
self.fillPath = None
self.invalidateBounds()
self.update()
#def setColor(self, color):
@ -221,7 +237,9 @@ class PlotCurveItem(GraphicsObject):
#self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly
## Test this bug with test_PlotWidget and zoom in on the animated plot
self.invalidateBounds()
self.prepareGeometryChange()
self.informViewBoundsChanged()
self.yData = kargs['y'].view(np.ndarray)
self.xData = kargs['x'].view(np.ndarray)
@ -349,36 +367,38 @@ class PlotCurveItem(GraphicsObject):
return self.path
def boundingRect(self):
(x, y) = self.getData()
if x is None or y is None or len(x) == 0 or len(y) == 0:
return QtCore.QRectF()
if self._boundingRect is None:
(x, y) = self.getData()
if x is None or y is None or len(x) == 0 or len(y) == 0:
return QtCore.QRectF()
if self.opts['shadowPen'] is not None:
lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
else:
lineWidth = (self.opts['pen'].width()+1)
pixels = self.pixelVectors()
if pixels == (None, None):
pixels = [Point(0,0), Point(0,0)]
xmin = x.min()
xmax = x.max()
ymin = y.min()
ymax = y.max()
if self.opts['shadowPen'] is not None:
lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
else:
lineWidth = (self.opts['pen'].width()+1)
if self.opts['fillLevel'] is not None:
ymin = min(ymin, self.opts['fillLevel'])
ymax = max(ymax, self.opts['fillLevel'])
xmin -= pixels[0].x() * lineWidth
xmax += pixels[0].x() * lineWidth
ymin -= abs(pixels[1].y()) * lineWidth
ymax += abs(pixels[1].y()) * lineWidth
pixels = self.pixelVectors()
if pixels == (None, None):
pixels = [Point(0,0), Point(0,0)]
xmin = x.min()
xmax = x.max()
ymin = y.min()
ymax = y.max()
if self.opts['fillLevel'] is not None:
ymin = min(ymin, self.opts['fillLevel'])
ymax = max(ymax, self.opts['fillLevel'])
xmin -= pixels[0].x() * lineWidth
xmax += pixels[0].x() * lineWidth
ymin -= abs(pixels[1].y()) * lineWidth
ymax += abs(pixels[1].y()) * lineWidth
return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
return self._boundingRect
def paint(self, p, opt, widget):
prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True)

View File

@ -369,9 +369,10 @@ class PlotDataItem(GraphicsObject):
self.updateItems()
prof.mark('update items')
view = self.getViewBox()
if view is not None:
view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
self.informViewBoundsChanged()
#view = self.getViewBox()
#if view is not None:
#view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
self.sigPlotChanged.emit(self)
prof.mark('emit')

View File

@ -34,8 +34,7 @@ class VTickGroup(UIGraphicsItem):
if xvals is None:
xvals = []
#bounds = QtCore.QRectF(0, yrange[0], 1, yrange[1]-yrange[0])
UIGraphicsItem.__init__(self)#, bounds=bounds)
UIGraphicsItem.__init__(self)
if pen is None:
pen = (200, 200, 200)
@ -44,15 +43,10 @@ class VTickGroup(UIGraphicsItem):
self.ticks = []
self.xvals = []
#if view is None:
#self.view = None
#else:
#self.view = weakref.ref(view)
self.yrange = [0,1]
self.setPen(pen)
self.setYRange(yrange)
self.setXVals(xvals)
#self.valid = False
def setPen(self, *args, **kwargs):
"""Set the pen to use for drawing ticks. Can be specified as any arguments valid
@ -75,80 +69,20 @@ class VTickGroup(UIGraphicsItem):
"""Set the y range [low, high] that the ticks are drawn on. 0 is the bottom of
the view, 1 is the top."""
self.yrange = vals
#self.relative = relative
#if self.view is not None:
#if relative:
#self.view().sigRangeChanged.connect(self.rescale)
#else:
#try:
#self.view().sigRangeChanged.disconnect(self.rescale)
#except:
#pass
self.rebuildTicks()
#self.valid = False
def dataBounds(self, *args, **kargs):
return None ## item should never affect view autoscaling
#def viewRangeChanged(self):
### called when the view is scaled
#UIGraphicsItem.viewRangeChanged(self)
#self.resetTransform()
##vb = self.view().viewRect()
##p1 = vb.bottom() - vb.height() * self.yrange[0]
##p2 = vb.bottom() - vb.height() * self.yrange[1]
##br = self.boundingRect()
##yr = [p1, p2]
##self.rebuildTicks()
##br = self.boundingRect()
##print br
##self.translate(0.0, br.y())
##self.scale(1.0, br.height())
##self.boundingRect()
#self.update()
#def boundingRect(self):
#print "--request bounds:"
#b = self.path.boundingRect()
#b2 = UIGraphicsItem.boundingRect(self)
#b2.setY(b.y())
#b2.setWidth(b.width())
#print " ", b
#print " ", b2
#print " ", self.mapRectToScene(b)
#return b2
def yRange(self):
#if self.relative:
#height = self.view.size().height()
#p1 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[0]))))
#p2 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[1]))))
#return [p1.y(), p2.y()]
#else:
#return self.yrange
return self.yrange
def rebuildTicks(self):
self.path = QtGui.QPainterPath()
yrange = self.yRange()
#print "rebuild ticks:", yrange
for x in self.xvals:
#path.moveTo(x, yrange[0])
#path.lineTo(x, yrange[1])
self.path.moveTo(x, 0.)
self.path.lineTo(x, 1.)
#self.setPath(self.path)
#self.valid = True
#self.rescale()
#print " done..", self.boundingRect()
def paint(self, p, *args):
UIGraphicsItem.paint(self, p, *args)
@ -161,7 +95,6 @@ class VTickGroup(UIGraphicsItem):
p.scale(1.0, br.height())
p.setPen(self.pen)
p.drawPath(self.path)
#QtGui.QGraphicsPathItem.paint(self, *args)
if __name__ == '__main__':

View File

@ -9,6 +9,7 @@ from pyqtgraph.GraphicsScene import GraphicsScene
import pyqtgraph
import weakref
from copy import deepcopy
import pyqtgraph.debug as debug
__all__ = ['ViewBox']
@ -110,6 +111,7 @@ class ViewBox(GraphicsWidget):
'background': None,
}
self._updatingRange = False ## Used to break recursive loops. See updateAutoRange.
self._itemBoundsCache = weakref.WeakKeyDictionary()
self.locateGroup = None ## items displayed when using ViewBox.locate(item)
@ -548,7 +550,7 @@ class ViewBox(GraphicsWidget):
fractionVisible[i] = 1.0
childRange = None
order = [0,1]
if self.state['autoVisibleOnly'][0] is True:
order = [1,0]
@ -571,40 +573,18 @@ class ViewBox(GraphicsWidget):
if xr is not None:
if self.state['autoPan'][ax]:
x = sum(xr) * 0.5
#x = childRect.center().x()
w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2.
#childRect.setLeft(x-w2)
#childRect.setRight(x+w2)
childRange[ax] = [x-w2, x+w2]
else:
#wp = childRect.width() * 0.02
wp = (xr[1] - xr[0]) * 0.02
#childRect = childRect.adjusted(-wp, 0, wp, 0)
childRange[ax][0] -= wp
childRange[ax][1] += wp
#targetRect[ax][0] = childRect.left()
#targetRect[ax][1] = childRect.right()
targetRect[ax] = childRange[ax]
args['xRange' if ax == 0 else 'yRange'] = targetRect[ax]
#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()
#args['yRange'] = targetRect[1]
if len(args) == 0:
return
args['padding'] = 0
args['disableAutoRange'] = False
#self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False)
self.setRange(**args)
finally:
self._updatingRange = False
@ -744,6 +724,7 @@ class ViewBox(GraphicsWidget):
self.updateAutoRange()
def itemBoundsChanged(self, item):
self._itemBoundsCache.pop(item, None)
self.updateAutoRange()
def invertY(self, b=True):
@ -1015,6 +996,8 @@ class ViewBox(GraphicsWidget):
[[xmin, xmax], [ymin, ymax]]
Values may be None if there are no specific bounds for an axis.
"""
prof = debug.Profiler('updateAutoRange', disabled=True)
#items = self.allChildren()
items = self.addedItems
@ -1029,38 +1012,36 @@ class ViewBox(GraphicsWidget):
if not item.isVisible():
continue
#print "=========", item
useX = True
useY = True
if hasattr(item, 'dataBounds'):
if frac is None:
frac = (1.0, 1.0)
xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1])
#print " xr:", xr, " yr:", yr
if xr is None or xr == (None, None):
useX = False
xr = (0,0)
if yr is None or yr == (None, None):
useY = False
yr = (0,0)
bounds = self._itemBoundsCache.get(item, None)
if bounds is None:
if frac is None:
frac = (1.0, 1.0)
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)
if yr is None or yr == (None, None):
useY = False
yr = (0,0)
bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0])
#print " xr:", xr, " yr:", yr
#print " item real:", bounds
bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0])
bounds = self.mapFromItemToView(item, bounds).boundingRect()
self._itemBoundsCache[item] = (bounds, useX, useY)
else:
bounds, useX, useY = bounds
else:
if int(item.flags() & item.ItemHasNoContents) > 0:
continue
#print " empty"
else:
bounds = item.boundingRect()
#bounds = [[item.left(), item.top()], [item.right(), item.bottom()]]
#print " item:", bounds
#bounds = QtCore.QRectF(bounds[0][0], bounds[1][0], bounds[0][1]-bounds[0][0], bounds[1][1]-bounds[1][0])
bounds = self.mapFromItemToView(item, bounds).boundingRect()
#print " ", bounds
bounds = self.mapFromItemToView(item, bounds).boundingRect()
prof.mark('1')
#print " useX:", useX, " useY:", useY
if not any([useX, useY]):
continue
@ -1073,11 +1054,6 @@ class ViewBox(GraphicsWidget):
else:
continue ## need to check for item rotations and decide how best to apply this boundary.
#print " useX:", useX, " useY:", useY
#print " range:", range
#print " bounds (r,l,t,b):", bounds.right(), bounds.left(), bounds.top(), bounds.bottom()
if useY:
if range[1] is not None:
range[1] = [min(bounds.top(), range[1][0]), max(bounds.bottom(), range[1][1])]
@ -1088,9 +1064,9 @@ class ViewBox(GraphicsWidget):
range[0] = [min(bounds.left(), range[0][0]), max(bounds.right(), range[0][1])]
else:
range[0] = [bounds.left(), bounds.right()]
prof.mark('2')
#print " range:", range
prof.finish()
return range
def childrenBoundingRect(self, *args, **kwds):
@ -1287,5 +1263,4 @@ class ViewBox(GraphicsWidget):
self.scene().removeItem(self.locateGroup)
self.locateGroup = None
from .ViewBoxMenu import ViewBoxMenu

View File

@ -143,7 +143,11 @@ class GraphicsView(QtGui.QGraphicsView):
else:
brush = fn.mkBrush(background)
self.setBackgroundBrush(brush)
def paintEvent(self, ev):
self.scene().prepareForPaint()
#print "GV: paint", ev.rect()
return QtGui.QGraphicsView.paintEvent(self, ev)
def close(self):
self.centralWidget = None