Plotting performance improvements:

- AxisItem shows fewer tick levels in some cases.
  - 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)

Improved performance for remote plotting:
  - reduced cost of transferring arrays between processes (pickle is too slow)
  - avoid unnecessary synchronous calls

Added RemoteSpeedTest example
This commit is contained in:
Luke Campagnola 2013-01-11 20:30:08 -05:00
commit 3a27997014
20 changed files with 530 additions and 290 deletions

View File

@ -0,0 +1,63 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
## Add path to library (just for examples; you do not need this)
import initExample
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
from pyqtgraph.ptime import time
#QtGui.QApplication.setGraphicsSystem('raster')
app = QtGui.QApplication([])
#mw = QtGui.QMainWindow()
#mw.resize(800,800)
p = pg.plot()
#p.setRange(QtCore.QRectF(0, -10, 5000, 20))
p.setLabel('bottom', 'Index', units='B')
nPlots = 10
#curves = [p.plot(pen=(i,nPlots*1.3)) for i in range(nPlots)]
curves = [pg.PlotCurveItem(pen=(i,nPlots*1.3)) for i in range(nPlots)]
for c in curves:
p.addItem(c)
rgn = pg.LinearRegionItem([1,100])
p.addItem(rgn)
data = np.random.normal(size=(53,5000/nPlots))
ptr = 0
lastTime = time()
fps = None
count = 0
def update():
global curve, data, ptr, p, lastTime, fps, nPlots, count
count += 1
#print "---------", count
for i in range(nPlots):
curves[i].setData(i+data[(ptr+i)%data.shape[0]])
#print " setData done."
ptr += nPlots
now = time()
dt = now - lastTime
lastTime = now
if fps is None:
fps = 1.0/dt
else:
s = np.clip(dt*3., 0, 1)
fps = fps * (1-s) + (1.0/dt) * s
p.setTitle('%0.2f fps' % fps)
#app.processEvents() ## force complete redraw for every plot
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)
## Start Qt event loop unless running in interactive mode.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -45,6 +45,9 @@ p5 = win.addPlot(title="Scatter plot, axis labels, log scale")
x = np.random.normal(size=1000) * 1e-5 x = np.random.normal(size=1000) * 1e-5
y = x*1000 + 0.005 * np.random.normal(size=1000) y = x*1000 + 0.005 * np.random.normal(size=1000)
y -= y.min()-1.0 y -= y.min()-1.0
mask = x > 1e-15
x = x[mask]
y = y[mask]
p5.plot(x, y, pen=None, symbol='t', symbolPen=None, symbolSize=10, symbolBrush=(100, 100, 255, 50)) p5.plot(x, y, pen=None, symbol='t', symbolPen=None, symbolSize=10, symbolBrush=(100, 100, 255, 50))
p5.setLabel('left', "Y Axis", units='A') p5.setLabel('left', "Y Axis", units='A')
p5.setLabel('bottom', "Y Axis", units='s') p5.setLabel('bottom', "Y Axis", units='s')

