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 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. 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() _addressCache = weakref.WeakValueDictionary()
ExportDirectory = None ExportDirectory = None
@ -98,6 +100,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
self.clickEvents = [] self.clickEvents = []
self.dragButtons = [] self.dragButtons = []
self.prepItems = weakref.WeakKeyDictionary() ## set of items with prepareForPaintMethods
self.mouseGrabber = None self.mouseGrabber = None
self.dragItem = None self.dragItem = None
self.lastDrag = None self.lastDrag = None
@ -112,6 +115,17 @@ class GraphicsScene(QtGui.QGraphicsScene):
self.exportDialog = None 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): def setClickRadius(self, r):
""" """
@ -224,7 +238,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
else: else:
acceptable = int(ev.buttons()) == 0 ## if we are in mid-drag, do not allow items to accept the hover event. acceptable = int(ev.buttons()) == 0 ## if we are in mid-drag, do not allow items to accept the hover event.
event = HoverEvent(ev, acceptable) event = HoverEvent(ev, acceptable)
items = self.itemsNearEvent(event) items = self.itemsNearEvent(event, hoverable=True)
self.sigMouseHover.emit(items) self.sigMouseHover.emit(items)
prevItems = list(self.hoverItems.keys()) prevItems = list(self.hoverItems.keys())
@ -402,7 +416,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
#return item #return item
return self.translateGraphicsItem(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) 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. 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) ## remove items whose shape does not contain point (scene.items() apparently sucks at this)
items2 = [] items2 = []
for item in items: for item in items:
if hoverable and not hasattr(item, 'hoverEvent'):
continue
shape = item.shape() shape = item.shape()
if shape is None: if shape is None:
continue continue

View File

@ -356,8 +356,14 @@ class GarbageWatcher(object):
return self.objs[item] return self.objs[item]
class Profiler(object): class Profiler:
"""Simple profiler allowing measurement of multiple time intervals. """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: Example:
prof = Profiler('Function') prof = Profiler('Function')
@ -368,34 +374,65 @@ class Profiler(object):
prof.finish() prof.finish()
""" """
depth = 0 depth = 0
msgs = []
def __init__(self, msg="Profiler", disabled=False): def __init__(self, msg="Profiler", disabled=False, delayed=True, globalDelay=True):
self.depth = Profiler.depth
Profiler.depth += 1
self.disabled = disabled self.disabled = disabled
if disabled: if disabled:
return 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.t0 = ptime.time()
self.t1 = self.t0 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: if self.disabled:
return return
t1 = ptime.time()
print(" "+self.msg, msg, "%gms" % ((t1-self.t1)*1000))
self.t1 = t1
def finish(self): if msg is None:
if self.disabled: 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 return
t1 = ptime.time()
print(self.msg, '<<< Finished, total time:', "%gms" % ((t1-self.t0)*1000))
def __del__(self): if msg is not None:
Profiler.depth -= 1 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): def profile(code, name='profile_run', sort='cumulative', num=30):

View File

