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
|
||||
y = x*1000 + 0.005 * np.random.normal(size=1000)
|
||||
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.setLabel('left', "Y Axis", units='A')
|
||||
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 -*-
|
||||
import sys, os
|
||||
## 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
|
||||
import pyqtgraph as pg
|
||||
|
@ -22,6 +22,7 @@ examples = OrderedDict([
|
||||
('Dock widgets', 'dockarea.py'),
|
||||
('Console', 'ConsoleWidget.py'),
|
||||
('Histograms', 'histogram.py'),
|
||||
('Remote Plotting', 'RemoteSpeedTest.py'),
|
||||
('GraphicsItems', OrderedDict([
|
||||
('Scatter Plot', 'ScatterPlot.py'),
|
||||
#('PlotItem', 'PlotItem.py'),
|
||||
@ -182,6 +183,7 @@ def testFile(name, f, exe, lib, graphicsSystem=None):
|
||||
code = """
|
||||
try:
|
||||
%s
|
||||
import initExample
|
||||
import pyqtgraph as pg
|
||||
%s
|
||||
import %s
|
||||
|
@ -16,7 +16,6 @@ if not hasattr(sys, 'frozen'):
|
||||
sys.path.remove(p)
|
||||
sys.path.insert(0, p)
|
||||
|
||||
|
||||
## should force example to use PySide instead of PyQt
|
||||
if 'pyside' in sys.argv:
|
||||
from PySide import QtGui
|
||||
|
@ -43,6 +43,9 @@ from pyqtgraph.Qt import QtCore, QtGui
|
||||
app = pg.QtGui.QApplication([])
|
||||
|
||||
print "\n=================\nStart QtProcess"
|
||||
import sys
|
||||
if (sys.flags.interactive != 1):
|
||||
print " (not interactive; remote process will exit immediately.)"
|
||||
proc = mp.QtProcess()
|
||||
d1 = 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
|
||||
sigMouseClicked = QtCore.Signal(object) ## emitted when mouse is clicked. Check for event.isAccepted() to see whether the event has already been acted on.
|
||||
|
||||
sigPrepareForPaint = QtCore.Signal() ## emitted immediately before the scene is about to be rendered
|
||||
|
||||
_addressCache = weakref.WeakValueDictionary()
|
||||
|
||||
ExportDirectory = None
|
||||
@ -98,6 +100,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
|
||||
self.clickEvents = []
|
||||
self.dragButtons = []
|
||||
self.prepItems = weakref.WeakKeyDictionary() ## set of items with prepareForPaintMethods
|
||||
self.mouseGrabber = None
|
||||
self.dragItem = None
|
||||
self.lastDrag = None
|
||||
@ -112,6 +115,17 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
|
||||
self.exportDialog = None
|
||||
|
||||
def render(self, *args):
|
||||
self.prepareForPaint()
|
||||
return QGraphicsScene.render(self, *args)
|
||||
|
||||
def prepareForPaint(self):
|
||||
"""Called before every render. This method will inform items that the scene is about to
|
||||
be rendered by emitting sigPrepareForPaint.
|
||||
|
||||
This allows items to delay expensive processing until they know a paint will be required."""
|
||||
self.sigPrepareForPaint.emit()
|
||||
|
||||
|
||||
def setClickRadius(self, r):
|
||||
"""
|
||||
@ -224,7 +238,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
else:
|
||||
acceptable = int(ev.buttons()) == 0 ## if we are in mid-drag, do not allow items to accept the hover event.
|
||||
event = HoverEvent(ev, acceptable)
|
||||
items = self.itemsNearEvent(event)
|
||||
items = self.itemsNearEvent(event, hoverable=True)
|
||||
self.sigMouseHover.emit(items)
|
||||
|
||||
prevItems = list(self.hoverItems.keys())
|
||||
@ -402,7 +416,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
#return item
|
||||
return self.translateGraphicsItem(item)
|
||||
|
||||
def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder):
|
||||
def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder, hoverable=False):
|
||||
"""
|
||||
Return an iterator that iterates first through the items that directly intersect point (in Z order)
|
||||
followed by any other items that are within the scene's click radius.
|
||||
@ -429,6 +443,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
## remove items whose shape does not contain point (scene.items() apparently sucks at this)
|
||||
items2 = []
|
||||
for item in items:
|
||||
if hoverable and not hasattr(item, 'hoverEvent'):
|
||||
continue
|
||||
shape = item.shape()
|
||||
if shape is None:
|
||||
continue
|
||||
|
@ -356,8 +356,14 @@ class GarbageWatcher(object):
|
||||
return self.objs[item]
|
||||
|
||||
|
||||
class Profiler(object):
|
||||
class Profiler:
|
||||
"""Simple profiler allowing measurement of multiple time intervals.
|
||||
Arguments:
|
||||
msg: message to print at start and finish of profiling
|
||||
disabled: If true, profiler does nothing (so you can leave it in place)
|
||||
delayed: If true, all messages are printed after call to finish()
|
||||
(this can result in more accurate time step measurements)
|
||||
globalDelay: if True, all nested profilers delay printing until the top level finishes
|
||||
|
||||
Example:
|
||||
prof = Profiler('Function')
|
||||
@ -368,34 +374,65 @@ class Profiler(object):
|
||||
prof.finish()
|
||||
"""
|
||||
depth = 0
|
||||
msgs = []
|
||||
|
||||
def __init__(self, msg="Profiler", disabled=False):
|
||||
self.depth = Profiler.depth
|
||||
Profiler.depth += 1
|
||||
|
||||
def __init__(self, msg="Profiler", disabled=False, delayed=True, globalDelay=True):
|
||||
self.disabled = disabled
|
||||
if disabled:
|
||||
return
|
||||
|
||||
self.markCount = 0
|
||||
self.finished = False
|
||||
self.depth = Profiler.depth
|
||||
Profiler.depth += 1
|
||||
if not globalDelay:
|
||||
self.msgs = []
|
||||
self.delayed = delayed
|
||||
self.msg = " "*self.depth + msg
|
||||
msg2 = self.msg + " >>> Started"
|
||||
if self.delayed:
|
||||
self.msgs.append(msg2)
|
||||
else:
|
||||
print msg2
|
||||
self.t0 = ptime.time()
|
||||
self.t1 = self.t0
|
||||
self.msg = " "*self.depth + msg
|
||||
print(self.msg, ">>> Started")
|
||||
|
||||
def mark(self, msg=''):
|
||||
def mark(self, msg=None):
|
||||
if self.disabled:
|
||||
return
|
||||
t1 = ptime.time()
|
||||
print(" "+self.msg, msg, "%gms" % ((t1-self.t1)*1000))
|
||||
self.t1 = t1
|
||||
|
||||
def finish(self):
|
||||
if self.disabled:
|
||||
if msg is None:
|
||||
msg = str(self.markCount)
|
||||
self.markCount += 1
|
||||
|
||||
t1 = ptime.time()
|
||||
msg2 = " "+self.msg+" "+msg+" "+"%gms" % ((t1-self.t1)*1000)
|
||||
if self.delayed:
|
||||
self.msgs.append(msg2)
|
||||
else:
|
||||
print msg2
|
||||
self.t1 = ptime.time() ## don't measure time it took to print
|
||||
|
||||
def finish(self, msg=None):
|
||||
if self.disabled or self.finished:
|
||||
return
|
||||
t1 = ptime.time()
|
||||
print(self.msg, '<<< Finished, total time:', "%gms" % ((t1-self.t0)*1000))
|
||||
|
||||
def __del__(self):
|
||||
Profiler.depth -= 1
|
||||
if msg is not None:
|
||||
self.mark(msg)
|
||||
t1 = ptime.time()
|
||||
msg = self.msg + ' <<< Finished, total time: %gms' % ((t1-self.t0)*1000)
|
||||
if self.delayed:
|
||||
self.msgs.append(msg)
|
||||
if self.depth == 0:
|
||||
for line in self.msgs:
|
||||
print line
|
||||
Profiler.msgs = []
|
||||
else:
|
||||
print msg
|
||||
Profiler.depth = self.depth
|
||||
self.finished = True
|
||||
|
||||
|
||||
|
||||
|
||||
def profile(code, name='profile_run', sort='cumulative', num=30):
|
||||
|
@ -350,9 +350,7 @@ class AxisItem(GraphicsWidget):
|
||||
|
||||
## decide optimal minor tick spacing in pixels (this is just aesthetics)
|
||||
pixelSpacing = np.log(size+10) * 5
|
||||
optimalTickCount = size / pixelSpacing
|
||||
if optimalTickCount < 1:
|
||||
optimalTickCount = 1
|
||||
optimalTickCount = max(2., size / pixelSpacing)
|
||||
|
||||
## optimal minor tick spacing
|
||||
optimalSpacing = dif / optimalTickCount
|
||||
@ -366,12 +364,21 @@ class AxisItem(GraphicsWidget):
|
||||
while intervals[minorIndex+1] <= optimalSpacing:
|
||||
minorIndex += 1
|
||||
|
||||
return [
|
||||
levels = [
|
||||
(intervals[minorIndex+2], 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
|
||||
### Determine major/minor tick spacings which flank the optimal spacing.
|
||||
#intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit
|
||||
@ -587,7 +594,7 @@ class AxisItem(GraphicsWidget):
|
||||
ticks = tickLevels[i][1]
|
||||
|
||||
## length of tick
|
||||
tickLength = self.tickLength / ((i*1.0)+1.0)
|
||||
tickLength = self.tickLength / ((i*0.5)+1.0)
|
||||
|
||||
lineAlpha = 255 / (i+1)
|
||||
if self.grid is not False:
|
||||
|
@ -3,8 +3,30 @@ from pyqtgraph.GraphicsScene import GraphicsScene
|
||||
from pyqtgraph.Point import Point
|
||||
import pyqtgraph.functions as fn
|
||||
import weakref
|
||||
from pyqtgraph.pgcollections import OrderedDict
|
||||
import operator
|
||||
|
||||
class FiniteCache(OrderedDict):
|
||||
"""Caches a finite number of objects, removing
|
||||
least-frequently used items."""
|
||||
def __init__(self, length):
|
||||
self._length = length
|
||||
OrderedDict.__init__(self)
|
||||
|
||||
def __setitem__(self, item, val):
|
||||
self.pop(item, None) # make sure item is added to end
|
||||
OrderedDict.__setitem__(self, item, val)
|
||||
while len(self) > self._length:
|
||||
del self[self.keys()[0]]
|
||||
|
||||
def __getitem__(self, item):
|
||||
val = dict.__getitem__(self, item)
|
||||
del self[item]
|
||||
self[item] = val ## promote this key
|
||||
return val
|
||||
|
||||
|
||||
|
||||
class GraphicsItem(object):
|
||||
"""
|
||||
**Bases:** :class:`object`
|
||||
@ -16,6 +38,8 @@ class GraphicsItem(object):
|
||||
|
||||
The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task.
|
||||
"""
|
||||
_pixelVectorGlobalCache = FiniteCache(100)
|
||||
|
||||
def __init__(self, register=True):
|
||||
if not hasattr(self, '_qtBaseClass'):
|
||||
for b in self.__class__.__bases__:
|
||||
@ -25,6 +49,7 @@ class GraphicsItem(object):
|
||||
if not hasattr(self, '_qtBaseClass'):
|
||||
raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self))
|
||||
|
||||
self._pixelVectorCache = [None, None]
|
||||
self._viewWidget = None
|
||||
self._viewBox = None
|
||||
self._connectedView = None
|
||||
@ -155,7 +180,6 @@ class GraphicsItem(object):
|
||||
|
||||
|
||||
|
||||
|
||||
def pixelVectors(self, direction=None):
|
||||
"""Return vectors in local coordinates representing the width and height of a view pixel.
|
||||
If direction is specified, then return vectors parallel and orthogonal to it.
|
||||
@ -164,12 +188,27 @@ class GraphicsItem(object):
|
||||
or if pixel size is below floating-point precision limit.
|
||||
"""
|
||||
|
||||
## This is an expensive function that gets called very frequently.
|
||||
## We have two levels of cache to try speeding things up.
|
||||
|
||||
dt = self.deviceTransform()
|
||||
if dt is None:
|
||||
return None, None
|
||||
|
||||
## check local cache
|
||||
if direction is None and dt == self._pixelVectorCache[0]:
|
||||
return self._pixelVectorCache[1]
|
||||
|
||||
## check global cache
|
||||
key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
|
||||
pv = self._pixelVectorGlobalCache.get(key, None)
|
||||
if pv is not None:
|
||||
self._pixelVectorCache = [dt, pv]
|
||||
return pv
|
||||
|
||||
|
||||
if direction is None:
|
||||
direction = Point(1, 0)
|
||||
direction = QtCore.QPointF(1, 0)
|
||||
if direction.manhattanLength() == 0:
|
||||
raise Exception("Cannot compute pixel length for 0-length vector.")
|
||||
|
||||
@ -184,28 +223,33 @@ class GraphicsItem(object):
|
||||
r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5
|
||||
directionr = direction * r
|
||||
|
||||
viewDir = Point(dt.map(directionr) - dt.map(Point(0,0)))
|
||||
if viewDir.manhattanLength() == 0:
|
||||
## map direction vector onto device
|
||||
#viewDir = Point(dt.map(directionr) - dt.map(Point(0,0)))
|
||||
#mdirection = dt.map(directionr)
|
||||
dirLine = QtCore.QLineF(QtCore.QPointF(0,0), directionr)
|
||||
viewDir = dt.map(dirLine)
|
||||
if viewDir.length() == 0:
|
||||
return None, None ## pixel size cannot be represented on this scale
|
||||
|
||||
orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
|
||||
|
||||
## get unit vector and orthogonal vector (length of pixel)
|
||||
#orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space
|
||||
try:
|
||||
normView = viewDir.norm() ## direction of one pixel orthogonal to line
|
||||
normOrtho = orthoDir.norm()
|
||||
normView = viewDir.unitVector()
|
||||
#normView = viewDir.norm() ## direction of one pixel orthogonal to line
|
||||
normOrtho = normView.normalVector()
|
||||
#normOrtho = orthoDir.norm()
|
||||
except:
|
||||
raise Exception("Invalid direction %s" %directionr)
|
||||
|
||||
|
||||
## map back to item
|
||||
dti = fn.invertQTransform(dt)
|
||||
return Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0)))
|
||||
#pv = Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0)))
|
||||
pv = Point(dti.map(normView).p2()), Point(dti.map(normOrtho).p2())
|
||||
self._pixelVectorCache[1] = pv
|
||||
self._pixelVectorCache[0] = dt
|
||||
self._pixelVectorGlobalCache[key] = pv
|
||||
return self._pixelVectorCache[1]
|
||||
|
||||
#vt = self.deviceTransform()
|
||||
#if vt is None:
|
||||
#return None
|
||||
#vt = vt.inverted()[0]
|
||||
#orig = vt.map(QtCore.QPointF(0, 0))
|
||||
#return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig
|
||||
|
||||
def pixelLength(self, direction, ortho=False):
|
||||
"""Return the length of one pixel in the direction indicated (in local coordinates)
|
||||
@ -221,7 +265,6 @@ class GraphicsItem(object):
|
||||
return normV.length()
|
||||
|
||||
|
||||
|
||||
def pixelSize(self):
|
||||
## deprecated
|
||||
v = self.pixelVectors()
|
||||
@ -235,7 +278,7 @@ class GraphicsItem(object):
|
||||
if vt is None:
|
||||
return 0
|
||||
vt = fn.invertQTransform(vt)
|
||||
return Point(vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).length()
|
||||
return vt.map(QtCore.QLineF(0, 0, 1, 0)).length()
|
||||
|
||||
def pixelHeight(self):
|
||||
## deprecated
|
||||
@ -243,7 +286,8 @@ class GraphicsItem(object):
|
||||
if vt is None:
|
||||
return 0
|
||||
vt = fn.invertQTransform(vt)
|
||||
return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length()
|
||||
return vt.map(QtCore.QLineF(0, 0, 0, 1)).length()
|
||||
#return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length()
|
||||
|
||||
|
||||
def mapToDevice(self, obj):
|
||||
@ -358,9 +402,10 @@ class GraphicsItem(object):
|
||||
tr = self.itemTransform(relativeItem)
|
||||
if isinstance(tr, tuple): ## difference between pyside and pyqt
|
||||
tr = tr[0]
|
||||
vec = tr.map(Point(1,0)) - tr.map(Point(0,0))
|
||||
return Point(vec).angle(Point(1,0))
|
||||
|
||||
#vec = tr.map(Point(1,0)) - tr.map(Point(0,0))
|
||||
vec = tr.map(QtCore.QLineF(0,0,1,0))
|
||||
#return Point(vec).angle(Point(1,0))
|
||||
return vec.angleTo(QtCore.QLineF(vec.p1(), vec.p1()+QtCore.QPointF(1,0)))
|
||||
|
||||
#def itemChange(self, change, value):
|
||||
#ret = self._qtBaseClass.itemChange(self, change, value)
|
||||
@ -500,3 +545,6 @@ class GraphicsItem(object):
|
||||
else:
|
||||
self._exportOpts = False
|
||||
|
||||
#def update(self):
|
||||
#self._qtBaseClass.update(self)
|
||||
#print "Update:", self
|
@ -52,7 +52,7 @@ class PlotCurveItem(GraphicsObject):
|
||||
self.clear()
|
||||
self.path = None
|
||||
self.fillPath = None
|
||||
|
||||
self._boundsCache = [None, None]
|
||||
|
||||
## this is disastrous for performance.
|
||||
#self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
|
||||
@ -85,6 +85,12 @@ class PlotCurveItem(GraphicsObject):
|
||||
return self.xData, self.yData
|
||||
|
||||
def dataBounds(self, ax, frac=1.0, orthoRange=None):
|
||||
## Need this to run as fast as possible.
|
||||
## check cache first:
|
||||
cache = self._boundsCache[ax]
|
||||
if cache is not None and cache[0] == (frac, orthoRange):
|
||||
return cache[1]
|
||||
|
||||
(x, y) = self.getData()
|
||||
if x is None or len(x) == 0:
|
||||
return (0, 0)
|
||||
@ -103,15 +109,22 @@ class PlotCurveItem(GraphicsObject):
|
||||
|
||||
|
||||
if frac >= 1.0:
|
||||
return (d.min(), d.max())
|
||||
b = (d.min(), d.max())
|
||||
elif frac <= 0.0:
|
||||
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
|
||||
else:
|
||||
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
|
||||
b = (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
|
||||
self._boundsCache[ax] = [(frac, orthoRange), b]
|
||||
return b
|
||||
|
||||
def invalidateBounds(self):
|
||||
self._boundingRect = None
|
||||
self._boundsCache = [None, None]
|
||||
|
||||
def setPen(self, *args, **kargs):
|
||||
"""Set the pen used to draw the curve."""
|
||||
self.opts['pen'] = fn.mkPen(*args, **kargs)
|
||||
self.invalidateBounds()
|
||||
self.update()
|
||||
|
||||
def setShadowPen(self, *args, **kargs):
|
||||
@ -120,52 +133,22 @@ class PlotCurveItem(GraphicsObject):
|
||||
pen to be visible.
|
||||
"""
|
||||
self.opts['shadowPen'] = fn.mkPen(*args, **kargs)
|
||||
self.invalidateBounds()
|
||||
self.update()
|
||||
|
||||
def setBrush(self, *args, **kargs):
|
||||
"""Set the brush used when filling the area under the curve"""
|
||||
self.opts['brush'] = fn.mkBrush(*args, **kargs)
|
||||
self.invalidateBounds()
|
||||
self.update()
|
||||
|
||||
def setFillLevel(self, level):
|
||||
"""Set the level filled to when filling under the curve"""
|
||||
self.opts['fillLevel'] = level
|
||||
self.fillPath = None
|
||||
self.invalidateBounds()
|
||||
self.update()
|
||||
|
||||
#def setColor(self, color):
|
||||
#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):
|
||||
"""
|
||||
============== ========================================================
|
||||
@ -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
|
||||
## Test this bug with test_PlotWidget and zoom in on the animated plot
|
||||
self.invalidateBounds()
|
||||
self.prepareGeometryChange()
|
||||
self.informViewBoundsChanged()
|
||||
self.yData = kargs['y'].view(np.ndarray)
|
||||
self.xData = kargs['x'].view(np.ndarray)
|
||||
|
||||
@ -349,36 +334,38 @@ class PlotCurveItem(GraphicsObject):
|
||||
return self.path
|
||||
|
||||
def boundingRect(self):
|
||||
(x, y) = self.getData()
|
||||
if x is None or y is None or len(x) == 0 or len(y) == 0:
|
||||
return QtCore.QRectF()
|
||||
if self._boundingRect is None:
|
||||
(x, y) = self.getData()
|
||||
if x is None or y is None or len(x) == 0 or len(y) == 0:
|
||||
return QtCore.QRectF()
|
||||
|
||||
|
||||
if self.opts['shadowPen'] is not None:
|
||||
lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
|
||||
else:
|
||||
lineWidth = (self.opts['pen'].width()+1)
|
||||
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)]
|
||||
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()
|
||||
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'])
|
||||
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
|
||||
xmin -= pixels[0].x() * lineWidth
|
||||
xmax += pixels[0].x() * lineWidth
|
||||
ymin -= abs(pixels[1].y()) * lineWidth
|
||||
ymax += abs(pixels[1].y()) * lineWidth
|
||||
|
||||
return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
|
||||
self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
|
||||
return self._boundingRect
|
||||
|
||||
def paint(self, p, opt, widget):
|
||||
prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True)
|
||||
@ -463,25 +450,6 @@ class PlotCurveItem(GraphicsObject):
|
||||
self.path = None
|
||||
#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):
|
||||
if not self.clickable or ev.button() != QtCore.Qt.LeftButton:
|
||||
return
|
||||
|
@ -164,6 +164,7 @@ class PlotDataItem(GraphicsObject):
|
||||
self.opts['fftMode'] = mode
|
||||
self.xDisp = self.yDisp = None
|
||||
self.updateItems()
|
||||
self.informViewBoundsChanged()
|
||||
|
||||
def setLogMode(self, xMode, yMode):
|
||||
if self.opts['logMode'] == [xMode, yMode]:
|
||||
@ -171,6 +172,7 @@ class PlotDataItem(GraphicsObject):
|
||||
self.opts['logMode'] = [xMode, yMode]
|
||||
self.xDisp = self.yDisp = None
|
||||
self.updateItems()
|
||||
self.informViewBoundsChanged()
|
||||
|
||||
def setPointMode(self, mode):
|
||||
if self.opts['pointMode'] == mode:
|
||||
@ -369,9 +371,10 @@ class PlotDataItem(GraphicsObject):
|
||||
self.updateItems()
|
||||
prof.mark('update items')
|
||||
|
||||
view = self.getViewBox()
|
||||
if view is not None:
|
||||
view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
|
||||
self.informViewBoundsChanged()
|
||||
#view = self.getViewBox()
|
||||
#if view is not None:
|
||||
#view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
|
||||
|
||||
self.sigPlotChanged.emit(self)
|
||||
prof.mark('emit')
|
||||
|
@ -34,8 +34,7 @@ class VTickGroup(UIGraphicsItem):
|
||||
if xvals is None:
|
||||
xvals = []
|
||||
|
||||
#bounds = QtCore.QRectF(0, yrange[0], 1, yrange[1]-yrange[0])
|
||||
UIGraphicsItem.__init__(self)#, bounds=bounds)
|
||||
UIGraphicsItem.__init__(self)
|
||||
|
||||
if pen is None:
|
||||
pen = (200, 200, 200)
|
||||
@ -44,15 +43,10 @@ class VTickGroup(UIGraphicsItem):
|
||||
|
||||
self.ticks = []
|
||||
self.xvals = []
|
||||
#if view is None:
|
||||
#self.view = None
|
||||
#else:
|
||||
#self.view = weakref.ref(view)
|
||||
self.yrange = [0,1]
|
||||
self.setPen(pen)
|
||||
self.setYRange(yrange)
|
||||
self.setXVals(xvals)
|
||||
#self.valid = False
|
||||
|
||||
def setPen(self, *args, **kwargs):
|
||||
"""Set the pen to use for drawing ticks. Can be specified as any arguments valid
|
||||
@ -75,80 +69,20 @@ class VTickGroup(UIGraphicsItem):
|
||||
"""Set the y range [low, high] that the ticks are drawn on. 0 is the bottom of
|
||||
the view, 1 is the top."""
|
||||
self.yrange = vals
|
||||
#self.relative = relative
|
||||
#if self.view is not None:
|
||||
#if relative:
|
||||
#self.view().sigRangeChanged.connect(self.rescale)
|
||||
#else:
|
||||
#try:
|
||||
#self.view().sigRangeChanged.disconnect(self.rescale)
|
||||
#except:
|
||||
#pass
|
||||
self.rebuildTicks()
|
||||
#self.valid = False
|
||||
|
||||
def dataBounds(self, *args, **kargs):
|
||||
return None ## item should never affect view autoscaling
|
||||
|
||||
#def viewRangeChanged(self):
|
||||
### called when the view is scaled
|
||||
|
||||
#UIGraphicsItem.viewRangeChanged(self)
|
||||
|
||||
#self.resetTransform()
|
||||
##vb = self.view().viewRect()
|
||||
##p1 = vb.bottom() - vb.height() * self.yrange[0]
|
||||
##p2 = vb.bottom() - vb.height() * self.yrange[1]
|
||||
|
||||
##br = self.boundingRect()
|
||||
##yr = [p1, p2]
|
||||
|
||||
|
||||
|
||||
##self.rebuildTicks()
|
||||
|
||||
##br = self.boundingRect()
|
||||
##print br
|
||||
##self.translate(0.0, br.y())
|
||||
##self.scale(1.0, br.height())
|
||||
##self.boundingRect()
|
||||
#self.update()
|
||||
|
||||
#def boundingRect(self):
|
||||
#print "--request bounds:"
|
||||
#b = self.path.boundingRect()
|
||||
#b2 = UIGraphicsItem.boundingRect(self)
|
||||
#b2.setY(b.y())
|
||||
#b2.setWidth(b.width())
|
||||
#print " ", b
|
||||
#print " ", b2
|
||||
#print " ", self.mapRectToScene(b)
|
||||
#return b2
|
||||
|
||||
def yRange(self):
|
||||
#if self.relative:
|
||||
#height = self.view.size().height()
|
||||
#p1 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[0]))))
|
||||
#p2 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[1]))))
|
||||
#return [p1.y(), p2.y()]
|
||||
#else:
|
||||
#return self.yrange
|
||||
|
||||
return self.yrange
|
||||
|
||||
def rebuildTicks(self):
|
||||
self.path = QtGui.QPainterPath()
|
||||
yrange = self.yRange()
|
||||
#print "rebuild ticks:", yrange
|
||||
for x in self.xvals:
|
||||
#path.moveTo(x, yrange[0])
|
||||
#path.lineTo(x, yrange[1])
|
||||
self.path.moveTo(x, 0.)
|
||||
self.path.lineTo(x, 1.)
|
||||
#self.setPath(self.path)
|
||||
#self.valid = True
|
||||
#self.rescale()
|
||||
#print " done..", self.boundingRect()
|
||||
|
||||
def paint(self, p, *args):
|
||||
UIGraphicsItem.paint(self, p, *args)
|
||||
@ -161,7 +95,6 @@ class VTickGroup(UIGraphicsItem):
|
||||
p.scale(1.0, br.height())
|
||||
p.setPen(self.pen)
|
||||
p.drawPath(self.path)
|
||||
#QtGui.QGraphicsPathItem.paint(self, *args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -9,6 +9,7 @@ from pyqtgraph.GraphicsScene import GraphicsScene
|
||||
import pyqtgraph
|
||||
import weakref
|
||||
from copy import deepcopy
|
||||
import pyqtgraph.debug as debug
|
||||
|
||||
__all__ = ['ViewBox']
|
||||
|
||||
@ -110,6 +111,7 @@ class ViewBox(GraphicsWidget):
|
||||
'background': None,
|
||||
}
|
||||
self._updatingRange = False ## Used to break recursive loops. See updateAutoRange.
|
||||
self._itemBoundsCache = weakref.WeakKeyDictionary()
|
||||
|
||||
self.locateGroup = None ## items displayed when using ViewBox.locate(item)
|
||||
|
||||
@ -571,40 +573,18 @@ class ViewBox(GraphicsWidget):
|
||||
if xr is not None:
|
||||
if self.state['autoPan'][ax]:
|
||||
x = sum(xr) * 0.5
|
||||
#x = childRect.center().x()
|
||||
w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2.
|
||||
#childRect.setLeft(x-w2)
|
||||
#childRect.setRight(x+w2)
|
||||
childRange[ax] = [x-w2, x+w2]
|
||||
else:
|
||||
#wp = childRect.width() * 0.02
|
||||
wp = (xr[1] - xr[0]) * 0.02
|
||||
#childRect = childRect.adjusted(-wp, 0, wp, 0)
|
||||
childRange[ax][0] -= wp
|
||||
childRange[ax][1] += wp
|
||||
#targetRect[ax][0] = childRect.left()
|
||||
#targetRect[ax][1] = childRect.right()
|
||||
targetRect[ax] = childRange[ax]
|
||||
args['xRange' if ax == 0 else 'yRange'] = targetRect[ax]
|
||||
#else:
|
||||
### Make corrections to Y range
|
||||
#if self.state['autoPan'][1]:
|
||||
#y = childRect.center().y()
|
||||
#h2 = (targetRect[1][1]-targetRect[1][0]) / 2.
|
||||
#childRect.setTop(y-h2)
|
||||
#childRect.setBottom(y+h2)
|
||||
#else:
|
||||
#hp = childRect.height() * 0.02
|
||||
#childRect = childRect.adjusted(0, -hp, 0, hp)
|
||||
|
||||
#targetRect[1][0] = childRect.top()
|
||||
#targetRect[1][1] = childRect.bottom()
|
||||
#args['yRange'] = targetRect[1]
|
||||
if len(args) == 0:
|
||||
return
|
||||
args['padding'] = 0
|
||||
args['disableAutoRange'] = False
|
||||
#self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False)
|
||||
self.setRange(**args)
|
||||
finally:
|
||||
self._updatingRange = False
|
||||
@ -744,6 +724,7 @@ class ViewBox(GraphicsWidget):
|
||||
self.updateAutoRange()
|
||||
|
||||
def itemBoundsChanged(self, item):
|
||||
self._itemBoundsCache.pop(item, None)
|
||||
self.updateAutoRange()
|
||||
|
||||
def invertY(self, b=True):
|
||||
@ -1015,6 +996,8 @@ class ViewBox(GraphicsWidget):
|
||||
[[xmin, xmax], [ymin, ymax]]
|
||||
Values may be None if there are no specific bounds for an axis.
|
||||
"""
|
||||
prof = debug.Profiler('updateAutoRange', disabled=True)
|
||||
|
||||
|
||||
#items = self.allChildren()
|
||||
items = self.addedItems
|
||||
@ -1029,38 +1012,36 @@ class ViewBox(GraphicsWidget):
|
||||
if not item.isVisible():
|
||||
continue
|
||||
|
||||
#print "=========", item
|
||||
useX = True
|
||||
useY = True
|
||||
if hasattr(item, 'dataBounds'):
|
||||
if frac is None:
|
||||
frac = (1.0, 1.0)
|
||||
xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
|
||||
yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1])
|
||||
#print " xr:", xr, " yr:", yr
|
||||
if xr is None or xr == (None, None):
|
||||
useX = False
|
||||
xr = (0,0)
|
||||
if yr is None or yr == (None, None):
|
||||
useY = False
|
||||
yr = (0,0)
|
||||
bounds = self._itemBoundsCache.get(item, None)
|
||||
if bounds is None:
|
||||
if frac is None:
|
||||
frac = (1.0, 1.0)
|
||||
xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
|
||||
yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1])
|
||||
if xr is None or xr == (None, None):
|
||||
useX = False
|
||||
xr = (0,0)
|
||||
if yr is None or yr == (None, None):
|
||||
useY = False
|
||||
yr = (0,0)
|
||||
|
||||
bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0])
|
||||
#print " xr:", xr, " yr:", yr
|
||||
#print " item real:", bounds
|
||||
bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0])
|
||||
bounds = self.mapFromItemToView(item, bounds).boundingRect()
|
||||
self._itemBoundsCache[item] = (bounds, useX, useY)
|
||||
else:
|
||||
bounds, useX, useY = bounds
|
||||
else:
|
||||
if int(item.flags() & item.ItemHasNoContents) > 0:
|
||||
continue
|
||||
#print " empty"
|
||||
else:
|
||||
bounds = item.boundingRect()
|
||||
#bounds = [[item.left(), item.top()], [item.right(), item.bottom()]]
|
||||
#print " item:", bounds
|
||||
#bounds = QtCore.QRectF(bounds[0][0], bounds[1][0], bounds[0][1]-bounds[0][0], bounds[1][1]-bounds[1][0])
|
||||
bounds = self.mapFromItemToView(item, bounds).boundingRect()
|
||||
#print " ", bounds
|
||||
bounds = self.mapFromItemToView(item, bounds).boundingRect()
|
||||
|
||||
prof.mark('1')
|
||||
|
||||
#print " useX:", useX, " useY:", useY
|
||||
if not any([useX, useY]):
|
||||
continue
|
||||
|
||||
@ -1073,11 +1054,6 @@ class ViewBox(GraphicsWidget):
|
||||
else:
|
||||
continue ## need to check for item rotations and decide how best to apply this boundary.
|
||||
|
||||
#print " useX:", useX, " useY:", useY
|
||||
|
||||
#print " range:", range
|
||||
#print " bounds (r,l,t,b):", bounds.right(), bounds.left(), bounds.top(), bounds.bottom()
|
||||
|
||||
if useY:
|
||||
if range[1] is not None:
|
||||
range[1] = [min(bounds.top(), range[1][0]), max(bounds.bottom(), range[1][1])]
|
||||
@ -1088,9 +1064,9 @@ class ViewBox(GraphicsWidget):
|
||||
range[0] = [min(bounds.left(), range[0][0]), max(bounds.right(), range[0][1])]
|
||||
else:
|
||||
range[0] = [bounds.left(), bounds.right()]
|
||||
prof.mark('2')
|
||||
|
||||
#print " range:", range
|
||||
|
||||
prof.finish()
|
||||
return range
|
||||
|
||||
def childrenBoundingRect(self, *args, **kwds):
|
||||
@ -1287,5 +1263,4 @@ class ViewBox(GraphicsWidget):
|
||||
self.scene().removeItem(self.locateGroup)
|
||||
self.locateGroup = None
|
||||
|
||||
|
||||
from .ViewBoxMenu import ViewBoxMenu
|
||||
|
@ -1,6 +1,6 @@
|
||||
import os, sys, time, multiprocessing, re
|
||||
from processes import ForkedProcess
|
||||
from remoteproxy import ExitError
|
||||
from remoteproxy import ClosedError
|
||||
|
||||
class CanceledError(Exception):
|
||||
"""Raised when the progress dialog is canceled during a processing operation."""
|
||||
@ -152,7 +152,7 @@ class Parallelize(object):
|
||||
n = ch.processRequests()
|
||||
if n > 0:
|
||||
waitingChildren += 1
|
||||
except ExitError:
|
||||
except ClosedError:
|
||||
#print ch.childPid, 'process finished'
|
||||
rem.append(ch)
|
||||
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 cPickle as pickle
|
||||
import multiprocessing.connection
|
||||
|
||||
__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ExitError', 'NoResultError']
|
||||
__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ClosedError', 'NoResultError']
|
||||
|
||||
class Process(RemoteEventHandler):
|
||||
"""
|
||||
@ -100,7 +100,7 @@ def startEventLoop(name, port, authkey):
|
||||
try:
|
||||
HANDLER.processRequests() # exception raised when the loop should exit
|
||||
time.sleep(0.01)
|
||||
except ExitError:
|
||||
except ClosedError:
|
||||
break
|
||||
|
||||
|
||||
@ -225,7 +225,7 @@ class ForkedProcess(RemoteEventHandler):
|
||||
try:
|
||||
self.processRequests() # exception raised when the loop should exit
|
||||
time.sleep(0.01)
|
||||
except ExitError:
|
||||
except ClosedError:
|
||||
break
|
||||
except:
|
||||
print "Error occurred in forked event loop:"
|
||||
@ -267,11 +267,11 @@ class RemoteQtEventHandler(RemoteEventHandler):
|
||||
def processRequests(self):
|
||||
try:
|
||||
RemoteEventHandler.processRequests(self)
|
||||
except ExitError:
|
||||
except ClosedError:
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
QtGui.QApplication.instance().quit()
|
||||
self.timer.stop()
|
||||
#raise
|
||||
#raise SystemExit
|
||||
|
||||
class QtProcess(Process):
|
||||
"""
|
||||
@ -315,7 +315,7 @@ class QtProcess(Process):
|
||||
def processRequests(self):
|
||||
try:
|
||||
Process.processRequests(self)
|
||||
except ExitError:
|
||||
except ClosedError:
|
||||
self.timer.stop()
|
||||
|
||||
def startQtEventLoop(name, port, authkey):
|
||||
|
@ -1,10 +1,15 @@
|
||||
import os, __builtin__, time, sys, traceback, weakref
|
||||
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
|
||||
|
||||
class NoResultError(Exception):
|
||||
"""Raised when a request for the return value of a remote call fails
|
||||
because the call has not yet returned."""
|
||||
pass
|
||||
|
||||
|
||||
@ -82,14 +87,14 @@ class RemoteEventHandler(object):
|
||||
Returns the number of events processed.
|
||||
"""
|
||||
if self.exited:
|
||||
raise ExitError()
|
||||
raise ClosedError()
|
||||
|
||||
numProcessed = 0
|
||||
while self.conn.poll():
|
||||
try:
|
||||
self.handleRequest()
|
||||
numProcessed += 1
|
||||
except ExitError:
|
||||
except ClosedError:
|
||||
self.exited = True
|
||||
raise
|
||||
except IOError as err:
|
||||
@ -108,14 +113,20 @@ class RemoteEventHandler(object):
|
||||
Blocks until a request is available."""
|
||||
result = None
|
||||
try:
|
||||
cmd, reqId, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails
|
||||
except EOFError:
|
||||
cmd, reqId, nByteMsgs, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails
|
||||
except (EOFError, IOError):
|
||||
## remote process has shut down; end event loop
|
||||
raise ExitError()
|
||||
except IOError:
|
||||
raise ExitError()
|
||||
raise ClosedError()
|
||||
#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:
|
||||
if cmd == 'result' or cmd == 'error':
|
||||
@ -137,17 +148,36 @@ class RemoteEventHandler(object):
|
||||
obj = opts['obj']
|
||||
fnargs = opts['args']
|
||||
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.
|
||||
#print obj, fnargs
|
||||
result = obj(*fnargs)
|
||||
else:
|
||||
result = obj(*fnargs, **fnkwds)
|
||||
|
||||
elif cmd == 'getObjValue':
|
||||
result = opts['obj'] ## has already been unpickled into its local value
|
||||
returnType = 'value'
|
||||
elif cmd == 'transfer':
|
||||
result = opts['obj']
|
||||
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':
|
||||
name = opts['module']
|
||||
fromlist = opts.get('fromlist', [])
|
||||
@ -201,7 +231,7 @@ class RemoteEventHandler(object):
|
||||
## (more importantly, do not call any code that would
|
||||
## normally be invoked at exit)
|
||||
else:
|
||||
raise ExitError()
|
||||
raise ClosedError()
|
||||
|
||||
|
||||
|
||||
@ -216,7 +246,7 @@ class RemoteEventHandler(object):
|
||||
except:
|
||||
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.
|
||||
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
|
||||
@ -235,6 +265,9 @@ class RemoteEventHandler(object):
|
||||
opts Extra arguments sent to the remote process that determine the way
|
||||
the request will be handled (see below)
|
||||
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:
|
||||
@ -312,7 +345,9 @@ class RemoteEventHandler(object):
|
||||
|
||||
if returnType is not None:
|
||||
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
|
||||
try:
|
||||
optStr = pickle.dumps(opts)
|
||||
@ -322,9 +357,19 @@ class RemoteEventHandler(object):
|
||||
print "======================================="
|
||||
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)
|
||||
|
||||
## 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':
|
||||
return
|
||||
|
||||
@ -345,10 +390,10 @@ class RemoteEventHandler(object):
|
||||
## raises NoResultError if the result is not available yet
|
||||
#print self.results.keys(), os.getpid()
|
||||
if reqId not in self.results:
|
||||
#self.readPipe()
|
||||
try:
|
||||
self.processRequests()
|
||||
except ExitError:
|
||||
except ClosedError: ## even if remote connection has closed, we may have
|
||||
## received new data during this call to processRequests()
|
||||
pass
|
||||
if reqId not in self.results:
|
||||
raise NoResultError()
|
||||
@ -393,17 +438,33 @@ class RemoteEventHandler(object):
|
||||
|
||||
def callObj(self, obj, args, kwds, **opts):
|
||||
opts = opts.copy()
|
||||
args = list(args)
|
||||
|
||||
## Decide whether to send arguments by value or by proxy
|
||||
noProxyTypes = opts.pop('noProxyTypes', None)
|
||||
if noProxyTypes is None:
|
||||
noProxyTypes = self.proxyOptions['noProxyTypes']
|
||||
autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy'])
|
||||
|
||||
autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy'])
|
||||
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():
|
||||
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):
|
||||
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)
|
||||
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):
|
||||
## 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 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 the process's connection has closed before the result arrives, raise ClosedError.
|
||||
"""
|
||||
|
||||
if self.gotResult:
|
||||
@ -464,6 +531,8 @@ class Request(object):
|
||||
if block:
|
||||
start = time.time()
|
||||
while not self.hasResult():
|
||||
if self.proc.exited:
|
||||
raise ClosedError()
|
||||
time.sleep(0.005)
|
||||
if timeout >= 0 and time.time() - start > timeout:
|
||||
print "Request timed out:", self.description
|
||||
|
@ -144,6 +144,10 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
brush = fn.mkBrush(background)
|
||||
self.setBackgroundBrush(brush)
|
||||
|
||||
def paintEvent(self, ev):
|
||||
self.scene().prepareForPaint()
|
||||
#print "GV: paint", ev.rect()
|
||||
return QtGui.QGraphicsView.paintEvent(self, ev)
|
||||
|
||||
def close(self):
|
||||
self.centralWidget = None
|
||||
|
@ -18,6 +18,8 @@ class RemoteGraphicsView(QtGui.QWidget):
|
||||
def __init__(self, parent=None, *args, **kwds):
|
||||
self._img = 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)
|
||||
self._proc = mp.QtProcess()
|
||||
self.pg = self._proc._import('pyqtgraph')
|
||||
@ -26,12 +28,18 @@ class RemoteGraphicsView(QtGui.QWidget):
|
||||
self._view = rpgRemote.Renderer(*args, **kwds)
|
||||
self._view._setProxyOptions(deferGetattr=True)
|
||||
self.setFocusPolicy(self._view.focusPolicy())
|
||||
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
|
||||
self.setMouseTracking(True)
|
||||
|
||||
shmFileName = self._view.shmFileName()
|
||||
self.shmFile = open(shmFileName, 'r')
|
||||
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']:
|
||||
setattr(self, method, getattr(self._view, method))
|
||||
@ -41,8 +49,12 @@ class RemoteGraphicsView(QtGui.QWidget):
|
||||
self._view.resize(self.size(), _callSync='off')
|
||||
return ret
|
||||
|
||||
def sizeHint(self):
|
||||
return QtCore.QSize(*self._sizeHint)
|
||||
|
||||
def remoteSceneChanged(self, data):
|
||||
w, h, size = data
|
||||
#self._sizeHint = (whint, hhint)
|
||||
if self.shm.size != size:
|
||||
self.shm.close()
|
||||
self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ)
|
||||
@ -82,7 +94,17 @@ class RemoteGraphicsView(QtGui.QWidget):
|
||||
ev.accept()
|
||||
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):
|
||||
|
||||
@ -126,6 +148,8 @@ class Renderer(GraphicsView):
|
||||
def renderView(self):
|
||||
if self.img is None:
|
||||
## 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
|
||||
if size > self.shm.size():
|
||||
self.shm.resize(size)
|
||||
@ -168,5 +192,14 @@ class Renderer(GraphicsView):
|
||||
GraphicsView.keyEvent(self, QtGui.QKeyEvent(typ, mods, text, autorep, count))
|
||||
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