View File

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
"""
This example demonstrates the use of RemoteGraphicsView to improve performance in
applications with heavy load. It works by starting a second process to handle
all graphics rendering, thus freeing up the main process to do its work.
In this example, the update() function is very expensive and is called frequently.
After update() generates a new set of data, it can either plot directly to a local
plot (bottom) or remotely via a RemoteGraphicsView (top), allowing speed comparison
between the two cases. IF you have a multi-core CPU, it should be obvious that the
remote case is much faster.
"""
import initExample ## Add path to library (just for examples; you do not need this)
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import pyqtgraph.widgets.RemoteGraphicsView
import numpy as np
app = pg.mkQApp()
view = pg.widgets.RemoteGraphicsView.RemoteGraphicsView()
pg.setConfigOptions(antialias=True) ## this will be expensive for the local plot
view.pg.setConfigOptions(antialias=True) ## prettier plots at no cost to the main process!
label = QtGui.QLabel()
rcheck = QtGui.QCheckBox('plot remote')
rcheck.setChecked(True)
lcheck = QtGui.QCheckBox('plot local')
lplt = pg.PlotWidget()
layout = pg.LayoutWidget()
layout.addWidget(rcheck)
layout.addWidget(lcheck)
layout.addWidget(label)
layout.addWidget(view, row=1, col=0, colspan=3)
layout.addWidget(lplt, row=2, col=0, colspan=3)
layout.resize(800,800)
layout.show()
## Create a PlotItem in the remote process that will be displayed locally
rplt = view.pg.PlotItem()
rplt._setProxyOptions(deferGetattr=True) ## speeds up access to rplt.plot
view.setCentralItem(rplt)
lastUpdate = pg.ptime.time()
avgFps = 0.0
def update():
global check, label, plt, lastUpdate, avgFps, rpltfunc
data = np.random.normal(size=(10000,50)).sum(axis=1)
data += 5 * np.sin(np.linspace(0, 10, data.shape[0]))
if rcheck.isChecked():
rplt.plot(data, clear=True, _callSync='off') ## We do not expect a return value.
## By turning off callSync, we tell
## the proxy that it does not need to
## wait for a reply from the remote
## process.
if lcheck.isChecked():
lplt.plot(data, clear=True)
now = pg.ptime.time()
fps = 1.0 / (now - lastUpdate)
lastUpdate = now
avgFps = avgFps * 0.8 + fps * 0.2
label.setText("Generating %0.2f fps" % avgFps)
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys, os
## Add path to library (just for examples; you do not need this) ## Add path to library (just for examples; you do not need this)
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) import initExample
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg import pyqtgraph as pg

View File

