From 01b8968a0aa6c4f140f8f7e078d840813ac51134 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 9 Jan 2013 22:21:32 -0500 Subject: [PATCH] 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. --- pyqtgraph/GraphicsScene/GraphicsScene.py | 20 ++++- pyqtgraph/debug.py | 71 ++++++++++++---- pyqtgraph/graphicsItems/AxisItem.py | 2 +- pyqtgraph/graphicsItems/GraphicsItem.py | 98 ++++++++++++++++------ pyqtgraph/graphicsItems/PlotCurveItem.py | 80 +++++++++++------- pyqtgraph/graphicsItems/PlotDataItem.py | 7 +- pyqtgraph/graphicsItems/VTickGroup.py | 69 +-------------- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 81 +++++++----------- pyqtgraph/widgets/GraphicsView.py | 6 +- 9 files changed, 234 insertions(+), 200 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index a5f60059..d0a75d16 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -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 diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index ea9157aa..d5f86139 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -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): diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 82cbcfae..7be3e30f 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -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 diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 2018fb4c..2314709d 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -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 \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 8af13e19..d035a0e4 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -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) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 22aa3ad9..e46279d7 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -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') diff --git a/pyqtgraph/graphicsItems/VTickGroup.py b/pyqtgraph/graphicsItems/VTickGroup.py index 85ed596e..c6880f91 100644 --- a/pyqtgraph/graphicsItems/VTickGroup.py +++ b/pyqtgraph/graphicsItems/VTickGroup.py @@ -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__': diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 8da539fb..ae7298ba 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -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 diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 403aed9d..dd49ab7d 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -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