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:
parent
fa9660e381
commit
01b8968a0a
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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__':
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user