@ -22,6 +22,7 @@ examples = OrderedDict([
('Dock widgets', 'dockarea.py'), ('Dock widgets', 'dockarea.py'),
('Console', 'ConsoleWidget.py'), ('Console', 'ConsoleWidget.py'),
('Histograms', 'histogram.py'), ('Histograms', 'histogram.py'),
('Remote Plotting', 'RemoteSpeedTest.py'),
('GraphicsItems', OrderedDict([ ('GraphicsItems', OrderedDict([
('Scatter Plot', 'ScatterPlot.py'), ('Scatter Plot', 'ScatterPlot.py'),
#('PlotItem', 'PlotItem.py'), #('PlotItem', 'PlotItem.py'),
@ -182,6 +183,7 @@ def testFile(name, f, exe, lib, graphicsSystem=None):
code = """ code = """
try: try:
%s %s
import initExample
import pyqtgraph as pg import pyqtgraph as pg
%s %s
import %s import %s

View File

@ -16,7 +16,6 @@ if not hasattr(sys, 'frozen'):
sys.path.remove(p) sys.path.remove(p)
sys.path.insert(0, p) sys.path.insert(0, p)
## should force example to use PySide instead of PyQt ## should force example to use PySide instead of PyQt
if 'pyside' in sys.argv: if 'pyside' in sys.argv:
from PySide import QtGui from PySide import QtGui

View File

@ -43,6 +43,9 @@ from pyqtgraph.Qt import QtCore, QtGui
app = pg.QtGui.QApplication([]) app = pg.QtGui.QApplication([])
print "\n=================\nStart QtProcess" print "\n=================\nStart QtProcess"
import sys
if (sys.flags.interactive != 1):
print " (not interactive; remote process will exit immediately.)"
proc = mp.QtProcess() proc = mp.QtProcess()
d1 = proc.transfer(np.random.normal(size=1000)) d1 = proc.transfer(np.random.normal(size=1000))
d2 = proc.transfer(np.random.normal(size=1000)) d2 = proc.transfer(np.random.normal(size=1000))

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

@ -350,9 +350,7 @@ class AxisItem(GraphicsWidget):
## decide optimal minor tick spacing in pixels (this is just aesthetics) ## decide optimal minor tick spacing in pixels (this is just aesthetics)
pixelSpacing = np.log(size+10) * 5 pixelSpacing = np.log(size+10) * 5
optimalTickCount = size / pixelSpacing optimalTickCount = max(2., size / pixelSpacing)
if optimalTickCount < 1:
optimalTickCount = 1
## optimal minor tick spacing ## optimal minor tick spacing
optimalSpacing = dif / optimalTickCount optimalSpacing = dif / optimalTickCount
@ -366,12 +364,21 @@ class AxisItem(GraphicsWidget):
while intervals[minorIndex+1] <= optimalSpacing: while intervals[minorIndex+1] <= optimalSpacing:
minorIndex += 1 minorIndex += 1
return [ levels = [
(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
] ]
## decide whether to include the last level of ticks
minSpacing = min(size / 20., 30.)
maxTickCount = size / minSpacing
if dif / intervals[minorIndex] <= maxTickCount:
levels.append((intervals[minorIndex], 0))
return levels
##### 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
### Determine major/minor tick spacings which flank the optimal spacing. ### Determine major/minor tick spacings which flank the optimal spacing.
#intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit #intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit
@ -587,7 +594,7 @@ class AxisItem(GraphicsWidget):
ticks = tickLevels[i][1] ticks = tickLevels[i][1]
## length of tick ## length of tick
tickLength = self.tickLength / ((i*1.0)+1.0) tickLength = self.tickLength / ((i*0.5)+1.0)
lineAlpha = 255 / (i+1) lineAlpha = 255 / (i+1)
if self.grid is not False: if self.grid is not False:

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.
@ -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) 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. 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)
@ -220,7 +264,6 @@ class GraphicsItem(object):
return orthoV.length() return orthoV.length()
return normV.length() return normV.length()
def pixelSize(self): def pixelSize(self):
## deprecated ## deprecated
@ -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):
@ -357,10 +401,11 @@ 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,52 +133,22 @@ 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):
#self.pen.setColor(color)
#self.update()
#def setAlpha(self, alpha, auto):
#self.opts['alphaHint'] = alpha
#self.opts['alphaMode'] = auto
#self.update()
#def setSpectrumMode(self, mode):
#self.opts['spectrumMode'] = mode
#self.xDisp = self.yDisp = None
#self.path = None
#self.update()
#def setLogMode(self, mode):
#self.opts['logMode'] = mode
#self.xDisp = self.yDisp = None
#self.path = None
#self.update()
#def setPointMode(self, mode):
#self.opts['pointMode'] = mode
#self.update()
#def setDownsampling(self, ds):
#if self.opts['downsample'] != ds:
#self.opts['downsample'] = ds
#self.xDisp = self.yDisp = None
#self.path = None
#self.update()
def setData(self, *args, **kargs): def setData(self, *args, **kargs):
""" """
============== ======================================================== ============== ========================================================
@ -221,7 +204,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,36 +334,38 @@ class PlotCurveItem(GraphicsObject):
return self.path return self.path
def boundingRect(self): def boundingRect(self):
(x, y) = self.getData() if self._boundingRect is None:
if x is None or y is None or len(x) == 0 or len(y) == 0: (x, y) = self.getData()
return QtCore.QRectF() 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: if self.opts['fillLevel'] is not None:
lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1) ymin = min(ymin, self.opts['fillLevel'])
else: ymax = max(ymax, self.opts['fillLevel'])
lineWidth = (self.opts['pen'].width()+1)
xmin -= pixels[0].x() * lineWidth
xmax += pixels[0].x() * lineWidth
ymin -= abs(pixels[1].y()) * lineWidth
ymax += abs(pixels[1].y()) * lineWidth
self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
pixels = self.pixelVectors() return self._boundingRect
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)
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)
@ -463,25 +450,6 @@ class PlotCurveItem(GraphicsObject):
self.path = None self.path = None
#del self.xData, self.yData, self.xDisp, self.yDisp, self.path #del self.xData, self.yData, self.xDisp, self.yDisp, self.path
#def mousePressEvent(self, ev):
##GraphicsObject.mousePressEvent(self, ev)
#if not self.clickable:
#ev.ignore()
#if ev.button() != QtCore.Qt.LeftButton:
#ev.ignore()
#self.mousePressPos = ev.pos()
#self.mouseMoved = False
#def mouseMoveEvent(self, ev):
##GraphicsObject.mouseMoveEvent(self, ev)
#self.mouseMoved = True
##print "move"
#def mouseReleaseEvent(self, ev):
##GraphicsObject.mouseReleaseEvent(self, ev)
#if not self.mouseMoved:
#self.sigClicked.emit(self)
def mouseClickEvent(self, ev): def mouseClickEvent(self, ev):
if not self.clickable or ev.button() != QtCore.Qt.LeftButton: if not self.clickable or ev.button() != QtCore.Qt.LeftButton:
return return

View File

@ -164,6 +164,7 @@ class PlotDataItem(GraphicsObject):
self.opts['fftMode'] = mode self.opts['fftMode'] = mode
self.xDisp = self.yDisp = None self.xDisp = self.yDisp = None
self.updateItems() self.updateItems()
self.informViewBoundsChanged()
def setLogMode(self, xMode, yMode): def setLogMode(self, xMode, yMode):
if self.opts['logMode'] == [xMode, yMode]: if self.opts['logMode'] == [xMode, yMode]:
@ -171,6 +172,7 @@ class PlotDataItem(GraphicsObject):
self.opts['logMode'] = [xMode, yMode] self.opts['logMode'] = [xMode, yMode]
self.xDisp = self.yDisp = None self.xDisp = self.yDisp = None
self.updateItems() self.updateItems()
self.informViewBoundsChanged()
def setPointMode(self, mode): def setPointMode(self, mode):
if self.opts['pointMode'] == mode: if self.opts['pointMode'] == mode:
@ -369,9 +371,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)
@ -548,7 +550,7 @@ class ViewBox(GraphicsWidget):
fractionVisible[i] = 1.0 fractionVisible[i] = 1.0
childRange = None childRange = None
order = [0,1] order = [0,1]
if self.state['autoVisibleOnly'][0] is True: if self.state['autoVisibleOnly'][0] is True:
order = [1,0] order = [1,0]
@ -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,38 +1012,36 @@ 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'):
if frac is None: bounds = self._itemBoundsCache.get(item, None)
frac = (1.0, 1.0) if bounds is None:
xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) if frac is None:
yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) frac = (1.0, 1.0)
#print " xr:", xr, " yr:", yr xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
if xr is None or xr == (None, None): yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1])
useX = False if xr is None or xr == (None, None):
xr = (0,0) useX = False
if yr is None or yr == (None, None): xr = (0,0)
useY = False if yr is None or yr == (None, None):
yr = (0,0) useY = False
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()]] bounds = self.mapFromItemToView(item, bounds).boundingRect()
#print " item:", bounds
#bounds = QtCore.QRectF(bounds[0][0], bounds[1][0], bounds[0][1]-bounds[0][0], bounds[1][1]-bounds[1][0]) prof.mark('1')
bounds = self.mapFromItemToView(item, bounds).boundingRect()
#print " ", bounds
#print " useX:", useX, " useY:", useY
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