@ -369,7 +369,7 @@ class AxisItem(GraphicsWidget):
return [ return [
(intervals[minorIndex+2], 0), (intervals[minorIndex+2], 0),
(intervals[minorIndex+1], 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 ##### 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 from pyqtgraph.Point import Point
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
import weakref import weakref
from pyqtgraph.pgcollections import OrderedDict
import operator 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): class GraphicsItem(object):
""" """
**Bases:** :class:`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. 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): def __init__(self, register=True):
if not hasattr(self, '_qtBaseClass'): if not hasattr(self, '_qtBaseClass'):
for b in self.__class__.__bases__: for b in self.__class__.__bases__:
@ -25,6 +49,7 @@ class GraphicsItem(object):
if not hasattr(self, '_qtBaseClass'): if not hasattr(self, '_qtBaseClass'):
raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self)) raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self))
self._pixelVectorCache = [None, None]
self._viewWidget = None self._viewWidget = None
self._viewBox = None self._viewBox = None
self._connectedView = None self._connectedView = None
@ -155,7 +180,6 @@ class GraphicsItem(object):
def pixelVectors(self, direction=None): def pixelVectors(self, direction=None):
"""Return vectors in local coordinates representing the width and height of a view pixel. """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. If direction is specified, then return vectors parallel and orthogonal to it.
@ -164,12 +188,27 @@ class GraphicsItem(object):
or if pixel size is below floating-point precision limit. 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() dt = self.deviceTransform()
if dt is None: if dt is None:
return None, 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: if direction is None:
direction = Point(1, 0) direction = QtCore.QPointF(1, 0)
if direction.manhattanLength() == 0: if direction.manhattanLength() == 0:
raise Exception("Cannot compute pixel length for 0-length vector.") 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 r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5
directionr = direction * r directionr = direction * r
viewDir = Point(dt.map(directionr) - dt.map(Point(0,0))) ## map direction vector onto device
if viewDir.manhattanLength() == 0: #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 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: try:
normView = viewDir.norm() ## direction of one pixel orthogonal to line normView = viewDir.unitVector()
normOrtho = orthoDir.norm() #normView = viewDir.norm() ## direction of one pixel orthogonal to line
normOrtho = normView.normalVector()
#normOrtho = orthoDir.norm()
except: except:
raise Exception("Invalid direction %s" %directionr) raise Exception("Invalid direction %s" %directionr)
## map back to item
dti = fn.invertQTransform(dt) 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): def pixelLength(self, direction, ortho=False):
"""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)
@ -221,7 +265,6 @@ class GraphicsItem(object):
return normV.length() return normV.length()
def pixelSize(self): def pixelSize(self):
## deprecated ## deprecated
v = self.pixelVectors() v = self.pixelVectors()
@ -235,7 +278,7 @@ class GraphicsItem(object):
if vt is None: if vt is None:
return 0 return 0
vt = fn.invertQTransform(vt) 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): def pixelHeight(self):
## deprecated ## deprecated
@ -243,7 +286,8 @@ class GraphicsItem(object):
if vt is None: if vt is None:
return 0 return 0
vt = fn.invertQTransform(vt) 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): def mapToDevice(self, obj):
@ -358,9 +402,10 @@ class GraphicsItem(object):
tr = self.itemTransform(relativeItem) tr = self.itemTransform(relativeItem)
if isinstance(tr, tuple): ## difference between pyside and pyqt if isinstance(tr, tuple): ## difference between pyside and pyqt
tr = tr[0] tr = tr[0]
vec = tr.map(Point(1,0)) - tr.map(Point(0,0)) #vec = tr.map(Point(1,0)) - tr.map(Point(0,0))
return Point(vec).angle(Point(1,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): #def itemChange(self, change, value):
#ret = self._qtBaseClass.itemChange(self, change, value) #ret = self._qtBaseClass.itemChange(self, change, value)
@ -500,3 +545,6 @@ class GraphicsItem(object):
else: else:
self._exportOpts = False 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.clear()
self.path = None self.path = None
self.fillPath = None self.fillPath = None
self._boundsCache = [None, None]
## this is disastrous for performance. ## this is disastrous for performance.
#self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
@ -85,6 +85,12 @@ class PlotCurveItem(GraphicsObject):
return self.xData, self.yData return self.xData, self.yData
def dataBounds(self, ax, frac=1.0, orthoRange=None): 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() (x, y) = self.getData()
if x is None or len(x) == 0: if x is None or len(x) == 0:
return (0, 0) return (0, 0)
@ -103,15 +109,22 @@ class PlotCurveItem(GraphicsObject):
if frac >= 1.0: if frac >= 1.0:
return (d.min(), d.max()) b = (d.min(), d.max())
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))
else: 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): def setPen(self, *args, **kargs):
"""Set the pen used to draw the curve.""" """Set the pen used to draw the curve."""
self.opts['pen'] = fn.mkPen(*args, **kargs) self.opts['pen'] = fn.mkPen(*args, **kargs)
self.invalidateBounds()
self.update() self.update()
def setShadowPen(self, *args, **kargs): def setShadowPen(self, *args, **kargs):
@ -120,17 +133,20 @@ class PlotCurveItem(GraphicsObject):
pen to be visible. pen to be visible.
""" """
self.opts['shadowPen'] = fn.mkPen(*args, **kargs) self.opts['shadowPen'] = fn.mkPen(*args, **kargs)
self.invalidateBounds()
self.update() self.update()
def setBrush(self, *args, **kargs): def setBrush(self, *args, **kargs):
"""Set the brush used when filling the area under the curve""" """Set the brush used when filling the area under the curve"""
self.opts['brush'] = fn.mkBrush(*args, **kargs) self.opts['brush'] = fn.mkBrush(*args, **kargs)
self.invalidateBounds()
self.update() self.update()
def setFillLevel(self, level): def setFillLevel(self, level):
"""Set the level filled to when filling under the curve""" """Set the level filled to when filling under the curve"""
self.opts['fillLevel'] = level self.opts['fillLevel'] = level
self.fillPath = None self.fillPath = None
self.invalidateBounds()
self.update() self.update()
#def setColor(self, color): #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 #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 ## Test this bug with test_PlotWidget and zoom in on the animated plot
self.invalidateBounds()
self.prepareGeometryChange() self.prepareGeometryChange()
self.informViewBoundsChanged()
self.yData = kargs['y'].view(np.ndarray) self.yData = kargs['y'].view(np.ndarray)
self.xData = kargs['x'].view(np.ndarray) self.xData = kargs['x'].view(np.ndarray)
@ -349,6 +367,7 @@ class PlotCurveItem(GraphicsObject):
return self.path return self.path
def boundingRect(self): def boundingRect(self):
if self._boundingRect is None:
(x, y) = self.getData() (x, y) = self.getData()
if x is None or y is None or len(x) == 0 or len(y) == 0: if x is None or y is None or len(x) == 0 or len(y) == 0:
return QtCore.QRectF() return QtCore.QRectF()
@ -378,7 +397,8 @@ class PlotCurveItem(GraphicsObject):
ymin -= abs(pixels[1].y()) * lineWidth ymin -= abs(pixels[1].y()) * lineWidth
ymax += 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): def paint(self, p, opt, widget):
prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True)

View File

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

View File

@ -34,8 +34,7 @@ class VTickGroup(UIGraphicsItem):
if xvals is None: if xvals is None:
xvals = [] xvals = []
#bounds = QtCore.QRectF(0, yrange[0], 1, yrange[1]-yrange[0]) UIGraphicsItem.__init__(self)
UIGraphicsItem.__init__(self)#, bounds=bounds)
if pen is None: if pen is None:
pen = (200, 200, 200) pen = (200, 200, 200)
@ -44,15 +43,10 @@ class VTickGroup(UIGraphicsItem):
self.ticks = [] self.ticks = []
self.xvals = [] self.xvals = []
#if view is None:
#self.view = None
#else:
#self.view = weakref.ref(view)
self.yrange = [0,1] self.yrange = [0,1]
self.setPen(pen) self.setPen(pen)
self.setYRange(yrange) self.setYRange(yrange)
self.setXVals(xvals) self.setXVals(xvals)
#self.valid = False
def setPen(self, *args, **kwargs): def setPen(self, *args, **kwargs):
"""Set the pen to use for drawing ticks. Can be specified as any arguments valid """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 """Set the y range [low, high] that the ticks are drawn on. 0 is the bottom of
the view, 1 is the top.""" the view, 1 is the top."""
self.yrange = vals 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.rebuildTicks()
#self.valid = False
def dataBounds(self, *args, **kargs): def dataBounds(self, *args, **kargs):
return None ## item should never affect view autoscaling 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): 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 return self.yrange
def rebuildTicks(self): def rebuildTicks(self):
self.path = QtGui.QPainterPath() self.path = QtGui.QPainterPath()
yrange = self.yRange() yrange = self.yRange()
#print "rebuild ticks:", yrange
for x in self.xvals: for x in self.xvals:
#path.moveTo(x, yrange[0])
#path.lineTo(x, yrange[1])
self.path.moveTo(x, 0.) self.path.moveTo(x, 0.)
self.path.lineTo(x, 1.) self.path.lineTo(x, 1.)
#self.setPath(self.path)
#self.valid = True
#self.rescale()
#print " done..", self.boundingRect()
def paint(self, p, *args): def paint(self, p, *args):
UIGraphicsItem.paint(self, p, *args) UIGraphicsItem.paint(self, p, *args)
@ -161,7 +95,6 @@ class VTickGroup(UIGraphicsItem):
p.scale(1.0, br.height()) p.scale(1.0, br.height())
p.setPen(self.pen) p.setPen(self.pen)
p.drawPath(self.path) p.drawPath(self.path)
#QtGui.QGraphicsPathItem.paint(self, *args)
if __name__ == '__main__': if __name__ == '__main__':

View File

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

View File

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