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:
commit
3a27997014
63
examples/MultiPlotSpeedTest.py
Normal file
63
examples/MultiPlotSpeedTest.py
Normal 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_()
|
@ -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')
|
||||||
|
78
examples/RemoteSpeedTest.py
Normal file
78
examples/RemoteSpeedTest.py
Normal 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_()
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -3,8 +3,30 @@ from pyqtgraph.GraphicsScene import GraphicsScene
|
|||||||
from pyqtgraph.Point import Point
|
from pyqtgraph.Point import Point
|
||||||
import pyqtgraph.functions as fn
|
import pyqtgraph.functions as fn
|
||||||
import weakref
|
import weakref
|
||||||
|
from pyqtgraph.pgcollections import OrderedDict
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
|
class FiniteCache(OrderedDict):
|
||||||
|
"""Caches a finite number of objects, removing
|
||||||
|
least-frequently used items."""
|
||||||
|
def __init__(self, length):
|
||||||
|
self._length = length
|
||||||
|
OrderedDict.__init__(self)
|
||||||
|
|
||||||
|
def __setitem__(self, item, val):
|
||||||
|
self.pop(item, None) # make sure item is added to end
|
||||||
|
OrderedDict.__setitem__(self, item, val)
|
||||||
|
while len(self) > self._length:
|
||||||
|
del self[self.keys()[0]]
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
val = dict.__getitem__(self, item)
|
||||||
|
del self[item]
|
||||||
|
self[item] = val ## promote this key
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class GraphicsItem(object):
|
class GraphicsItem(object):
|
||||||
"""
|
"""
|
||||||
**Bases:** :class:`object`
|
**Bases:** :class:`object`
|
||||||
@ -16,6 +38,8 @@ class GraphicsItem(object):
|
|||||||
|
|
||||||
The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task.
|
The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task.
|
||||||
"""
|
"""
|
||||||
|
_pixelVectorGlobalCache = FiniteCache(100)
|
||||||
|
|
||||||
def __init__(self, register=True):
|
def __init__(self, register=True):
|
||||||
if not hasattr(self, '_qtBaseClass'):
|
if not hasattr(self, '_qtBaseClass'):
|
||||||
for b in self.__class__.__bases__:
|
for b in self.__class__.__bases__:
|
||||||
@ -25,6 +49,7 @@ class GraphicsItem(object):
|
|||||||
if not hasattr(self, '_qtBaseClass'):
|
if not hasattr(self, '_qtBaseClass'):
|
||||||
raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self))
|
raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self))
|
||||||
|
|
||||||
|
self._pixelVectorCache = [None, None]
|
||||||
self._viewWidget = None
|
self._viewWidget = None
|
||||||
self._viewBox = None
|
self._viewBox = None
|
||||||
self._connectedView = None
|
self._connectedView = None
|
||||||
@ -155,7 +180,6 @@ class GraphicsItem(object):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def pixelVectors(self, direction=None):
|
def pixelVectors(self, direction=None):
|
||||||
"""Return vectors in local coordinates representing the width and height of a view pixel.
|
"""Return vectors in local coordinates representing the width and height of a view pixel.
|
||||||
If direction is specified, then return vectors parallel and orthogonal to it.
|
If direction is specified, then return vectors parallel and orthogonal to it.
|
||||||
@ -164,12 +188,27 @@ class GraphicsItem(object):
|
|||||||
or if pixel size is below floating-point precision limit.
|
or if pixel size is below floating-point precision limit.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
## This is an expensive function that gets called very frequently.
|
||||||
|
## We have two levels of cache to try speeding things up.
|
||||||
|
|
||||||
dt = self.deviceTransform()
|
dt = self.deviceTransform()
|
||||||
if dt is None:
|
if dt is None:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
## check local cache
|
||||||
|
if direction is None and dt == self._pixelVectorCache[0]:
|
||||||
|
return self._pixelVectorCache[1]
|
||||||
|
|
||||||
|
## check global cache
|
||||||
|
key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
|
||||||
|
pv = self._pixelVectorGlobalCache.get(key, None)
|
||||||
|
if pv is not None:
|
||||||
|
self._pixelVectorCache = [dt, pv]
|
||||||
|
return pv
|
||||||
|
|
||||||
|
|
||||||
if direction is None:
|
if direction is None:
|
||||||
direction = Point(1, 0)
|
direction = QtCore.QPointF(1, 0)
|
||||||
if direction.manhattanLength() == 0:
|
if direction.manhattanLength() == 0:
|
||||||
raise Exception("Cannot compute pixel length for 0-length vector.")
|
raise Exception("Cannot compute pixel length for 0-length vector.")
|
||||||
|
|
||||||
@ -184,28 +223,33 @@ class GraphicsItem(object):
|
|||||||
r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5
|
r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5
|
||||||
directionr = direction * r
|
directionr = direction * r
|
||||||
|
|
||||||
viewDir = Point(dt.map(directionr) - dt.map(Point(0,0)))
|
## map direction vector onto device
|
||||||
if viewDir.manhattanLength() == 0:
|
#viewDir = Point(dt.map(directionr) - dt.map(Point(0,0)))
|
||||||
|
#mdirection = dt.map(directionr)
|
||||||
|
dirLine = QtCore.QLineF(QtCore.QPointF(0,0), directionr)
|
||||||
|
viewDir = dt.map(dirLine)
|
||||||
|
if viewDir.length() == 0:
|
||||||
return None, None ## pixel size cannot be represented on this scale
|
return None, None ## pixel size cannot be represented on this scale
|
||||||
|
|
||||||
orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
|
## get unit vector and orthogonal vector (length of pixel)
|
||||||
|
#orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
|
||||||
try:
|
try:
|
||||||
normView = viewDir.norm() ## direction of one pixel orthogonal to line
|
normView = viewDir.unitVector()
|
||||||
normOrtho = orthoDir.norm()
|
#normView = viewDir.norm() ## direction of one pixel orthogonal to line
|
||||||
|
normOrtho = normView.normalVector()
|
||||||
|
#normOrtho = orthoDir.norm()
|
||||||
except:
|
except:
|
||||||
raise Exception("Invalid direction %s" %directionr)
|
raise Exception("Invalid direction %s" %directionr)
|
||||||
|
|
||||||
|
## map back to item
|
||||||
dti = fn.invertQTransform(dt)
|
dti = fn.invertQTransform(dt)
|
||||||
return Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0)))
|
#pv = Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0)))
|
||||||
|
pv = Point(dti.map(normView).p2()), Point(dti.map(normOrtho).p2())
|
||||||
|
self._pixelVectorCache[1] = pv
|
||||||
|
self._pixelVectorCache[0] = dt
|
||||||
|
self._pixelVectorGlobalCache[key] = pv
|
||||||
|
return self._pixelVectorCache[1]
|
||||||
|
|
||||||
#vt = self.deviceTransform()
|
|
||||||
#if vt is None:
|
|
||||||
#return None
|
|
||||||
#vt = vt.inverted()[0]
|
|
||||||
#orig = vt.map(QtCore.QPointF(0, 0))
|
|
||||||
#return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig
|
|
||||||
|
|
||||||
def pixelLength(self, direction, ortho=False):
|
def pixelLength(self, direction, ortho=False):
|
||||||
"""Return the length of one pixel in the direction indicated (in local coordinates)
|
"""Return the length of one pixel in the direction indicated (in local coordinates)
|
||||||
@ -221,7 +265,6 @@ class GraphicsItem(object):
|
|||||||
return normV.length()
|
return normV.length()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def pixelSize(self):
|
def pixelSize(self):
|
||||||
## deprecated
|
## deprecated
|
||||||
v = self.pixelVectors()
|
v = self.pixelVectors()
|
||||||
@ -235,7 +278,7 @@ class GraphicsItem(object):
|
|||||||
if vt is None:
|
if vt is None:
|
||||||
return 0
|
return 0
|
||||||
vt = fn.invertQTransform(vt)
|
vt = fn.invertQTransform(vt)
|
||||||
return Point(vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).length()
|
return vt.map(QtCore.QLineF(0, 0, 1, 0)).length()
|
||||||
|
|
||||||
def pixelHeight(self):
|
def pixelHeight(self):
|
||||||
## deprecated
|
## deprecated
|
||||||
@ -243,7 +286,8 @@ class GraphicsItem(object):
|
|||||||
if vt is None:
|
if vt is None:
|
||||||
return 0
|
return 0
|
||||||
vt = fn.invertQTransform(vt)
|
vt = fn.invertQTransform(vt)
|
||||||
return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length()
|
return vt.map(QtCore.QLineF(0, 0, 0, 1)).length()
|
||||||
|
#return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length()
|
||||||
|
|
||||||
|
|
||||||
def mapToDevice(self, obj):
|
def mapToDevice(self, obj):
|
||||||
@ -358,9 +402,10 @@ class GraphicsItem(object):
|
|||||||
tr = self.itemTransform(relativeItem)
|
tr = self.itemTransform(relativeItem)
|
||||||
if isinstance(tr, tuple): ## difference between pyside and pyqt
|
if isinstance(tr, tuple): ## difference between pyside and pyqt
|
||||||
tr = tr[0]
|
tr = tr[0]
|
||||||
vec = tr.map(Point(1,0)) - tr.map(Point(0,0))
|
#vec = tr.map(Point(1,0)) - tr.map(Point(0,0))
|
||||||
return Point(vec).angle(Point(1,0))
|
vec = tr.map(QtCore.QLineF(0,0,1,0))
|
||||||
|
#return Point(vec).angle(Point(1,0))
|
||||||
|
return vec.angleTo(QtCore.QLineF(vec.p1(), vec.p1()+QtCore.QPointF(1,0)))
|
||||||
|
|
||||||
#def itemChange(self, change, value):
|
#def itemChange(self, change, value):
|
||||||
#ret = self._qtBaseClass.itemChange(self, change, value)
|
#ret = self._qtBaseClass.itemChange(self, change, value)
|
||||||
@ -500,3 +545,6 @@ class GraphicsItem(object):
|
|||||||
else:
|
else:
|
||||||
self._exportOpts = False
|
self._exportOpts = False
|
||||||
|
|
||||||
|
#def update(self):
|
||||||
|
#self._qtBaseClass.update(self)
|
||||||
|
#print "Update:", self
|
@ -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:
|
if self.opts['shadowPen'] is not None:
|
||||||
lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
|
lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
|
||||||
else:
|
else:
|
||||||
lineWidth = (self.opts['pen'].width()+1)
|
lineWidth = (self.opts['pen'].width()+1)
|
||||||
|
|
||||||
|
|
||||||
pixels = self.pixelVectors()
|
pixels = self.pixelVectors()
|
||||||
if pixels == (None, None):
|
if pixels == (None, None):
|
||||||
pixels = [Point(0,0), Point(0,0)]
|
pixels = [Point(0,0), Point(0,0)]
|
||||||
|
|
||||||
xmin = x.min()
|
xmin = x.min()
|
||||||
xmax = x.max()
|
xmax = x.max()
|
||||||
ymin = y.min()
|
ymin = y.min()
|
||||||
ymax = y.max()
|
ymax = y.max()
|
||||||
|
|
||||||
if self.opts['fillLevel'] is not None:
|
if self.opts['fillLevel'] is not None:
|
||||||
ymin = min(ymin, self.opts['fillLevel'])
|
ymin = min(ymin, self.opts['fillLevel'])
|
||||||
ymax = max(ymax, self.opts['fillLevel'])
|
ymax = max(ymax, self.opts['fillLevel'])
|
||||||
|
|
||||||
xmin -= pixels[0].x() * lineWidth
|
xmin -= pixels[0].x() * lineWidth
|
||||||
xmax += pixels[0].x() * lineWidth
|
xmax += pixels[0].x() * lineWidth
|
||||||
ymin -= abs(pixels[1].y()) * lineWidth
|
ymin -= abs(pixels[1].y()) * lineWidth
|
||||||
ymax += abs(pixels[1].y()) * lineWidth
|
ymax += abs(pixels[1].y()) * lineWidth
|
||||||
|
|
||||||
return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
|
self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
|
||||||
|
return self._boundingRect
|
||||||
|
|
||||||
def paint(self, p, opt, widget):
|
def paint(self, p, opt, widget):
|
||||||
prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True)
|
prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True)
|
||||||
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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__':
|
||||||
|
@ -9,6 +9,7 @@ from pyqtgraph.GraphicsScene import GraphicsScene
|
|||||||
import pyqtgraph
|
import pyqtgraph
|
||||||
import weakref
|
import weakref
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
import pyqtgraph.debug as debug
|
||||||
|
|
||||||
__all__ = ['ViewBox']
|
__all__ = ['ViewBox']
|
||||||
|
|
||||||
@ -110,6 +111,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
'background': None,
|
'background': None,
|
||||||
}
|
}
|
||||||
self._updatingRange = False ## Used to break recursive loops. See updateAutoRange.
|
self._updatingRange = False ## Used to break recursive loops. See updateAutoRange.
|
||||||
|
self._itemBoundsCache = weakref.WeakKeyDictionary()
|
||||||
|
|
||||||
self.locateGroup = None ## items displayed when using ViewBox.locate(item)
|
self.locateGroup = None ## items displayed when using ViewBox.locate(item)
|
||||||
|
|
||||||
@ -571,40 +573,18 @@ class ViewBox(GraphicsWidget):
|
|||||||
if xr is not None:
|
if xr is not None:
|
||||||
if self.state['autoPan'][ax]:
|
if self.state['autoPan'][ax]:
|
||||||
x = sum(xr) * 0.5
|
x = sum(xr) * 0.5
|
||||||
#x = childRect.center().x()
|
|
||||||
w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2.
|
w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2.
|
||||||
#childRect.setLeft(x-w2)
|
|
||||||
#childRect.setRight(x+w2)
|
|
||||||
childRange[ax] = [x-w2, x+w2]
|
childRange[ax] = [x-w2, x+w2]
|
||||||
else:
|
else:
|
||||||
#wp = childRect.width() * 0.02
|
|
||||||
wp = (xr[1] - xr[0]) * 0.02
|
wp = (xr[1] - xr[0]) * 0.02
|
||||||
#childRect = childRect.adjusted(-wp, 0, wp, 0)
|
|
||||||
childRange[ax][0] -= wp
|
childRange[ax][0] -= wp
|
||||||
childRange[ax][1] += wp
|
childRange[ax][1] += wp
|
||||||
#targetRect[ax][0] = childRect.left()
|
|
||||||
#targetRect[ax][1] = childRect.right()
|
|
||||||
targetRect[ax] = childRange[ax]
|
targetRect[ax] = childRange[ax]
|
||||||
args['xRange' if ax == 0 else 'yRange'] = targetRect[ax]
|
args['xRange' if ax == 0 else 'yRange'] = targetRect[ax]
|
||||||
#else:
|
|
||||||
### Make corrections to Y range
|
|
||||||
#if self.state['autoPan'][1]:
|
|
||||||
#y = childRect.center().y()
|
|
||||||
#h2 = (targetRect[1][1]-targetRect[1][0]) / 2.
|
|
||||||
#childRect.setTop(y-h2)
|
|
||||||
#childRect.setBottom(y+h2)
|
|
||||||
#else:
|
|
||||||
#hp = childRect.height() * 0.02
|
|
||||||
#childRect = childRect.adjusted(0, -hp, 0, hp)
|
|
||||||
|
|
||||||
#targetRect[1][0] = childRect.top()
|
|
||||||
#targetRect[1][1] = childRect.bottom()
|
|
||||||
#args['yRange'] = targetRect[1]
|
|
||||||
if len(args) == 0:
|
if len(args) == 0:
|
||||||
return
|
return
|
||||||
args['padding'] = 0
|
args['padding'] = 0
|
||||||
args['disableAutoRange'] = False
|
args['disableAutoRange'] = False
|
||||||
#self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False)
|
|
||||||
self.setRange(**args)
|
self.setRange(**args)
|
||||||
finally:
|
finally:
|
||||||
self._updatingRange = False
|
self._updatingRange = False
|
||||||
@ -744,6 +724,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
self.updateAutoRange()
|
self.updateAutoRange()
|
||||||
|
|
||||||
def itemBoundsChanged(self, item):
|
def itemBoundsChanged(self, item):
|
||||||
|
self._itemBoundsCache.pop(item, None)
|
||||||
self.updateAutoRange()
|
self.updateAutoRange()
|
||||||
|
|
||||||
def invertY(self, b=True):
|
def invertY(self, b=True):
|
||||||
@ -1015,6 +996,8 @@ class ViewBox(GraphicsWidget):
|
|||||||
[[xmin, xmax], [ymin, ymax]]
|
[[xmin, xmax], [ymin, ymax]]
|
||||||
Values may be None if there are no specific bounds for an axis.
|
Values may be None if there are no specific bounds for an axis.
|
||||||
"""
|
"""
|
||||||
|
prof = debug.Profiler('updateAutoRange', disabled=True)
|
||||||
|
|
||||||
|
|
||||||
#items = self.allChildren()
|
#items = self.allChildren()
|
||||||
items = self.addedItems
|
items = self.addedItems
|
||||||
@ -1029,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
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -144,6 +144,10 @@ class GraphicsView(QtGui.QGraphicsView):
|
|||||||
brush = fn.mkBrush(background)
|
brush = fn.mkBrush(background)
|
||||||
self.setBackgroundBrush(brush)
|
self.setBackgroundBrush(brush)
|
||||||
|
|
||||||
|
def paintEvent(self, ev):
|
||||||
|
self.scene().prepareForPaint()
|
||||||
|
#print "GV: paint", ev.rect()
|
||||||
|
return QtGui.QGraphicsView.paintEvent(self, ev)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.centralWidget = None
|
self.centralWidget = None
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user