@ -1,6 +1,6 @@
import os, sys, time, multiprocessing, re import os, sys, time, multiprocessing, re
from processes import ForkedProcess from processes import ForkedProcess
from remoteproxy import ExitError from remoteproxy import ClosedError
class CanceledError(Exception): class CanceledError(Exception):
"""Raised when the progress dialog is canceled during a processing operation.""" """Raised when the progress dialog is canceled during a processing operation."""
@ -152,7 +152,7 @@ class Parallelize(object):
n = ch.processRequests() n = ch.processRequests()
if n > 0: if n > 0:
waitingChildren += 1 waitingChildren += 1
except ExitError: except ClosedError:
#print ch.childPid, 'process finished' #print ch.childPid, 'process finished'
rem.append(ch) rem.append(ch)
if self.showProgress: if self.showProgress:

View File

@ -1,9 +1,9 @@
from remoteproxy import RemoteEventHandler, ExitError, NoResultError, LocalObjectProxy, ObjectProxy from remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy
import subprocess, atexit, os, sys, time, random, socket, signal import subprocess, atexit, os, sys, time, random, socket, signal
import cPickle as pickle import cPickle as pickle
import multiprocessing.connection import multiprocessing.connection
__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ExitError', 'NoResultError'] __all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ClosedError', 'NoResultError']
class Process(RemoteEventHandler): class Process(RemoteEventHandler):
""" """
@ -100,7 +100,7 @@ def startEventLoop(name, port, authkey):
try: try:
HANDLER.processRequests() # exception raised when the loop should exit HANDLER.processRequests() # exception raised when the loop should exit
time.sleep(0.01) time.sleep(0.01)
except ExitError: except ClosedError:
break break
@ -225,7 +225,7 @@ class ForkedProcess(RemoteEventHandler):
try: try:
self.processRequests() # exception raised when the loop should exit self.processRequests() # exception raised when the loop should exit
time.sleep(0.01) time.sleep(0.01)
except ExitError: except ClosedError:
break break
except: except:
print "Error occurred in forked event loop:" print "Error occurred in forked event loop:"
@ -267,11 +267,11 @@ class RemoteQtEventHandler(RemoteEventHandler):
def processRequests(self): def processRequests(self):
try: try:
RemoteEventHandler.processRequests(self) RemoteEventHandler.processRequests(self)
except ExitError: except ClosedError:
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore
QtGui.QApplication.instance().quit() QtGui.QApplication.instance().quit()
self.timer.stop() self.timer.stop()
#raise #raise SystemExit
class QtProcess(Process): class QtProcess(Process):
""" """
@ -315,7 +315,7 @@ class QtProcess(Process):
def processRequests(self): def processRequests(self):
try: try:
Process.processRequests(self) Process.processRequests(self)
except ExitError: except ClosedError:
self.timer.stop() self.timer.stop()
def startQtEventLoop(name, port, authkey): def startQtEventLoop(name, port, authkey):

View File

@ -1,10 +1,15 @@
import os, __builtin__, time, sys, traceback, weakref import os, __builtin__, time, sys, traceback, weakref
import cPickle as pickle import cPickle as pickle
import numpy as np
class ExitError(Exception): class ClosedError(Exception):
"""Raised when an event handler receives a request to close the connection
or discovers that the connection has been closed."""
pass pass
class NoResultError(Exception): class NoResultError(Exception):
"""Raised when a request for the return value of a remote call fails
because the call has not yet returned."""
pass pass
@ -82,14 +87,14 @@ class RemoteEventHandler(object):
Returns the number of events processed. Returns the number of events processed.
""" """
if self.exited: if self.exited:
raise ExitError() raise ClosedError()
numProcessed = 0 numProcessed = 0
while self.conn.poll(): while self.conn.poll():
try: try:
self.handleRequest() self.handleRequest()
numProcessed += 1 numProcessed += 1
except ExitError: except ClosedError:
self.exited = True self.exited = True
raise raise
except IOError as err: except IOError as err:
@ -108,14 +113,20 @@ class RemoteEventHandler(object):
Blocks until a request is available.""" Blocks until a request is available."""
result = None result = None
try: try:
cmd, reqId, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails cmd, reqId, nByteMsgs, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails
except EOFError: except (EOFError, IOError):
## remote process has shut down; end event loop ## remote process has shut down; end event loop
raise ExitError() raise ClosedError()
except IOError:
raise ExitError()
#print os.getpid(), "received request:", cmd, reqId #print os.getpid(), "received request:", cmd, reqId
## read byte messages following the main request
byteData = []
for i in range(nByteMsgs):
try:
byteData.append(self.conn.recv_bytes())
except (EOFError, IOError):
raise ClosedError()
try: try:
if cmd == 'result' or cmd == 'error': if cmd == 'result' or cmd == 'error':
@ -137,17 +148,36 @@ class RemoteEventHandler(object):
obj = opts['obj'] obj = opts['obj']
fnargs = opts['args'] fnargs = opts['args']
fnkwds = opts['kwds'] fnkwds = opts['kwds']
## If arrays were sent as byte messages, they must be re-inserted into the
## arguments
if len(byteData) > 0:
for i,arg in enumerate(fnargs):
if isinstance(arg, tuple) and len(arg) > 0 and arg[0] == '__byte_message__':
ind = arg[1]
dtype, shape = arg[2]
fnargs[i] = np.fromstring(byteData[ind], dtype=dtype).reshape(shape)
for k,arg in fnkwds.items():
if isinstance(arg, tuple) and len(arg) > 0 and arg[0] == '__byte_message__':
ind = arg[1]
dtype, shape = arg[2]
fnkwds[k] = np.fromstring(byteData[ind], dtype=dtype).reshape(shape)
if len(fnkwds) == 0: ## need to do this because some functions do not allow keyword arguments. if len(fnkwds) == 0: ## need to do this because some functions do not allow keyword arguments.
#print obj, fnargs
result = obj(*fnargs) result = obj(*fnargs)
else: else:
result = obj(*fnargs, **fnkwds) result = obj(*fnargs, **fnkwds)
elif cmd == 'getObjValue': elif cmd == 'getObjValue':
result = opts['obj'] ## has already been unpickled into its local value result = opts['obj'] ## has already been unpickled into its local value
returnType = 'value' returnType = 'value'
elif cmd == 'transfer': elif cmd == 'transfer':
result = opts['obj'] result = opts['obj']
returnType = 'proxy' returnType = 'proxy'
elif cmd == 'transferArray':
## read array data from next message:
result = np.fromstring(byteData[0], dtype=opts['dtype']).reshape(opts['shape'])
returnType = 'proxy'
elif cmd == 'import': elif cmd == 'import':
name = opts['module'] name = opts['module']
fromlist = opts.get('fromlist', []) fromlist = opts.get('fromlist', [])
@ -201,7 +231,7 @@ class RemoteEventHandler(object):
## (more importantly, do not call any code that would ## (more importantly, do not call any code that would
## normally be invoked at exit) ## normally be invoked at exit)
else: else:
raise ExitError() raise ClosedError()
@ -216,7 +246,7 @@ class RemoteEventHandler(object):
except: except:
self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=None, excString=excStr)) self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=None, excString=excStr))
def send(self, request, opts=None, reqId=None, callSync='sync', timeout=10, returnType=None, **kwds): def send(self, request, opts=None, reqId=None, callSync='sync', timeout=10, returnType=None, byteData=None, **kwds):
"""Send a request or return packet to the remote process. """Send a request or return packet to the remote process.
Generally it is not necessary to call this method directly; it is for internal use. Generally it is not necessary to call this method directly; it is for internal use.
(The docstring has information that is nevertheless useful to the programmer (The docstring has information that is nevertheless useful to the programmer
@ -235,6 +265,9 @@ class RemoteEventHandler(object):
opts Extra arguments sent to the remote process that determine the way opts Extra arguments sent to the remote process that determine the way
the request will be handled (see below) the request will be handled (see below)
returnType 'proxy', 'value', or 'auto' returnType 'proxy', 'value', or 'auto'
byteData If specified, this is a list of objects to be sent as byte messages
to the remote process.
This is used to send large arrays without the cost of pickling.
========== ==================================================================== ========== ====================================================================
Description of request strings and options allowed for each: Description of request strings and options allowed for each:
@ -312,7 +345,9 @@ class RemoteEventHandler(object):
if returnType is not None: if returnType is not None:
opts['returnType'] = returnType opts['returnType'] = returnType
#print "send", opts
#print os.getpid(), "send request:", request, reqId, opts
## double-pickle args to ensure that at least status and request ID get through ## double-pickle args to ensure that at least status and request ID get through
try: try:
optStr = pickle.dumps(opts) optStr = pickle.dumps(opts)
@ -322,9 +357,19 @@ class RemoteEventHandler(object):
print "=======================================" print "======================================="
raise raise
request = (request, reqId, optStr) nByteMsgs = 0
if byteData is not None:
nByteMsgs = len(byteData)
## Send primary request
request = (request, reqId, nByteMsgs, optStr)
self.conn.send(request) self.conn.send(request)
## follow up by sending byte messages
if byteData is not None:
for obj in byteData: ## Remote process _must_ be prepared to read the same number of byte messages!
self.conn.send_bytes(obj)
if callSync == 'off': if callSync == 'off':
return return
@ -345,10 +390,10 @@ class RemoteEventHandler(object):
## raises NoResultError if the result is not available yet ## raises NoResultError if the result is not available yet
#print self.results.keys(), os.getpid() #print self.results.keys(), os.getpid()
if reqId not in self.results: if reqId not in self.results:
#self.readPipe()
try: try:
self.processRequests() self.processRequests()
except ExitError: except ClosedError: ## even if remote connection has closed, we may have
## received new data during this call to processRequests()
pass pass
if reqId not in self.results: if reqId not in self.results:
raise NoResultError() raise NoResultError()
@ -393,17 +438,33 @@ class RemoteEventHandler(object):
def callObj(self, obj, args, kwds, **opts): def callObj(self, obj, args, kwds, **opts):
opts = opts.copy() opts = opts.copy()
args = list(args)
## Decide whether to send arguments by value or by proxy
noProxyTypes = opts.pop('noProxyTypes', None) noProxyTypes = opts.pop('noProxyTypes', None)
if noProxyTypes is None: if noProxyTypes is None:
noProxyTypes = self.proxyOptions['noProxyTypes'] noProxyTypes = self.proxyOptions['noProxyTypes']
autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy'])
autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy'])
if autoProxy is True: if autoProxy is True:
args = tuple([self.autoProxy(v, noProxyTypes) for v in args]) args = [self.autoProxy(v, noProxyTypes) for v in args]
for k, v in kwds.iteritems(): for k, v in kwds.iteritems():
opts[k] = self.autoProxy(v, noProxyTypes) opts[k] = self.autoProxy(v, noProxyTypes)
return self.send(request='callObj', opts=dict(obj=obj, args=args, kwds=kwds), **opts) byteMsgs = []
## If there are arrays in the arguments, send those as byte messages.
## We do this because pickling arrays is too expensive.
for i,arg in enumerate(args):
if arg.__class__ == np.ndarray:
args[i] = ("__byte_message__", len(byteMsgs), (arg.dtype, arg.shape))
byteMsgs.append(arg)
for k,v in kwds.items():
if v.__class__ == np.ndarray:
kwds[k] = ("__byte_message__", len(byteMsgs), (v.dtype, v.shape))
byteMsgs.append(v)
return self.send(request='callObj', opts=dict(obj=obj, args=args, kwds=kwds), byteData=byteMsgs, **opts)
def registerProxy(self, proxy): def registerProxy(self, proxy):
ref = weakref.ref(proxy, self.deleteProxy) ref = weakref.ref(proxy, self.deleteProxy)
@ -421,7 +482,11 @@ class RemoteEventHandler(object):
Transfer an object by value to the remote host (the object must be picklable) Transfer an object by value to the remote host (the object must be picklable)
and return a proxy for the new remote object. and return a proxy for the new remote object.
""" """
return self.send(request='transfer', opts=dict(obj=obj), **kwds) if obj.__class__ is np.ndarray:
opts = {'dtype': obj.dtype, 'shape': obj.shape}
return self.send(request='transferArray', opts=opts, byteData=[obj], **kwds)
else:
return self.send(request='transfer', opts=dict(obj=obj), **kwds)
def autoProxy(self, obj, noProxyTypes): def autoProxy(self, obj, noProxyTypes):
## Return object wrapped in LocalObjectProxy _unless_ its type is in noProxyTypes. ## Return object wrapped in LocalObjectProxy _unless_ its type is in noProxyTypes.
@ -453,6 +518,8 @@ class Request(object):
If block is True, wait until the result has arrived or *timeout* seconds passes. If block is True, wait until the result has arrived or *timeout* seconds passes.
If the timeout is reached, raise NoResultError. (use timeout=None to disable) If the timeout is reached, raise NoResultError. (use timeout=None to disable)
If block is False, raise NoResultError immediately if the result has not arrived yet. If block is False, raise NoResultError immediately if the result has not arrived yet.
If the process's connection has closed before the result arrives, raise ClosedError.
""" """
if self.gotResult: if self.gotResult:
@ -464,6 +531,8 @@ class Request(object):
if block: if block:
start = time.time() start = time.time()
while not self.hasResult(): while not self.hasResult():
if self.proc.exited:
raise ClosedError()
time.sleep(0.005) time.sleep(0.005)
if timeout >= 0 and time.time() - start > timeout: if timeout >= 0 and time.time() - start > timeout:
print "Request timed out:", self.description print "Request timed out:", self.description

View File

@ -143,7 +143,11 @@ class GraphicsView(QtGui.QGraphicsView):
else: else:
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

View File

@ -18,6 +18,8 @@ class RemoteGraphicsView(QtGui.QWidget):
def __init__(self, parent=None, *args, **kwds): def __init__(self, parent=None, *args, **kwds):
self._img = None self._img = None
self._imgReq = None self._imgReq = None
self._sizeHint = (640,480) ## no clue why this is needed, but it seems to be the default sizeHint for GraphicsView.
## without it, the widget will not compete for space against another GraphicsView.
QtGui.QWidget.__init__(self) QtGui.QWidget.__init__(self)
self._proc = mp.QtProcess() self._proc = mp.QtProcess()
self.pg = self._proc._import('pyqtgraph') self.pg = self._proc._import('pyqtgraph')
@ -26,12 +28,18 @@ class RemoteGraphicsView(QtGui.QWidget):
self._view = rpgRemote.Renderer(*args, **kwds) self._view = rpgRemote.Renderer(*args, **kwds)
self._view._setProxyOptions(deferGetattr=True) self._view._setProxyOptions(deferGetattr=True)
self.setFocusPolicy(self._view.focusPolicy()) self.setFocusPolicy(self._view.focusPolicy())
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
self.setMouseTracking(True)
shmFileName = self._view.shmFileName() shmFileName = self._view.shmFileName()
self.shmFile = open(shmFileName, 'r') self.shmFile = open(shmFileName, 'r')
self.shm = mmap.mmap(self.shmFile.fileno(), mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_READ) self.shm = mmap.mmap(self.shmFile.fileno(), mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_READ)
self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) #, callSync='off'))
## Note: we need synchronous signals
## even though there is no return value--
## this informs the renderer that it is
## safe to begin rendering again.
for method in ['scene', 'setCentralItem']: for method in ['scene', 'setCentralItem']:
setattr(self, method, getattr(self._view, method)) setattr(self, method, getattr(self._view, method))
@ -41,8 +49,12 @@ class RemoteGraphicsView(QtGui.QWidget):
self._view.resize(self.size(), _callSync='off') self._view.resize(self.size(), _callSync='off')
return ret return ret
def sizeHint(self):
return QtCore.QSize(*self._sizeHint)
def remoteSceneChanged(self, data): def remoteSceneChanged(self, data):
w, h, size = data w, h, size = data
#self._sizeHint = (whint, hhint)
if self.shm.size != size: if self.shm.size != size:
self.shm.close() self.shm.close()
self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ)
@ -82,7 +94,17 @@ class RemoteGraphicsView(QtGui.QWidget):
ev.accept() ev.accept()
return QtGui.QWidget.keyEvent(self, ev) return QtGui.QWidget.keyEvent(self, ev)
def enterEvent(self, ev):
self._view.enterEvent(ev.type(), _callSync='off')
return QtGui.QWidget.enterEvent(self, ev)
def leaveEvent(self, ev):
self._view.leaveEvent(ev.type(), _callSync='off')
return QtGui.QWidget.leaveEvent(self, ev)
def remoteProcess(self):
"""Return the remote process handle. (see multiprocess.remoteproxy.RemoteEventHandler)"""
return self._proc
class Renderer(GraphicsView): class Renderer(GraphicsView):
@ -126,6 +148,8 @@ class Renderer(GraphicsView):
def renderView(self): def renderView(self):
if self.img is None: if self.img is None:
## make sure shm is large enough and get its address ## make sure shm is large enough and get its address
if self.width() == 0 or self.height() == 0:
return
size = self.width() * self.height() * 4 size = self.width() * self.height() * 4
if size > self.shm.size(): if size > self.shm.size():
self.shm.resize(size) self.shm.resize(size)
@ -168,5 +192,14 @@ class Renderer(GraphicsView):
GraphicsView.keyEvent(self, QtGui.QKeyEvent(typ, mods, text, autorep, count)) GraphicsView.keyEvent(self, QtGui.QKeyEvent(typ, mods, text, autorep, count))
return ev.accepted() return ev.accepted()
def enterEvent(self, typ):
ev = QtCore.QEvent(QtCore.QEvent.Type(typ))
return GraphicsView.enterEvent(self, ev)
def leaveEvent(self, typ):
ev = QtCore.QEvent(QtCore.QEvent.Type(typ))
return GraphicsView.leaveEvent(self, ev)