Features:

- Added GraphItem class for displaying networks/trees
  - Added ColorMap class for mapping linear gradients and generating lookup tables
    (Provides gradient editor functionality without the GUI)
  - Added ColorMapWidget for complex user-defined color mapping
  - Added ScatterPlotWidget for exploring relationships in multi-column tables
  - Added ErrorBarItem
  - SVG and image exporters can now copy to clipboard
  - PlotItem gets new methods: addLine, setLabels, and listDataItems
  - AxisItem gets setTickFont method
  - Added functions.arrayToQPath, shared between GraphItem and PlotCurveItem
  - Added gradient editors to parametertree
  - Expanded documentation, added beginning of Qt crash course

Bugfixes:
  - Fixed auto-ranging bugs: ViewBox now properly handles pixel-padding around data items
  - ViewBox ignores bounds of zoom-rect when auto ranging
  - Fixed AxisItem artifacts
  - Fixed GraphicsItem.pixelVector caching bugs and simplified workaround for fp-precision errors
  - LinearRegionItem.hoverEvent obeys 'movable' flag                                                                                                                                         
  - Fixed PlotDataItem nan masking bugs                                                                                                                                                      
  - Workaround for segmentation fault in QPainter.drawPixmapFragments                                                                                                                        
  - multiprocess and RemoteGraphicsView work correctly in Windows.                                                                                                                           
  - Expanded python 3 support                                                                                                                                                                
  - Silenced weave errors by default                                                                                                                                                         
  - Fixed " 'win' in sys.platform " occurrences matching 'darwin' (duh)
  - Workaround for change in QImage API (PyQt 4.9.6)
  - Fixed axis ordering bug in GLScatterPlotItem
This commit is contained in:
Luke Campagnola 2013-02-14 08:29:11 -05:00
commit 2e79185d2f
65 changed files with 2451 additions and 430 deletions

View File

@ -10,5 +10,7 @@ Contents:
graphicsItems/index graphicsItems/index
widgets/index widgets/index
3dgraphics/index 3dgraphics/index
colormap
parametertree/index parametertree/index
graphicsscene/index graphicsscene/index
flowchart/index

8
doc/source/colormap.rst Normal file
View File

@ -0,0 +1,8 @@
ColorMap
========
.. autoclass:: pyqtgraph.ColorMap
:members:
.. automethod:: pyqtgraph.ColorMap.__init__

View File

@ -91,6 +91,8 @@ Mesh Generation Functions
Miscellaneous Functions Miscellaneous Functions
----------------------- -----------------------
.. autofunction:: pyqtgraph.arrayToQPath
.. autofunction:: pyqtgraph.pseudoScatter .. autofunction:: pyqtgraph.pseudoScatter
.. autofunction:: pyqtgraph.systemInfo .. autofunction:: pyqtgraph.systemInfo

View File

@ -0,0 +1,8 @@
GraphItem
=========
.. autoclass:: pyqtgraph.GraphItem
:members:
.. automethod:: pyqtgraph.GraphItem.__init__

View File

@ -12,6 +12,7 @@ Contents:
plotdataitem plotdataitem
plotitem plotitem
imageitem imageitem
graphitem
viewbox viewbox
linearregionitem linearregionitem
infiniteline infiniteline

View File

@ -15,6 +15,7 @@ Contents:
mouse_interaction mouse_interaction
how_to_use how_to_use
installation installation
qtcrashcourse
plotting plotting
images images
3dgraphics 3dgraphics

View File

@ -3,20 +3,76 @@ Qt Crash Course
Pyqtgraph makes extensive use of Qt for generating nearly all of its visual output and interfaces. Qt's documentation is very well written and we encourage all pyqtgraph developers to familiarize themselves with it. The purpose of this section is to provide an introduction to programming with Qt (using either PyQt or PySide) for the pyqtgraph developer. Pyqtgraph makes extensive use of Qt for generating nearly all of its visual output and interfaces. Qt's documentation is very well written and we encourage all pyqtgraph developers to familiarize themselves with it. The purpose of this section is to provide an introduction to programming with Qt (using either PyQt or PySide) for the pyqtgraph developer.
QWidgets and Layouts QWidgets and Layouts
-------------------- --------------------
A Qt GUI is almost always composed of a few basic components:
* A window. This is often provided by QMainWindow, but note that all QWidgets can be displayed in their window by simply calling widget.show() if the widget does not have a parent.
* Multiple QWidget instances such as QPushButton, QLabel, QComboBox, etc.
* QLayout instances (optional, but strongly encouraged) which automatically manage the positioning of widgets to allow the GUI to resize in a usable way.
Pyqtgraph fits into this scheme by providing its own QWidget subclasses to be inserted into your GUI.
Example::
from PyQt4 import QtGui # (the example applies equally well to PySide)
import pyqtgraph as pg
## Always start by initializing Qt (only once per application)
app = QtGui.QApplication([])
## Define a top-level widget to hold everything
w = QtGui.QWidget()
## Create some widgets to be placed inside
btn = QtGui.QPushButton('press me')
text = QtGui.QLineEdit('enter text')
listw = QtGui.QListWidget()
plot = pg.PlotWidget()
## Create a grid layout to manage the widgets size and position
layout = QtGui.QGridLayout()
w.setLayout(layout)
## Add widgets to the layout in their proper positions
layout.addWidget(btn, 0, 0) # button goes in upper-left
layout.addWidget(text, 1, 0) # text edit goes in middle-left
layout.addWidget(listw, 2, 0) # list widget goes in bottom-left
layout.addWidget(plot, 0, 1, 3, 1) # plot goes on right side, spanning 3 rows
## Display the widget as a new window
w.show()
## Start the Qt event loop
app.exec_()
More complex interfaces may be designed graphically using Qt Designer, which allows you to simply drag widgets into your window to define its appearance.
Naming Conventions
------------------
Virtually every class in pyqtgraph is an extension of base classes provided by Qt. When reading the documentation, remember that all of Qt's classes start with the letter 'Q', whereas pyqtgraph's classes do not. When reading through the methods for any class, it is often helpful to see which Qt base classes are used and look through the Qt documentation as well.
Most of Qt's classes define signals which can be difficult to tell apart from regular methods. Almost all signals explicity defined by pyqtgraph are named beginning with 'sig' to indicate that these signals are not defined at the Qt level.
In most cases, classes which end in 'Widget' are subclassed from QWidget and can therefore be used as a GUI element in a Qt window. Classes which end in 'Item' are subclasses of QGraphicsItem and can only be displayed within a QGraphicsView instance (such as GraphicsLayoutWidget or PlotWidget).
Signals, Slots, and Events Signals, Slots, and Events
-------------------------- --------------------------
[ to be continued.. please post a request on the pyqtgraph forum if you'd like to read more ]
GraphicsView and GraphicsItems GraphicsView and GraphicsItems
------------------------------ ------------------------------
Coordinate Systems Coordinate Systems and Transformations
------------------ --------------------------------------
Mouse and Keyboard Input Mouse and Keyboard Input
@ -26,3 +82,7 @@ Mouse and Keyboard Input
QTimer, the Event Loop, and Multi-Threading QTimer, the Event Loop, and Multi-Threading
------------------------------------------- -------------------------------------------
Multi-threading vs Multi-processing in Qt
-----------------------------------------

View File

@ -0,0 +1,12 @@
ColorMapWidget
==============
.. autoclass:: pyqtgraph.ColorMapWidget
:members:
.. automethod:: pyqtgraph.ColorMapWidget.__init__
.. automethod:: pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.setFields
.. automethod:: pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.map

View File

@ -17,6 +17,8 @@ Contents:
gradientwidget gradientwidget
histogramlutwidget histogramlutwidget
parametertree parametertree
colormapwidget
scatterplotwidget
graphicsview graphicsview
rawimagewidget rawimagewidget
datatreewidget datatreewidget

View File

@ -0,0 +1,8 @@
ScatterPlotWidget
=================
.. autoclass:: pyqtgraph.ScatterPlotWidget
:members:
.. automethod:: pyqtgraph.ScatterPlotWidget.__init__

32
examples/ErrorBarItem.py Normal file
View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
"""
Demonstrates basic use of ErrorBarItem
"""
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui
import numpy as np
import pyqtgraph as pg
import numpy as np
pg.setConfigOptions(antialias=True)
x = np.arange(10)
y = np.arange(10) %3
top = np.linspace(1.0, 3.0, 10)
bottom = np.linspace(2, 0.5, 10)
plt = pg.plot()
err = pg.ErrorBarItem(x=x, y=y, top=top, bottom=bottom, beam=0.5)
plt.addItem(err)
plt.plot(x, y, symbol='o', pen={'color': 0.8, 'width': 2})
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -62,7 +62,7 @@ w.addItem(p3)
## Animated example ## Animated example
## compute surface vertex data ## compute surface vertex data
cols = 100 cols = 90
rows = 100 rows = 100
x = np.linspace(-8, 8, cols+1).reshape(cols+1,1) x = np.linspace(-8, 8, cols+1).reshape(cols+1,1)
y = np.linspace(-8, 8, rows+1).reshape(1,rows+1) y = np.linspace(-8, 8, rows+1).reshape(1,rows+1)

63
examples/GraphItem.py Normal file
View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""
Simple example of GridItem use.
"""
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
w = pg.GraphicsWindow()
v = w.addViewBox()
v.setAspectLocked()
g = pg.GraphItem()
v.addItem(g)
## Define positions of nodes
pos = np.array([
[0,0],
[10,0],
[0,10],
[10,10],
[5,5],
[15,5]
])
## Define the set of connections in the graph
adj = np.array([
[0,1],
[1,3],
[3,2],
[2,0],
[1,5],
[3,5],
])
## Define the symbol to use for each node (this is optional)
symbols = ['o','o','o','o','t','+']
## Define the line style for each connection (this is optional)
lines = np.array([
(255,0,0,255,1),
(255,0,255,255,2),
(255,0,255,255,3),
(255,255,0,255,2),
(255,0,0,255,1),
(255,255,255,255,4),
], dtype=[('red',np.ubyte),('green',np.ubyte),('blue',np.ubyte),('alpha',np.ubyte),('width',float)])
## Update the graph
g.setData(pos=pos, adj=adj, pen=lines, size=1, symbol=symbols, pxMode=False)
## 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_()

42
examples/LogPlotTest.py Normal file
View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
## This example demonstrates many of the 2D plotting capabilities
## in pyqtgraph. All of the plots may be panned/scaled by dragging with
## the left/right mouse buttons. Right click on any plot to show a context menu.
import initExample ## Add path to library (just for examples; you do not need this)
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
#QtGui.QApplication.setGraphicsSystem('raster')
app = QtGui.QApplication([])
#mw = QtGui.QMainWindow()
#mw.resize(800,800)
win = pg.GraphicsWindow(title="Basic plotting examples")
win.resize(1000,600)
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')
p5.setLogMode(x=True, y=False)
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -59,7 +59,6 @@ pos = np.random.normal(size=(2,n), scale=1e-5)
spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)] spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)]
s2.addPoints(spots) s2.addPoints(spots)
w2.addItem(s2) w2.addItem(s2)
w2.setRange(s2.boundingRect())
s2.sigClicked.connect(clicked) s2.sigClicked.connect(clicked)
@ -71,7 +70,7 @@ s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to tr
spots3 = [] spots3 = []
for i in range(10): for i in range(10):
for j in range(10): for j in range(10):
spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'brush':pg.intColor(i*10+j, 100)}) spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'pen': {'color': 'w', 'width': 2}, 'brush':pg.intColor(i*10+j, 100)})
s3.addPoints(spots3) s3.addPoints(spots3)
w3.addItem(s3) w3.addItem(s3)
s3.sigClicked.connect(clicked) s3.sigClicked.connect(clicked)

12
examples/SimplePlot.py Normal file
View File

@ -0,0 +1,12 @@
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 numpy as np
pg.plot(np.random.normal(size=100000), title="Simplest possible plotting example")
## 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'):
pg.QtGui.QApplication.exec_()

View File

@ -27,6 +27,8 @@ examples = OrderedDict([
('Scatter Plot', 'ScatterPlot.py'), ('Scatter Plot', 'ScatterPlot.py'),
#('PlotItem', 'PlotItem.py'), #('PlotItem', 'PlotItem.py'),
('IsocurveItem', 'isocurve.py'), ('IsocurveItem', 'isocurve.py'),
('GraphItem', 'GraphItem.py'),
('ErrorBarItem', 'ErrorBarItem.py'),
('ImageItem - video', 'ImageItem.py'), ('ImageItem - video', 'ImageItem.py'),
('ImageItem - draw', 'Draw.py'), ('ImageItem - draw', 'Draw.py'),
('Region-of-Interest', 'ROIExamples.py'), ('Region-of-Interest', 'ROIExamples.py'),

View File

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
app = pg.mkQApp()
plt = pg.PlotWidget()
app.processEvents()
## Putting this at the beginning or end does not have much effect
plt.show()
## The auto-range is recomputed after each item is added,
## so disabling it before plotting helps
plt.enableAutoRange(False, False)
def plot():
start = pg.ptime.time()
n = 15
pts = 100
x = np.linspace(0, 0.8, pts)
y = np.random.random(size=pts)*0.8
for i in xrange(n):
for j in xrange(n):
## calling PlotWidget.plot() generates a PlotDataItem, which
## has a bit more overhead than PlotCurveItem, which is all
## we need here. This overhead adds up quickly and makes a big
## difference in speed.
#plt.plot(x=x+i, y=y+j)
plt.addItem(pg.PlotCurveItem(x=x+i, y=y+j))
#path = pg.arrayToQPath(x+i, y+j)
#item = QtGui.QGraphicsPathItem(path)
#item.setPen(pg.mkPen('w'))
#plt.addItem(item)
dt = pg.ptime.time() - start
print "Create plots took: %0.3fms" % (dt*1000)
## Plot and clear 5 times, printing the time it took
for i in range(5):
plt.clear()
plot()
app.processEvents()
plt.autoRange()
def fastPlot():
## Different approach: generate a single item with all data points.
## This runs about 20x faster.
start = pg.ptime.time()
n = 15
pts = 100
x = np.linspace(0, 0.8, pts)
y = np.random.random(size=pts)*0.8
xdata = np.empty((n, n, pts))
xdata[:] = x.reshape(1,1,pts) + np.arange(n).reshape(n,1,1)
ydata = np.empty((n, n, pts))
ydata[:] = y.reshape(1,1,pts) + np.arange(n).reshape(1,n,1)
conn = np.ones((n*n,pts))
conn[:,-1] = False # make sure plots are disconnected
path = pg.arrayToQPath(xdata.flatten(), ydata.flatten(), conn.flatten())
item = QtGui.QGraphicsPathItem(path)
item.setPen(pg.mkPen('w'))
plt.addItem(item)
dt = pg.ptime.time() - start
print "Create plots took: %0.3fms" % (dt*1000)
## Plot and clear 5 times, printing the time it took
if hasattr(pg, 'arrayToQPath'):
for i in range(5):
plt.clear()
fastPlot()
app.processEvents()
else:
print "Skipping fast tests--arrayToQPath function is missing."
plt.autoRange()
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -8,32 +8,32 @@ import time
print "\n=================\nStart Process" print("\n=================\nStart Process")
proc = mp.Process() proc = mp.Process()
import os import os
print "parent:", os.getpid(), "child:", proc.proc.pid print("parent:", os.getpid(), "child:", proc.proc.pid)
print "started" print("started")
rnp = proc._import('numpy') rnp = proc._import('numpy')
arr = rnp.array([1,2,3,4]) arr = rnp.array([1,2,3,4])
print repr(arr) print(repr(arr))
print str(arr) print(str(arr))
print "return value:", repr(arr.mean(_returnType='value')) print("return value:", repr(arr.mean(_returnType='value')))
print "return proxy:", repr(arr.mean(_returnType='proxy')) print( "return proxy:", repr(arr.mean(_returnType='proxy')))
print "return auto: ", repr(arr.mean(_returnType='auto')) print( "return auto: ", repr(arr.mean(_returnType='auto')))
proc.join() proc.join()
print "process finished" print( "process finished")
print "\n=================\nStart ForkedProcess" print( "\n=================\nStart ForkedProcess")
proc = mp.ForkedProcess() proc = mp.ForkedProcess()
rnp = proc._import('numpy') rnp = proc._import('numpy')
arr = rnp.array([1,2,3,4]) arr = rnp.array([1,2,3,4])
print repr(arr) print( repr(arr))
print str(arr) print( str(arr))
print repr(arr.mean()) print( repr(arr.mean()))
proc.join() proc.join()
print "process finished" print( "process finished")
@ -42,10 +42,10 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
app = pg.QtGui.QApplication([]) app = pg.QtGui.QApplication([])
print "\n=================\nStart QtProcess" print( "\n=================\nStart QtProcess")
import sys import sys
if (sys.flags.interactive != 1): if (sys.flags.interactive != 1):
print " (not interactive; remote process will exit immediately.)" print( " (not interactive; remote process will exit immediately.)")
proc = mp.QtProcess() proc = mp.QtProcess()
d1 = proc.transfer(np.random.normal(size=1000)) d1 = proc.transfer(np.random.normal(size=1000))
d2 = proc.transfer(np.random.normal(size=1000)) d2 = proc.transfer(np.random.normal(size=1000))

View File

@ -5,7 +5,7 @@ import pyqtgraph.multiprocess as mp
import pyqtgraph as pg import pyqtgraph as pg
import time import time
print "\n=================\nParallelize" print( "\n=================\nParallelize")
## Do a simple task: ## Do a simple task:
## for x in range(N): ## for x in range(N):
@ -36,7 +36,7 @@ with pg.ProgressDialog('processing serially..', maximum=len(tasks)) as dlg:
dlg += 1 dlg += 1
if dlg.wasCanceled(): if dlg.wasCanceled():
raise Exception('processing canceled') raise Exception('processing canceled')
print "Serial time: %0.2f" % (time.time() - start) print( "Serial time: %0.2f" % (time.time() - start))
### Use parallelize, but force a single worker ### Use parallelize, but force a single worker
### (this simulates the behavior seen on windows, which lacks os.fork) ### (this simulates the behavior seen on windows, which lacks os.fork)
@ -47,8 +47,8 @@ with mp.Parallelize(enumerate(tasks), results=results2, workers=1, progressDialo
for j in xrange(size): for j in xrange(size):
tot += j * x tot += j * x
tasker.results[i] = tot tasker.results[i] = tot
print "\nParallel time, 1 worker: %0.2f" % (time.time() - start) print( "\nParallel time, 1 worker: %0.2f" % (time.time() - start))
print "Results match serial: ", results2 == results print( "Results match serial: %s" % str(results2 == results))
### Use parallelize with multiple workers ### Use parallelize with multiple workers
start = time.time() start = time.time()
@ -58,6 +58,6 @@ with mp.Parallelize(enumerate(tasks), results=results3, progressDialog='processi
for j in xrange(size): for j in xrange(size):
tot += j * x tot += j * x
tasker.results[i] = tot tasker.results[i] = tot
print "\nParallel time, %d workers: %0.2f" % (mp.Parallelize.suggestedWorkerCount(), time.time() - start) print( "\nParallel time, %d workers: %0.2f" % (mp.Parallelize.suggestedWorkerCount(), time.time() - start))
print "Results match serial: ", results3 == results print( "Results match serial: %s" % str(results3 == results))

View File

@ -70,6 +70,7 @@ params = [
{'name': 'Named List', 'type': 'list', 'values': {"one": 1, "two": 2, "three": 3}, 'value': 2}, {'name': 'Named List', 'type': 'list', 'values': {"one": 1, "two": 2, "three": 3}, 'value': 2},
{'name': 'Boolean', 'type': 'bool', 'value': True, 'tip': "This is a checkbox"}, {'name': 'Boolean', 'type': 'bool', 'value': True, 'tip': "This is a checkbox"},
{'name': 'Color', 'type': 'color', 'value': "FF0", 'tip': "This is a color button"}, {'name': 'Color', 'type': 'color', 'value': "FF0", 'tip': "This is a color button"},
{'name': 'Gradient', 'type': 'colormap'},
{'name': 'Subgroup', 'type': 'group', 'children': [ {'name': 'Subgroup', 'type': 'group', 'children': [
{'name': 'Sub-param 1', 'type': 'int', 'value': 10}, {'name': 'Sub-param 1', 'type': 'int', 'value': 10},
{'name': 'Sub-param 2', 'type': 'float', 'value': 1.2e6}, {'name': 'Sub-param 2', 'type': 'float', 'value': 1.2e6},

View File

@ -117,7 +117,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
def render(self, *args): def render(self, *args):
self.prepareForPaint() self.prepareForPaint()
return QGraphicsScene.render(self, *args) return QtGui.QGraphicsScene.render(self, *args)
def prepareForPaint(self): def prepareForPaint(self):
"""Called before every render. This method will inform items that the scene is about to """Called before every render. This method will inform items that the scene is about to

View File

@ -27,6 +27,7 @@ class ExportDialog(QtGui.QWidget):
self.ui.closeBtn.clicked.connect(self.close) self.ui.closeBtn.clicked.connect(self.close)
self.ui.exportBtn.clicked.connect(self.exportClicked) self.ui.exportBtn.clicked.connect(self.exportClicked)
self.ui.copyBtn.clicked.connect(self.copyClicked)
self.ui.itemTree.currentItemChanged.connect(self.exportItemChanged) self.ui.itemTree.currentItemChanged.connect(self.exportItemChanged)
self.ui.formatList.currentItemChanged.connect(self.exportFormatChanged) self.ui.formatList.currentItemChanged.connect(self.exportFormatChanged)
@ -116,11 +117,16 @@ class ExportDialog(QtGui.QWidget):
else: else:
self.ui.paramTree.setParameters(params) self.ui.paramTree.setParameters(params)
self.currentExporter = exp self.currentExporter = exp
self.ui.copyBtn.setEnabled(exp.allowCopy)
def exportClicked(self): def exportClicked(self):
self.selectBox.hide() self.selectBox.hide()
self.currentExporter.export() self.currentExporter.export()
def copyClicked(self):
self.selectBox.hide()
self.currentExporter.export(copy=True)
def close(self): def close(self):
self.selectBox.setVisible(False) self.selectBox.setVisible(False)
self.setVisible(False) self.setVisible(False)

View File

@ -79,6 +79,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0">
<widget class="QPushButton" name="copyBtn">
<property name="text">
<string>Copy</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<customwidgets> <customwidgets>

View File

@ -2,8 +2,8 @@
# Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui' # Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui'
# #
# Created: Sun Sep 9 14:41:31 2012 # Created: Wed Jan 30 21:02:28 2013
# by: PyQt4 UI code generator 4.9.1 # by: PyQt4 UI code generator 4.9.3
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -49,6 +49,9 @@ class Ui_Form(object):
self.label_3 = QtGui.QLabel(Form) self.label_3 = QtGui.QLabel(Form)
self.label_3.setObjectName(_fromUtf8("label_3")) self.label_3.setObjectName(_fromUtf8("label_3"))
self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3)
self.copyBtn = QtGui.QPushButton(Form)
self.copyBtn.setObjectName(_fromUtf8("copyBtn"))
self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1)
self.retranslateUi(Form) self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form) QtCore.QMetaObject.connectSlotsByName(Form)
@ -60,5 +63,6 @@ class Ui_Form(object):
self.exportBtn.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8)) self.exportBtn.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8))
self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8)) self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8))
self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8)) self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8))
self.copyBtn.setText(QtGui.QApplication.translate("Form", "Copy", None, QtGui.QApplication.UnicodeUTF8))
from pyqtgraph.parametertree import ParameterTree from pyqtgraph.parametertree import ParameterTree

View File

@ -2,8 +2,8 @@
# Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui' # Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui'
# #
# Created: Sun Sep 9 14:41:31 2012 # Created: Wed Jan 30 21:02:28 2013
# by: pyside-uic 0.2.13 running on PySide 1.1.0 # by: pyside-uic 0.2.13 running on PySide 1.1.1
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -44,6 +44,9 @@ class Ui_Form(object):
self.label_3 = QtGui.QLabel(Form) self.label_3 = QtGui.QLabel(Form)
self.label_3.setObjectName("label_3") self.label_3.setObjectName("label_3")
self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3)
self.copyBtn = QtGui.QPushButton(Form)
self.copyBtn.setObjectName("copyBtn")
self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1)
self.retranslateUi(Form) self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form) QtCore.QMetaObject.connectSlotsByName(Form)
@ -55,5 +58,6 @@ class Ui_Form(object):
self.exportBtn.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8)) self.exportBtn.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8))
self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8)) self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8))
self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8)) self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8))
self.copyBtn.setText(QtGui.QApplication.translate("Form", "Copy", None, QtGui.QApplication.UnicodeUTF8))
from pyqtgraph.parametertree import ParameterTree from pyqtgraph.parametertree import ParameterTree

55
pyqtgraph/PlotData.py Normal file
View File

@ -0,0 +1,55 @@
class PlotData(object):
"""
Class used for managing plot data
- allows data sharing between multiple graphics items (curve, scatter, graph..)
- each item may define the columns it needs
- column groupings ('pos' or x, y, z)
- efficiently appendable
- log, fft transformations
- color mode conversion (float/byte/qcolor)
- pen/brush conversion
- per-field cached masking
- allows multiple masking fields (different graphics need to mask on different criteria)
- removal of nan/inf values
- option for single value shared by entire column
- cached downsampling
"""
def __init__(self):
self.fields = {}
self.maxVals = {} ## cache for max/min
self.minVals = {}
def addFields(self, fields):
for f in fields:
if f not in self.fields:
self.fields[f] = None
def hasField(self, f):
return f in self.fields
def __getitem__(self, field):
return self.fields[field]
def __setitem__(self, field, val):
self.fields[field] = val
def max(self, field):
mx = self.maxVals.get(field, None)
if mx is None:
mx = np.max(self[field])
self.maxVals[field] = mx
return mx
def min(self, field):
mn = self.minVals.get(field, None)
if mn is None:
mn = np.min(self[field])
self.minVals[field] = mn
return mn

View File

@ -16,6 +16,9 @@ from .Qt import QtGui
#if QtGui.QApplication.instance() is None: #if QtGui.QApplication.instance() is None:
#app = QtGui.QApplication([]) #app = QtGui.QApplication([])
import numpy ## pyqtgraph requires numpy
## (import here to avoid massive error dump later on if numpy is not available)
import os, sys import os, sys
## check python version ## check python version
@ -49,6 +52,8 @@ CONFIG_OPTIONS = {
'background': (0, 0, 0), ## default background for GraphicsWidget 'background': (0, 0, 0), ## default background for GraphicsWidget
'antialias': False, 'antialias': False,
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets 'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
'useWeave': True, ## Use weave to speed up some operations, if it is available
'weaveDebug': False, ## Print full error message if weave compile fails
} }
@ -182,6 +187,7 @@ from .SRTTransform3D import SRTTransform3D
from .functions import * from .functions import *
from .graphicsWindows import * from .graphicsWindows import *
from .SignalProxy import * from .SignalProxy import *
from .colormap import *
from .ptime import time from .ptime import time

239
pyqtgraph/colormap.py Normal file
View File

@ -0,0 +1,239 @@
import numpy as np
import scipy.interpolate
from pyqtgraph.Qt import QtGui, QtCore
class ColorMap(object):
"""
A ColorMap defines a relationship between a scalar value and a range of colors.
ColorMaps are commonly used for false-coloring monochromatic images, coloring
scatter-plot points, and coloring surface plots by height.
Each color map is defined by a set of colors, each corresponding to a
particular scalar value. For example:
| 0.0 -> black
| 0.2 -> red
| 0.6 -> yellow
| 1.0 -> white
The colors for intermediate values are determined by interpolating between
the two nearest colors in either RGB or HSV color space.
To provide user-defined color mappings, see :class:`GradientWidget <pyqtgraph.GradientWidget>`.
"""
## color interpolation modes
RGB = 1
HSV_POS = 2
HSV_NEG = 3
## boundary modes
CLIP = 1
REPEAT = 2
MIRROR = 3
## return types
BYTE = 1
FLOAT = 2
QCOLOR = 3
enumMap = {
'rgb': RGB,
'hsv+': HSV_POS,
'hsv-': HSV_NEG,
'clip': CLIP,
'repeat': REPEAT,
'mirror': MIRROR,
'byte': BYTE,
'float': FLOAT,
'qcolor': QCOLOR,
}
def __init__(self, pos, color, mode=None):
"""
========= ==============================================================
Arguments
pos Array of positions where each color is defined
color Array of RGBA colors.
Integer data types are interpreted as 0-255; float data types
are interpreted as 0.0-1.0
mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG)
indicating the color space that should be used when
interpolating between stops. Note that the last mode value is
ignored. By default, the mode is entirely RGB.
========= ==============================================================
"""
self.pos = pos
self.color = color
if mode is None:
mode = np.ones(len(pos))
self.mode = mode
self.stopsCache = {}
def map(self, data, mode='byte'):
"""
Return an array of colors corresponding to the values in *data*.
Data must be either a scalar position or an array (any shape) of positions.
The *mode* argument determines the type of data returned:
=========== ===============================================================
byte (default) Values are returned as 0-255 unsigned bytes.
float Values are returned as 0.0-1.0 floats.
qcolor Values are returned as an array of QColor objects.
=========== ===============================================================
"""
if isinstance(mode, basestring):
mode = self.enumMap[mode.lower()]
if mode == self.QCOLOR:
pos, color = self.getStops(self.BYTE)
else:
pos, color = self.getStops(mode)
data = np.clip(data, pos.min(), pos.max())
if not isinstance(data, np.ndarray):
interp = scipy.interpolate.griddata(pos, color, np.array([data]))[0]
else:
interp = scipy.interpolate.griddata(pos, color, data)
if mode == self.QCOLOR:
if not isinstance(data, np.ndarray):
return QtGui.QColor(*interp)
else:
return [QtGui.QColor(*x) for x in interp]
else:
return interp
def mapToQColor(self, data):
"""Convenience function; see :func:`map() <pyqtgraph.ColorMap.map>`."""
return self.map(data, mode=self.QCOLOR)
def mapToByte(self, data):
"""Convenience function; see :func:`map() <pyqtgraph.ColorMap.map>`."""
return self.map(data, mode=self.BYTE)
def mapToFloat(self, data):
"""Convenience function; see :func:`map() <pyqtgraph.ColorMap.map>`."""
return self.map(data, mode=self.FLOAT)
def getGradient(self, p1=None, p2=None):
"""Return a QLinearGradient object spanning from QPoints p1 to p2."""
if p1 == None:
p1 = QtCore.QPointF(0,0)
if p2 == None:
p2 = QtCore.QPointF(self.pos.max()-self.pos.min(),0)
g = QtGui.QLinearGradient(p1, p2)
pos, color = self.getStops(mode=self.BYTE)
color = [QtGui.QColor(*x) for x in color]
g.setStops(zip(pos, color))
#if self.colorMode == 'rgb':
#ticks = self.listTicks()
#g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks])
#elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop
#ticks = self.listTicks()
#stops = []
#stops.append((ticks[0][1], ticks[0][0].color))
#for i in range(1,len(ticks)):
#x1 = ticks[i-1][1]
#x2 = ticks[i][1]
#dx = (x2-x1) / 10.
#for j in range(1,10):
#x = x1 + dx*j
#stops.append((x, self.getColor(x)))
#stops.append((x2, self.getColor(x2)))
#g.setStops(stops)
return g
def getColors(self, mode=None):
"""Return list of all color stops converted to the specified mode.
If mode is None, then no conversion is done."""
if isinstance(mode, basestring):
mode = self.enumMap[mode.lower()]
color = self.color
if mode in [self.BYTE, self.QCOLOR] and color.dtype.kind == 'f':
color = (color * 255).astype(np.ubyte)
elif mode == self.FLOAT and color.dtype.kind != 'f':
color = color.astype(float) / 255.
if mode == self.QCOLOR:
color = [QtGui.QColor(*x) for x in color]
return color
def getStops(self, mode):
## Get fully-expanded set of RGBA stops in either float or byte mode.
if mode not in self.stopsCache:
color = self.color
if mode == self.BYTE and color.dtype.kind == 'f':
color = (color * 255).astype(np.ubyte)
elif mode == self.FLOAT and color.dtype.kind != 'f':
color = color.astype(float) / 255.
## to support HSV mode, we need to do a little more work..
#stops = []
#for i in range(len(self.pos)):
#pos = self.pos[i]
#color = color[i]
#imode = self.mode[i]
#if imode == self.RGB:
#stops.append((x,color))
#else:
#ns =
self.stopsCache[mode] = (self.pos, color)
return self.stopsCache[mode]
def getLookupTable(self, start=0.0, stop=1.0, nPts=512, alpha=None, mode='byte'):
"""
Return an RGB(A) lookup table (ndarray).
============= ============================================================================
**Arguments**
start The starting value in the lookup table (default=0.0)
stop The final value in the lookup table (default=1.0)
nPts The number of points in the returned lookup table.
alpha True, False, or None - Specifies whether or not alpha values are included
in the table. If alpha is None, it will be automatically determined.
mode Determines return type: 'byte' (0-255), 'float' (0.0-1.0), or 'qcolor'.
See :func:`map() <pyqtgraph.ColorMap.map>`.
============= ============================================================================
"""
if isinstance(mode, basestring):
mode = self.enumMap[mode.lower()]
if alpha is None:
alpha = self.usesAlpha()
x = np.linspace(start, stop, nPts)
table = self.map(x, mode)
if not alpha:
return table[:,:3]
else:
return table
def usesAlpha(self):
"""Return True if any stops have an alpha < 255"""
max = 1.0 if self.color.dtype.kind == 'f' else 255
return np.any(self.color[:,3] != max)
def isMapTrivial(self):
"""
Return True if the gradient has exactly two stops in it: black at 0.0 and white at 1.0.
"""
if len(self.pos) != 2:
return False
if self.pos[0] != 0.0 or self.pos[1] != 1.0:
return False
if self.color.dtype.kind == 'f':
return np.all(self.color == np.array([[0.,0.,0.,1.], [1.,1.,1.,1.]]))
else:
return np.all(self.color == np.array([[0,0,0,255], [255,255,255,255]]))

View File

@ -393,7 +393,7 @@ class Profiler:
if self.delayed: if self.delayed:
self.msgs.append(msg2) self.msgs.append(msg2)
else: else:
print msg2 print(msg2)
self.t0 = ptime.time() self.t0 = ptime.time()
self.t1 = self.t0 self.t1 = self.t0
@ -410,7 +410,7 @@ class Profiler:
if self.delayed: if self.delayed:
self.msgs.append(msg2) self.msgs.append(msg2)
else: else:
print msg2 print(msg2)
self.t1 = ptime.time() ## don't measure time it took to print self.t1 = ptime.time() ## don't measure time it took to print
def finish(self, msg=None): def finish(self, msg=None):
@ -425,10 +425,10 @@ class Profiler:
self.msgs.append(msg) self.msgs.append(msg)
if self.depth == 0: if self.depth == 0:
for line in self.msgs: for line in self.msgs:
print line print(line)
Profiler.msgs = [] Profiler.msgs = []
else: else:
print msg print(msg)
Profiler.depth = self.depth Profiler.depth = self.depth
self.finished = True self.finished = True
@ -917,3 +917,21 @@ def qObjectReport(verbose=False):
for t in typs: for t in typs:
print(count[t], "\t", t) print(count[t], "\t", t)
class PrintDetector(object):
def __init__(self):
self.stdout = sys.stdout
sys.stdout = self
def remove(self):
sys.stdout = self.stdout
def __del__(self):
self.remove()
def write(self, x):
self.stdout.write(x)
traceback.print_stack()
def flush(self):
self.stdout.flush()

View File

@ -9,7 +9,8 @@ class Exporter(object):
""" """
Abstract class used for exporting graphics to file / printer / whatever. Abstract class used for exporting graphics to file / printer / whatever.
""" """
allowCopy = False # subclasses set this to True if they can use the copy buffer
def __init__(self, item): def __init__(self, item):
""" """
Initialize with the item to be exported. Initialize with the item to be exported.
@ -25,10 +26,11 @@ class Exporter(object):
"""Return the parameters used to configure this exporter.""" """Return the parameters used to configure this exporter."""
raise Exception("Abstract method must be overridden in subclass.") raise Exception("Abstract method must be overridden in subclass.")
def export(self, fileName=None, toBytes=False): def export(self, fileName=None, toBytes=False, copy=False):
""" """
If *fileName* is None, pop-up a file dialog. If *fileName* is None, pop-up a file dialog.
If *toString* is True, return a bytes object rather than writing to file. If *toBytes* is True, return a bytes object rather than writing to file.
If *copy* is True, export to the copy buffer rather than writing to file.
""" """
raise Exception("Abstract method must be overridden in subclass.") raise Exception("Abstract method must be overridden in subclass.")
@ -64,7 +66,7 @@ class Exporter(object):
if selectedExt is not None: if selectedExt is not None:
selectedExt = selectedExt.groups()[0].lower() selectedExt = selectedExt.groups()[0].lower()
if ext != selectedExt: if ext != selectedExt:
fileName = fileName + selectedExt fileName = fileName + '.' + selectedExt.lstrip('.')
self.export(fileName=fileName, **self.fileDialog.opts) self.export(fileName=fileName, **self.fileDialog.opts)

View File

@ -8,6 +8,8 @@ __all__ = ['ImageExporter']
class ImageExporter(Exporter): class ImageExporter(Exporter):
Name = "Image File (PNG, TIF, JPG, ...)" Name = "Image File (PNG, TIF, JPG, ...)"
allowCopy = True
def __init__(self, item): def __init__(self, item):
Exporter.__init__(self, item) Exporter.__init__(self, item)
tr = self.getTargetRect() tr = self.getTargetRect()
@ -38,8 +40,8 @@ class ImageExporter(Exporter):
def parameters(self): def parameters(self):
return self.params return self.params
def export(self, fileName=None): def export(self, fileName=None, toBytes=False, copy=False):
if fileName is None: if fileName is None and not toBytes and not copy:
filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()] filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()]
preferred = ['*.png', '*.tif', '*.jpg'] preferred = ['*.png', '*.tif', '*.jpg']
for p in preferred[::-1]: for p in preferred[::-1]:
@ -78,6 +80,12 @@ class ImageExporter(Exporter):
finally: finally:
self.setExportMode(False) self.setExportMode(False)
painter.end() painter.end()
self.png.save(fileName)
if copy:
QtGui.QApplication.clipboard().setImage(self.png)
elif toBytes:
return self.png
else:
self.png.save(fileName)

View File

@ -11,6 +11,8 @@ __all__ = ['SVGExporter']
class SVGExporter(Exporter): class SVGExporter(Exporter):
Name = "Scalable Vector Graphics (SVG)" Name = "Scalable Vector Graphics (SVG)"
allowCopy=True
def __init__(self, item): def __init__(self, item):
Exporter.__init__(self, item) Exporter.__init__(self, item)
#tr = self.getTargetRect() #tr = self.getTargetRect()
@ -37,8 +39,8 @@ class SVGExporter(Exporter):
def parameters(self): def parameters(self):
return self.params return self.params
def export(self, fileName=None, toBytes=False): def export(self, fileName=None, toBytes=False, copy=False):
if toBytes is False and fileName is None: if toBytes is False and copy is False and fileName is None:
self.fileSaveDialog(filter="Scalable Vector Graphics (*.svg)") self.fileSaveDialog(filter="Scalable Vector Graphics (*.svg)")
return return
#self.svg = QtSvg.QSvgGenerator() #self.svg = QtSvg.QSvgGenerator()
@ -83,11 +85,16 @@ class SVGExporter(Exporter):
xml = generateSvg(self.item) xml = generateSvg(self.item)
if toBytes: if toBytes:
return bytes(xml) return xml.encode('UTF-8')
elif copy:
md = QtCore.QMimeData()
md.setData('image/svg+xml', QtCore.QByteArray(xml.encode('UTF-8')))
QtGui.QApplication.clipboard().setMimeData(md)
else: else:
with open(fileName, 'w') as fh: with open(fileName, 'w') as fh:
fh.write(xml.encode('UTF-8')) fh.write(xml.encode('UTF-8'))
xmlHeader = """\ xmlHeader = """\
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny">
@ -148,7 +155,7 @@ def _generateItemSvg(item, nodes=None, root=None):
## ##
## Both 2 and 3 can be addressed by drawing all items in world coordinates. ## Both 2 and 3 can be addressed by drawing all items in world coordinates.
prof = pg.debug.Profiler('generateItemSvg %s' % str(item), disabled=True)
if nodes is None: ## nodes maps all node IDs to their XML element. if nodes is None: ## nodes maps all node IDs to their XML element.
## this allows us to ensure all elements receive unique names. ## this allows us to ensure all elements receive unique names.
@ -170,8 +177,12 @@ def _generateItemSvg(item, nodes=None, root=None):
tr = QtGui.QTransform() tr = QtGui.QTransform()
if isinstance(item, QtGui.QGraphicsScene): if isinstance(item, QtGui.QGraphicsScene):
xmlStr = "<g>\n</g>\n" xmlStr = "<g>\n</g>\n"
childs = [i for i in item.items() if i.parentItem() is None]
doc = xml.parseString(xmlStr) doc = xml.parseString(xmlStr)
childs = [i for i in item.items() if i.parentItem() is None]
elif item.__class__.paint == QtGui.QGraphicsItem.paint:
xmlStr = "<g>\n</g>\n"
doc = xml.parseString(xmlStr)
childs = item.childItems()
else: else:
childs = item.childItems() childs = item.childItems()
tr = itemTransform(item, item.scene()) tr = itemTransform(item, item.scene())
@ -220,14 +231,15 @@ def _generateItemSvg(item, nodes=None, root=None):
## get list of sub-groups ## get list of sub-groups
g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g'] g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g']
except: except:
print doc.toxml() print(doc.toxml())
raise raise
prof.mark('render')
## Get rid of group transformation matrices by applying ## Get rid of group transformation matrices by applying
## transformation to inner coordinates ## transformation to inner coordinates
correctCoordinates(g1, item) correctCoordinates(g1, item)
prof.mark('correct')
## make sure g1 has the transformation matrix ## make sure g1 has the transformation matrix
#m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32()) #m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32())
#g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m) #g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m)
@ -277,6 +289,8 @@ def _generateItemSvg(item, nodes=None, root=None):
childGroup = g1.ownerDocument.createElement('g') childGroup = g1.ownerDocument.createElement('g')
childGroup.setAttribute('clip-path', 'url(#%s)' % clip) childGroup.setAttribute('clip-path', 'url(#%s)' % clip)
g1.appendChild(childGroup) g1.appendChild(childGroup)
prof.mark('clipping')
## Add all child items as sub-elements. ## Add all child items as sub-elements.
childs.sort(key=lambda c: c.zValue()) childs.sort(key=lambda c: c.zValue())
for ch in childs: for ch in childs:
@ -284,7 +298,8 @@ def _generateItemSvg(item, nodes=None, root=None):
if cg is None: if cg is None:
continue continue
childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now) childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now)
prof.mark('children')
prof.finish()
return g1 return g1
def correctCoordinates(node, item): def correctCoordinates(node, item):

View File

@ -236,7 +236,7 @@ class EvalNode(Node):
text = fn + "\n".join([" "+l for l in str(self.text.toPlainText()).split('\n')]) + run text = fn + "\n".join([" "+l for l in str(self.text.toPlainText()).split('\n')]) + run
exec(text) exec(text)
except: except:
print "Error processing node:", self.name() print("Error processing node: %s" % self.name())
raise raise
return output return output

View File

@ -23,16 +23,19 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY'
from .Qt import QtGui, QtCore, USE_PYSIDE from .Qt import QtGui, QtCore, USE_PYSIDE
from pyqtgraph import getConfigOption
import numpy as np import numpy as np
import decimal, re import decimal, re
import ctypes import ctypes
import sys, struct
try: try:
import scipy.ndimage import scipy.ndimage
HAVE_SCIPY = True HAVE_SCIPY = True
WEAVE_DEBUG = getConfigOption('weaveDebug')
try: try:
import scipy.weave import scipy.weave
USE_WEAVE = True USE_WEAVE = getConfigOption('useWeave')
except: except:
USE_WEAVE = False USE_WEAVE = False
except ImportError: except ImportError:
@ -563,8 +566,8 @@ def transformCoordinates(tr, coords, transpose=False):
def solve3DTransform(points1, points2): def solve3DTransform(points1, points2):
""" """
Find a 3D transformation matrix that maps points1 onto points2 Find a 3D transformation matrix that maps points1 onto points2.
points must be specified as a list of 4 Vectors. Points must be specified as a list of 4 Vectors.
""" """
if not HAVE_SCIPY: if not HAVE_SCIPY:
raise Exception("This function depends on the scipy library, but it does not appear to be importable.") raise Exception("This function depends on the scipy library, but it does not appear to be importable.")
@ -580,8 +583,8 @@ def solve3DTransform(points1, points2):
def solveBilinearTransform(points1, points2): def solveBilinearTransform(points1, points2):
""" """
Find a bilinear transformation matrix (2x4) that maps points1 onto points2 Find a bilinear transformation matrix (2x4) that maps points1 onto points2.
points must be specified as a list of 4 Vector, Point, QPointF, etc. Points must be specified as a list of 4 Vector, Point, QPointF, etc.
To use this matrix to map a point [x,y]:: To use this matrix to map a point [x,y]::
@ -631,7 +634,8 @@ def rescaleData(data, scale, offset, dtype=None):
data = newData.reshape(data.shape) data = newData.reshape(data.shape)
except: except:
if USE_WEAVE: if USE_WEAVE:
debug.printExc("Error; disabling weave.") if WEAVE_DEBUG:
debug.printExc("Error; disabling weave.")
USE_WEAVE = False USE_WEAVE = False
#p = np.poly1d([scale, -offset*scale]) #p = np.poly1d([scale, -offset*scale])
@ -795,7 +799,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
if levels.shape != (data.shape[-1], 2): if levels.shape != (data.shape[-1], 2):
raise Exception('levels must have shape (data.shape[-1], 2)') raise Exception('levels must have shape (data.shape[-1], 2)')
else: else:
print levels print(levels)
raise Exception("levels argument must be 1D or 2D.") raise Exception("levels argument must be 1D or 2D.")
#levels = np.array(levels) #levels = np.array(levels)
#if levels.shape == (2,): #if levels.shape == (2,):
@ -947,8 +951,15 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
ch = ctypes.c_char.from_buffer(imgData, 0) ch = ctypes.c_char.from_buffer(imgData, 0)
img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat)
else: else:
addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0)) #addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0))
img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) ## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was)
## So we first attempt the 4.9.6 API, then fall back to 4.9.3
addr = ctypes.c_char.from_buffer(imgData, 0)
try:
img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
except TypeError:
addr = ctypes.addressof(addr)
img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
img.data = imgData img.data = imgData
return img return img
#try: #try:
@ -1038,6 +1049,97 @@ def colorToAlpha(data, color):
def arrayToQPath(x, y, connect='all'):
"""Convert an array of x,y coordinats to QPainterPath as efficiently as possible.
The *connect* argument may be 'all', indicating that each point should be
connected to the next; 'pairs', indicating that each pair of points
should be connected, or an array of int32 values (0 or 1) indicating
connections.
"""
## Create all vertices in path. The method used below creates a binary format so that all
## vertices can be read in at once. This binary format may change in future versions of Qt,
## so the original (slower) method is left here for emergencies:
#path.moveTo(x[0], y[0])
#for i in range(1, y.shape[0]):
# path.lineTo(x[i], y[i])
## Speed this up using >> operator
## Format is:
## numVerts(i4) 0(i4)
## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect
## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex
## ...
## 0(i4)
##
## All values are big endian--pack using struct.pack('>d') or struct.pack('>i')
path = QtGui.QPainterPath()
#prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True)
if sys.version_info[0] == 2: ## So this is disabled for python 3... why??
n = x.shape[0]
# create empty array, pad with extra space on either end
arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')])
# write first two integers
#prof.mark('allocate empty')
arr.data[12:20] = struct.pack('>ii', n, 0)
#prof.mark('pack header')
# Fill array with vertex values
arr[1:-1]['x'] = x
arr[1:-1]['y'] = y
# decide which points are connected by lines
if connect == 'pairs':
connect = np.empty((n/2,2), dtype=np.int32)
connect[:,0] = 1
connect[:,1] = 0
connect = connect.flatten()
if connect == 'all':
arr[1:-1]['c'] = 1
elif isinstance(connect, np.ndarray):
arr[1:-1]['c'] = connect
else:
raise Exception('connect argument must be "all", "pairs", or array')
#prof.mark('fill array')
# write last 0
lastInd = 20*(n+1)
arr.data[lastInd:lastInd+4] = struct.pack('>i', 0)
#prof.mark('footer')
# create datastream object and stream into path
buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here
#prof.mark('create buffer')
ds = QtCore.QDataStream(buf)
#prof.mark('create datastream')
ds >> path
#prof.mark('load')
#prof.finish()
else:
## This does exactly the same as above, but less efficiently (and more simply).
path.moveTo(x[0], y[0])
if connect == 'all':
for i in range(1, y.shape[0]):
path.lineTo(x[i], y[i])
elif connect == 'pairs':
for i in range(1, y.shape[0]):
if i%2 == 0:
path.lineTo(x[i], y[i])
else:
path.moveTo(x[i], y[i])
elif isinstance(connect, np.ndarray):
for i in range(1, y.shape[0]):
if connect[i] == 1:
path.lineTo(x[i], y[i])
else:
path.moveTo(x[i], y[i])
else:
raise Exception('connect argument must be "all", "pairs", or array')
return path
#def isosurface(data, level): #def isosurface(data, level):
#""" #"""
#Generate isosurface from volumetric data using marching tetrahedra algorithm. #Generate isosurface from volumetric data using marching tetrahedra algorithm.
@ -1257,7 +1359,7 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False):
points[b[1]].append([b,a]) points[b[1]].append([b,a])
## rearrange into chains ## rearrange into chains
for k in points.keys(): for k in list(points.keys()):
try: try:
chains = points[k] chains = points[k]
except KeyError: ## already used this point elsewhere except KeyError: ## already used this point elsewhere
@ -1871,4 +1973,4 @@ def pseudoScatter(data, spacing=None, shuffle=True):
yvals[i] = y yvals[i] = y
return yvals[np.argsort(inds)] ## un-shuffle values before returning return yvals[np.argsort(inds)] ## un-shuffle values before returning

View File

@ -12,6 +12,10 @@ class ArrowItem(QtGui.QGraphicsPathItem):
def __init__(self, **opts): def __init__(self, **opts):
"""
Arrows can be initialized with any keyword arguments accepted by
the setStyle() method.
"""
QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None)) QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None))
if 'size' in opts: if 'size' in opts:
opts['headLen'] = opts['size'] opts['headLen'] = opts['size']
@ -40,6 +44,32 @@ class ArrowItem(QtGui.QGraphicsPathItem):
self.moveBy(*self.opts['pos']) self.moveBy(*self.opts['pos'])
def setStyle(self, **opts): def setStyle(self, **opts):
"""
Changes the appearance of the arrow.
All arguments are optional:
================= =================================================
Keyword Arguments
angle Orientation of the arrow in degrees. Default is
0; arrow pointing to the left.
headLen Length of the arrow head, from tip to base.
default=20
headWidth Width of the arrow head at its base.
tipAngle Angle of the tip of the arrow in degrees. Smaller
values make a 'sharper' arrow. If tipAngle is
specified, ot overrides headWidth. default=25
baseAngle Angle of the base of the arrow head. Default is
0, which means that the base of the arrow head
is perpendicular to the arrow shaft.
tailLen Length of the arrow tail, measured from the base
of the arrow head to the tip of the tail. If
this value is None, no tail will be drawn.
default=None
tailWidth Width of the tail. default=3
pen The pen used to draw the outline of the arrow.
brush The brush used to fill the arrow.
================= =================================================
"""
self.opts = opts self.opts = opts
opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])

View File

@ -58,13 +58,14 @@ class AxisItem(GraphicsWidget):
self.labelUnitPrefix='' self.labelUnitPrefix=''
self.labelStyle = {} self.labelStyle = {}
self.logMode = False self.logMode = False
self.tickFont = None
self.textHeight = 18 self.textHeight = 18
self.tickLength = maxTickLength self.tickLength = maxTickLength
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
self.scale = 1.0 self.scale = 1.0
self.autoScale = True self.autoScale = True
self.setRange(0, 1) self.setRange(0, 1)
self.setPen(pen) self.setPen(pen)
@ -72,12 +73,12 @@ class AxisItem(GraphicsWidget):
self._linkedView = None self._linkedView = None
if linkView is not None: if linkView is not None:
self.linkToView(linkView) self.linkToView(linkView)
self.showLabel(False) self.showLabel(False)
self.grid = False self.grid = False
#self.setCacheMode(self.DeviceCoordinateCache) #self.setCacheMode(self.DeviceCoordinateCache)
def close(self): def close(self):
self.scene().removeItem(self.label) self.scene().removeItem(self.label)
self.label = None self.label = None
@ -100,6 +101,14 @@ class AxisItem(GraphicsWidget):
self.picture = None self.picture = None
self.update() self.update()
def setTickFont(self, font):
self.tickFont = font
self.picture = None
self.prepareGeometryChange()
## Need to re-allocate space depending on font size?
self.update()
def resizeEvent(self, ev=None): def resizeEvent(self, ev=None):
#s = self.size() #s = self.size()
@ -139,7 +148,31 @@ class AxisItem(GraphicsWidget):
self.setScale() self.setScale()
def setLabel(self, text=None, units=None, unitPrefix=None, **args): def setLabel(self, text=None, units=None, unitPrefix=None, **args):
"""Set the text displayed adjacent to the axis.""" """Set the text displayed adjacent to the axis.
============= =============================================================
Arguments
text The text (excluding units) to display on the label for this
axis.
units The units for this axis. Units should generally be given
without any scaling prefix (eg, 'V' instead of 'mV'). The
scaling prefix will be automatically prepended based on the
range of data displayed.
**args All extra keyword arguments become CSS style options for
the <span> tag which will surround the axis label and units.
============= =============================================================
The final text generated for the label will look like::
<span style="...options...">{text} (prefix{units})</span>
Each extra keyword argument will become a CSS option in the above template.
For example, you can set the font size and color of the label::
labelStyle = {'color': '#FFF', 'font-size': '14pt'}
axis.setLabel('label text', units='V', **labelStyle)
"""
if text is not None: if text is not None:
self.labelText = text self.labelText = text
self.showLabel() self.showLabel()
@ -287,14 +320,21 @@ class AxisItem(GraphicsWidget):
if linkedView is None or self.grid is False: if linkedView is None or self.grid is False:
rect = self.mapRectFromParent(self.geometry()) rect = self.mapRectFromParent(self.geometry())
## extend rect if ticks go in negative direction ## extend rect if ticks go in negative direction
## also extend to account for text that flows past the edges
if self.orientation == 'left': if self.orientation == 'left':
rect.setRight(rect.right() - min(0,self.tickLength)) #rect.setRight(rect.right() - min(0,self.tickLength))
#rect.setTop(rect.top() - 15)
#rect.setBottom(rect.bottom() + 15)
rect = rect.adjusted(0, -15, -min(0,self.tickLength), 15)
elif self.orientation == 'right': elif self.orientation == 'right':
rect.setLeft(rect.left() + min(0,self.tickLength)) #rect.setLeft(rect.left() + min(0,self.tickLength))
rect = rect.adjusted(min(0,self.tickLength), -15, 0, 15)
elif self.orientation == 'top': elif self.orientation == 'top':
rect.setBottom(rect.bottom() - min(0,self.tickLength)) #rect.setBottom(rect.bottom() - min(0,self.tickLength))
rect = rect.adjusted(-15, 0, 15, -min(0,self.tickLength))
elif self.orientation == 'bottom': elif self.orientation == 'bottom':
rect.setTop(rect.top() + min(0,self.tickLength)) #rect.setTop(rect.top() + min(0,self.tickLength))
rect = rect.adjusted(-15, min(0,self.tickLength), 15, 0)
return rect return rect
else: else:
return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
@ -623,6 +663,9 @@ class AxisItem(GraphicsWidget):
prof.mark('draw ticks') prof.mark('draw ticks')
## Draw text until there is no more room (or no more text) ## Draw text until there is no more room (or no more text)
if self.tickFont is not None:
p.setFont(self.tickFont)
textRects = [] textRects = []
for i in range(len(tickLevels)): for i in range(len(tickLevels)):
## Get the list of strings to display for this level ## Get the list of strings to display for this level
@ -640,7 +683,7 @@ class AxisItem(GraphicsWidget):
if tickPositions[i][j] is None: if tickPositions[i][j] is None:
strings[j] = None strings[j] = None
textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None]) textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s)) for s in strings if s is not None])
if i > 0: ## always draw top level if i > 0: ## always draw top level
## measure all text, make sure there's enough room ## measure all text, make sure there's enough room
if axis == 0: if axis == 0:
@ -656,8 +699,9 @@ class AxisItem(GraphicsWidget):
#strings = self.tickStrings(values, self.scale, spacing) #strings = self.tickStrings(values, self.scale, spacing)
for j in range(len(strings)): for j in range(len(strings)):
vstr = strings[j] vstr = strings[j]
if vstr is None:## this tick was ignored because it is out of bounds if vstr is None: ## this tick was ignored because it is out of bounds
continue continue
vstr = str(vstr)
x = tickPositions[i][j] x = tickPositions[i][j]
textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
height = textRect.height() height = textRect.height()

View File

@ -0,0 +1,133 @@
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
from .GraphicsObject import GraphicsObject
__all__ = ['ErrorBarItem']
class ErrorBarItem(GraphicsObject):
def __init__(self, **opts):
"""
Valid keyword options are:
x, y, height, width, top, bottom, left, right, beam, pen
x and y must be numpy arrays specifying the coordinates of data points.
height, width, top, bottom, left, right, and beam may be numpy arrays,
single values, or None to disable. All values should be positive.
If height is specified, it overrides top and bottom.
If width is specified, it overrides left and right.
"""
GraphicsObject.__init__(self)
self.opts = dict(
x=None,
y=None,
height=None,
width=None,
top=None,
bottom=None,
left=None,
right=None,
beam=None,
pen=None
)
self.setOpts(**opts)
def setOpts(self, **opts):
self.opts.update(opts)
self.path = None
self.update()
self.informViewBoundsChanged()
def drawPath(self):
p = QtGui.QPainterPath()
x, y = self.opts['x'], self.opts['y']
if x is None or y is None:
return
beam = self.opts['beam']
height, top, bottom = self.opts['height'], self.opts['top'], self.opts['bottom']
if height is not None or top is not None or bottom is not None:
## draw vertical error bars
if height is not None:
y1 = y - height/2.
y2 = y + height/2.
else:
if bottom is None:
y1 = y
else:
y1 = y - bottom
if top is None:
y2 = y
else:
y2 = y + top
for i in range(len(x)):
p.moveTo(x[i], y1[i])
p.lineTo(x[i], y2[i])
if beam is not None and beam > 0:
x1 = x - beam/2.
x2 = x + beam/2.
if height is not None or top is not None:
for i in range(len(x)):
p.moveTo(x1[i], y2[i])
p.lineTo(x2[i], y2[i])
if height is not None or bottom is not None:
for i in range(len(x)):
p.moveTo(x1[i], y1[i])
p.lineTo(x2[i], y1[i])
width, right, left = self.opts['width'], self.opts['right'], self.opts['left']
if width is not None or right is not None or left is not None:
## draw vertical error bars
if width is not None:
x1 = x - width/2.
x2 = x + width/2.
else:
if left is None:
x1 = x
else:
x1 = x - left
if right is None:
x2 = x
else:
x2 = x + right
for i in range(len(x)):
p.moveTo(x1[i], y[i])
p.lineTo(x2[i], y[i])
if beam is not None and beam > 0:
y1 = y - beam/2.
y2 = y + beam/2.
if width is not None or right is not None:
for i in range(len(x)):
p.moveTo(x2[i], y1[i])
p.lineTo(x2[i], y2[i])
if width is not None or left is not None:
for i in range(len(x)):
p.moveTo(x1[i], y1[i])
p.lineTo(x1[i], y2[i])
self.path = p
self.prepareGeometryChange()
def paint(self, p, *args):
if self.path is None:
self.drawPath()
pen = self.opts['pen']
if pen is None:
pen = pg.getConfigOption('foreground')
p.setPen(pg.mkPen(pen))
p.drawPath(self.path)
def boundingRect(self):
if self.path is None:
self.drawPath()
return self.path.boundingRect()

View File

@ -5,6 +5,8 @@ from .GraphicsObject import GraphicsObject
from .GraphicsWidget import GraphicsWidget from .GraphicsWidget import GraphicsWidget
import weakref import weakref
from pyqtgraph.pgcollections import OrderedDict from pyqtgraph.pgcollections import OrderedDict
from pyqtgraph.colormap import ColorMap
import numpy as np import numpy as np
__all__ = ['TickSliderItem', 'GradientEditorItem'] __all__ = ['TickSliderItem', 'GradientEditorItem']
@ -22,6 +24,9 @@ Gradients = OrderedDict([
]) ])
class TickSliderItem(GraphicsWidget): class TickSliderItem(GraphicsWidget):
## public class ## public class
"""**Bases:** :class:`GraphicsWidget <pyqtgraph.GraphicsWidget>` """**Bases:** :class:`GraphicsWidget <pyqtgraph.GraphicsWidget>`
@ -490,6 +495,18 @@ class GradientEditorItem(TickSliderItem):
self.colorMode = cm self.colorMode = cm
self.updateGradient() self.updateGradient()
def colorMap(self):
"""Return a ColorMap object representing the current state of the editor."""
if self.colorMode == 'hsv':
raise NotImplementedError('hsv colormaps not yet supported')
pos = []
color = []
for t,x in self.listTicks():
pos.append(x)
c = t.color
color.append([c.red(), c.green(), c.blue(), c.alpha()])
return ColorMap(np.array(pos), np.array(color, dtype=np.ubyte))
def updateGradient(self): def updateGradient(self):
#private #private
self.gradient = self.getGradient() self.gradient = self.getGradient()
@ -611,7 +628,7 @@ class GradientEditorItem(TickSliderItem):
b = c1.blue() * (1.-f) + c2.blue() * f b = c1.blue() * (1.-f) + c2.blue() * f
a = c1.alpha() * (1.-f) + c2.alpha() * f a = c1.alpha() * (1.-f) + c2.alpha() * f
if toQColor: if toQColor:
return QtGui.QColor(r, g, b,a) return QtGui.QColor(int(r), int(g), int(b), int(a))
else: else:
return (r,g,b,a) return (r,g,b,a)
elif self.colorMode == 'hsv': elif self.colorMode == 'hsv':
@ -751,6 +768,18 @@ class GradientEditorItem(TickSliderItem):
self.addTick(t[0], c, finish=False) self.addTick(t[0], c, finish=False)
self.updateGradient() self.updateGradient()
self.sigGradientChangeFinished.emit(self) self.sigGradientChangeFinished.emit(self)
def setColorMap(self, cm):
self.setColorMode('rgb')
for t in list(self.ticks.keys()):
self.removeTick(t, finish=False)
colors = cm.getColors(mode='qcolor')
for i in range(len(cm.pos)):
x = cm.pos[i]
c = colors[i]
self.addTick(x, c, finish=False)
self.updateGradient()
self.sigGradientChangeFinished.emit(self)
class Tick(GraphicsObject): class Tick(GraphicsObject):

View File

@ -0,0 +1,115 @@
from .. import functions as fn
from .GraphicsObject import GraphicsObject
from .ScatterPlotItem import ScatterPlotItem
import pyqtgraph as pg
import numpy as np
__all__ = ['GraphItem']
class GraphItem(GraphicsObject):
"""A GraphItem displays graph information as
a set of nodes connected by lines (as in 'graph theory', not 'graphics').
Useful for drawing networks, trees, etc.
"""
def __init__(self, **kwds):
GraphicsObject.__init__(self)
self.scatter = ScatterPlotItem()
self.scatter.setParentItem(self)
self.adjacency = None
self.pos = None
self.picture = None
self.pen = 'default'
self.setData(**kwds)
def setData(self, **kwds):
"""
Change the data displayed by the graph.
============ =========================================================
Arguments
pos (N,2) array of the positions of each node in the graph.
adj (M,2) array of connection data. Each row contains indexes
of two nodes that are connected.
pen The pen to use when drawing lines between connected
nodes. May be one of:
* QPen
* a single argument to pass to pg.mkPen
* a record array of length M
with fields (red, green, blue, alpha, width). Note
that using this option may have a significant performance
cost.
* None (to disable connection drawing)
* 'default' to use the default foreground color.
symbolPen The pen used for drawing nodes.
``**opts`` All other keyword arguments are given to
:func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>`
to affect the appearance of nodes (symbol, size, brush,
etc.)
============ =========================================================
"""
if 'adj' in kwds:
self.adjacency = kwds.pop('adj')
assert self.adjacency.dtype.kind in 'iu'
self.picture = None
if 'pos' in kwds:
self.pos = kwds['pos']
self.picture = None
if 'pen' in kwds:
self.setPen(kwds.pop('pen'))
self.picture = None
if 'symbolPen' in kwds:
kwds['pen'] = kwds.pop('symbolPen')
self.scatter.setData(**kwds)
self.informViewBoundsChanged()
def setPen(self, pen):
self.pen = pen
self.picture = None
def generatePicture(self):
self.picture = pg.QtGui.QPicture()
if self.pen is None or self.pos is None or self.adjacency is None:
return
p = pg.QtGui.QPainter(self.picture)
try:
pts = self.pos[self.adjacency]
pen = self.pen
if isinstance(pen, np.ndarray):
lastPen = None
for i in range(pts.shape[0]):
pen = self.pen[i]
if np.any(pen != lastPen):
lastPen = pen
if pen.dtype.fields is None:
p.setPen(pg.mkPen(color=(pen[0], pen[1], pen[2], pen[3]), width=1))
else:
p.setPen(pg.mkPen(color=(pen['red'], pen['green'], pen['blue'], pen['alpha']), width=pen['width']))
p.drawLine(pg.QtCore.QPointF(*pts[i][0]), pg.QtCore.QPointF(*pts[i][1]))
else:
if pen == 'default':
pen = pg.getConfigOption('foreground')
p.setPen(pg.mkPen(pen))
pts = pts.reshape((pts.shape[0]*pts.shape[1], pts.shape[2]))
path = fn.arrayToQPath(x=pts[:,0], y=pts[:,1], connect='pairs')
p.drawPath(path)
finally:
p.end()
def paint(self, p, *args):
if self.picture == None:
self.generatePicture()
self.picture.play(p)
def boundingRect(self):
return self.scatter.boundingRect()

View File

@ -4,7 +4,7 @@ from pyqtgraph.Point import Point
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
import weakref import weakref
from pyqtgraph.pgcollections import OrderedDict from pyqtgraph.pgcollections import OrderedDict
import operator import operator, sys
class FiniteCache(OrderedDict): class FiniteCache(OrderedDict):
"""Caches a finite number of objects, removing """Caches a finite number of objects, removing
@ -17,10 +17,10 @@ class FiniteCache(OrderedDict):
self.pop(item, None) # make sure item is added to end self.pop(item, None) # make sure item is added to end
OrderedDict.__setitem__(self, item, val) OrderedDict.__setitem__(self, item, val)
while len(self) > self._length: while len(self) > self._length:
del self[self.keys()[0]] del self[list(self.keys())[0]]
def __getitem__(self, item): def __getitem__(self, item):
val = dict.__getitem__(self, item) val = OrderedDict.__getitem__(self, item)
del self[item] del self[item]
self[item] = val ## promote this key self[item] = val ## promote this key
return val return val
@ -194,17 +194,22 @@ class GraphicsItem(object):
dt = self.deviceTransform() dt = self.deviceTransform()
if dt is None: if dt is None:
return None, None return None, None
## Ignore translation. If the translation is much larger than the scale
## (such as when looking at unix timestamps), we can get floating-point errors.
dt.setMatrix(dt.m11(), dt.m12(), 0, dt.m21(), dt.m22(), 0, 0, 0, 1)
## check local cache ## check local cache
if direction is None and dt == self._pixelVectorCache[0]: if direction is None and dt == self._pixelVectorCache[0]:
return self._pixelVectorCache[1] return tuple(map(Point, self._pixelVectorCache[1])) ## return a *copy*
## check global cache ## check global cache
key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32()) #key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
key = (dt.m11(), dt.m21(), dt.m12(), dt.m22())
pv = self._pixelVectorGlobalCache.get(key, None) pv = self._pixelVectorGlobalCache.get(key, None)
if pv is not None: if direction is None and pv is not None:
self._pixelVectorCache = [dt, pv] self._pixelVectorCache = [dt, pv]
return pv return tuple(map(Point,pv)) ## return a *copy*
if direction is None: if direction is None:
@ -213,15 +218,32 @@ class GraphicsItem(object):
raise Exception("Cannot compute pixel length for 0-length vector.") raise Exception("Cannot compute pixel length for 0-length vector.")
## attempt to re-scale direction vector to fit within the precision of the coordinate system ## attempt to re-scale direction vector to fit within the precision of the coordinate system
if direction.x() == 0: ## Here's the problem: we need to map the vector 'direction' from the item to the device, via transform 'dt'.
r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22())) ## In some extreme cases, this mapping can fail unless the length of 'direction' is cleverly chosen.
#r = 1.0/(abs(dt.m12()) + abs(dt.m22())) ## Example:
elif direction.y() == 0: ## dt = [ 1, 0, 2
r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21())) ## 0, 2, 1e20
#r = 1.0/(abs(dt.m11()) + abs(dt.m21())) ## 0, 0, 1 ]
else: ## Then we map the origin (0,0) and direction (0,1) and get:
r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5 ## o' = 2,1e20
directionr = direction * r ## d' = 2,1e20 <-- should be 1e20+2, but this can't be represented with a 32-bit float
##
## |o' - d'| == 0 <-- this is the problem.
## Perhaps the easiest solution is to exclude the transformation column from dt. Does this cause any other problems?
#if direction.x() == 0:
#r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))
##r = 1.0/(abs(dt.m12()) + abs(dt.m22()))
#elif direction.y() == 0:
#r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))
##r = 1.0/(abs(dt.m11()) + abs(dt.m21()))
#else:
#r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5
#if r == 0:
#r = 1. ## shouldn't need to do this; probably means the math above is wrong?
#directionr = direction * r
directionr = direction
## map direction vector onto device ## map direction vector onto device
#viewDir = Point(dt.map(directionr) - dt.map(Point(0,0))) #viewDir = Point(dt.map(directionr) - dt.map(Point(0,0)))
@ -547,4 +569,4 @@ class GraphicsItem(object):
#def update(self): #def update(self):
#self._qtBaseClass.update(self) #self._qtBaseClass.update(self)
#print "Update:", self #print "Update:", self

View File

@ -245,7 +245,7 @@ class LinearRegionItem(UIGraphicsItem):
def hoverEvent(self, ev): def hoverEvent(self, ev):
if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): if self.movable and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
self.setMouseHover(True) self.setMouseHover(True)
else: else:
self.setMouseHover(False) self.setMouseHover(False)

View File

@ -93,7 +93,7 @@ class PlotCurveItem(GraphicsObject):
(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 (None, None)
if ax == 0: if ax == 0:
d = x d = x
@ -102,20 +102,106 @@ class PlotCurveItem(GraphicsObject):
d = y d = y
d2 = x d2 = x
## If an orthogonal range is specified, mask the data now
if orthoRange is not None: if orthoRange is not None:
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask] d = d[mask]
d2 = d2[mask] d2 = d2[mask]
## Get min/max (or percentiles) of the requested data range
if frac >= 1.0: if frac >= 1.0:
b = (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:
b = (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)))
## adjust for fill level
if ax == 1 and self.opts['fillLevel'] is not None:
b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel']))
## Add pen width only if it is non-cosmetic.
pen = self.opts['pen']
spen = self.opts['shadowPen']
if not pen.isCosmetic():
b = (b[0] - pen.widthF()*0.7072, b[1] + pen.widthF()*0.7072)
if spen is not None and not spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen:
b = (b[0] - spen.widthF()*0.7072, b[1] + spen.widthF()*0.7072)
self._boundsCache[ax] = [(frac, orthoRange), b] self._boundsCache[ax] = [(frac, orthoRange), b]
return b return b
def pixelPadding(self):
pen = self.opts['pen']
spen = self.opts['shadowPen']
w = 0
if pen.isCosmetic():
w += pen.widthF()*0.7072
if spen is not None and spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen:
w = max(w, spen.widthF()*0.7072)
return w
def boundingRect(self):
if self._boundingRect is None:
(xmn, xmx) = self.dataBounds(ax=0)
(ymn, ymx) = self.dataBounds(ax=1)
if xmn is None:
return QtCore.QRectF()
px = py = 0.0
pxPad = self.pixelPadding()
if pxPad > 0:
# determine length of pixel in local x, y directions
px, py = self.pixelVectors()
px = 0 if px is None else px.length()
py = 0 if py is None else py.length()
# return bounds expanded by pixel size
px *= pxPad
py *= pxPad
#px += self._maxSpotWidth * 0.5
#py += self._maxSpotWidth * 0.5
self._boundingRect = QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)
return self._boundingRect
def viewTransformChanged(self):
self.invalidateBounds()
self.prepareGeometryChange()
#def boundingRect(self):
#if self._boundingRect is None:
#(x, y) = self.getData()
#if x is None or y is None or len(x) == 0 or len(y) == 0:
#return QtCore.QRectF()
#if self.opts['shadowPen'] is not None:
#lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
#else:
#lineWidth = (self.opts['pen'].width()+1)
#pixels = self.pixelVectors()
#if pixels == (None, None):
#pixels = [Point(0,0), Point(0,0)]
#xmin = x.min()
#xmax = x.max()
#ymin = y.min()
#ymax = y.max()
#if self.opts['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
#self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
#return self._boundingRect
def invalidateBounds(self): def invalidateBounds(self):
self._boundingRect = None self._boundingRect = None
@ -249,26 +335,6 @@ class PlotCurveItem(GraphicsObject):
prof.finish() prof.finish()
def generatePath(self, x, y): def generatePath(self, x, y):
prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True)
path = QtGui.QPainterPath()
## Create all vertices in path. The method used below creates a binary format so that all
## vertices can be read in at once. This binary format may change in future versions of Qt,
## so the original (slower) method is left here for emergencies:
#path.moveTo(x[0], y[0])
#for i in range(1, y.shape[0]):
# path.lineTo(x[i], y[i])
## Speed this up using >> operator
## Format is:
## numVerts(i4) 0(i4)
## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect
## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex
## ...
## 0(i4)
##
## All values are big endian--pack using struct.pack('>d') or struct.pack('>i')
if self.opts['stepMode']: if self.opts['stepMode']:
## each value in the x/y arrays generates 2 points. ## each value in the x/y arrays generates 2 points.
x2 = np.empty((len(x),2), dtype=x.dtype) x2 = np.empty((len(x),2), dtype=x.dtype)
@ -286,41 +352,8 @@ class PlotCurveItem(GraphicsObject):
y = y2.reshape(y2.size)[1:-1] y = y2.reshape(y2.size)[1:-1]
y[0] = self.opts['fillLevel'] y[0] = self.opts['fillLevel']
y[-1] = self.opts['fillLevel'] y[-1] = self.opts['fillLevel']
path = fn.arrayToQPath(x, y, connect='all')
if sys.version_info[0] == 2: ## So this is disabled for python 3... why??
n = x.shape[0]
# create empty array, pad with extra space on either end
arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')])
# write first two integers
prof.mark('allocate empty')
arr.data[12:20] = struct.pack('>ii', n, 0)
prof.mark('pack header')
# Fill array with vertex values
arr[1:-1]['x'] = x
arr[1:-1]['y'] = y
arr[1:-1]['c'] = 1
prof.mark('fill array')
# write last 0
lastInd = 20*(n+1)
arr.data[lastInd:lastInd+4] = struct.pack('>i', 0)
prof.mark('footer')
# create datastream object and stream into path
buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here
prof.mark('create buffer')
ds = QtCore.QDataStream(buf)
prof.mark('create datastream')
ds >> path
prof.mark('load')
prof.finish()
else:
path.moveTo(x[0], y[0])
for i in range(1, y.shape[0]):
path.lineTo(x[i], y[i])
return path return path
@ -333,40 +366,6 @@ class PlotCurveItem(GraphicsObject):
return QtGui.QPainterPath() return QtGui.QPainterPath()
return self.path return self.path
def boundingRect(self):
if self._boundingRect is None:
(x, y) = self.getData()
if x is None or y is None or len(x) == 0 or len(y) == 0:
return QtCore.QRectF()
if self.opts['shadowPen'] is not None:
lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
else:
lineWidth = (self.opts['pen'].width()+1)
pixels = self.pixelVectors()
if pixels == (None, None):
pixels = [Point(0,0), Point(0,0)]
xmin = x.min()
xmax = x.max()
ymin = y.min()
ymax = y.max()
if self.opts['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
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)
if self.xData is None: if self.xData is None:

View File

@ -104,6 +104,7 @@ class PlotDataItem(GraphicsObject):
self.yData = None self.yData = None
self.xDisp = None self.xDisp = None
self.yDisp = None self.yDisp = None
self.dataMask = None
#self.curves = [] #self.curves = []
#self.scatters = [] #self.scatters = []
self.curve = PlotCurveItem() self.curve = PlotCurveItem()
@ -393,6 +394,7 @@ class PlotDataItem(GraphicsObject):
scatterArgs[v] = self.opts[k] scatterArgs[v] = self.opts[k]
x,y = self.getData() x,y = self.getData()
scatterArgs['mask'] = self.dataMask
if curveArgs['pen'] is not None or (curveArgs['brush'] is not None and curveArgs['fillLevel'] is not None): if curveArgs['pen'] is not None or (curveArgs['brush'] is not None and curveArgs['fillLevel'] is not None):
self.curve.setData(x=x, y=y, **curveArgs) self.curve.setData(x=x, y=y, **curveArgs)
@ -413,11 +415,15 @@ class PlotDataItem(GraphicsObject):
if self.xDisp is None: if self.xDisp is None:
nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData) nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData)
if any(nanMask): if any(nanMask):
x = self.xData[~nanMask] self.dataMask = ~nanMask
y = self.yData[~nanMask] x = self.xData[self.dataMask]
y = self.yData[self.dataMask]
else: else:
self.dataMask = None
x = self.xData x = self.xData
y = self.yData y = self.yData
ds = self.opts['downsample'] ds = self.opts['downsample']
if ds > 1: if ds > 1:
x = x[::ds] x = x[::ds]
@ -435,8 +441,11 @@ class PlotDataItem(GraphicsObject):
if any(self.opts['logMode']): ## re-check for NANs after log if any(self.opts['logMode']): ## re-check for NANs after log
nanMask = np.isinf(x) | np.isinf(y) | np.isnan(x) | np.isnan(y) nanMask = np.isinf(x) | np.isinf(y) | np.isnan(x) | np.isnan(y)
if any(nanMask): if any(nanMask):
x = x[~nanMask] self.dataMask = ~nanMask
y = y[~nanMask] x = x[self.dataMask]
y = y[self.dataMask]
else:
self.dataMask = None
self.xDisp = x self.xDisp = x
self.yDisp = y self.yDisp = y
#print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max()
@ -462,33 +471,57 @@ class PlotDataItem(GraphicsObject):
and max) and max)
=============== ============================================================= =============== =============================================================
""" """
if frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
(x, y) = self.getData() range = [None, None]
if x is None or len(x) == 0: if self.curve.isVisible():
return None range = self.curve.dataBounds(ax, frac, orthoRange)
elif self.scatter.isVisible():
r2 = self.scatter.dataBounds(ax, frac, orthoRange)
range = [
r2[0] if range[0] is None else (range[0] if r2[0] is None else min(r2[0], range[0])),
r2[1] if range[1] is None else (range[1] if r2[1] is None else min(r2[1], range[1]))
]
return range
#if frac <= 0.0:
#raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
#(x, y) = self.getData()
#if x is None or len(x) == 0:
#return None
if ax == 0: #if ax == 0:
d = x #d = x
d2 = y #d2 = y
elif ax == 1: #elif ax == 1:
d = y #d = y
d2 = x #d2 = x
if orthoRange is not None: #if orthoRange is not None:
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) #mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask] #d = d[mask]
#d2 = d2[mask] ##d2 = d2[mask]
if len(d) > 0: #if len(d) > 0:
if frac >= 1.0: #if frac >= 1.0:
return (np.min(d), np.max(d)) #return (np.min(d), np.max(d))
else: #else:
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) #return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
else: #else:
return None #return None
def pixelPadding(self):
"""
Return the size in pixels that this item may draw beyond the values returned by dataBounds().
This method is called by ViewBox when auto-scaling.
"""
pad = 0
if self.curve.isVisible():
pad = max(pad, self.curve.pixelPadding())
elif self.scatter.isVisible():
pad = max(pad, self.scatter.pixelPadding())
return pad
def clear(self): def clear(self):
#for i in self.curves+self.scatters: #for i in self.curves+self.scatters:

View File

@ -36,6 +36,7 @@ from .. LabelItem import LabelItem
from .. LegendItem import LegendItem from .. LegendItem import LegendItem
from .. GraphicsWidget import GraphicsWidget from .. GraphicsWidget import GraphicsWidget
from .. ButtonItem import ButtonItem from .. ButtonItem import ButtonItem
from .. InfiniteLine import InfiniteLine
from pyqtgraph.WidgetGroup import WidgetGroup from pyqtgraph.WidgetGroup import WidgetGroup
__all__ = ['PlotItem'] __all__ = ['PlotItem']
@ -548,10 +549,35 @@ class PlotItem(GraphicsWidget):
print("PlotItem.addDataItem is deprecated. Use addItem instead.") print("PlotItem.addDataItem is deprecated. Use addItem instead.")
self.addItem(item, *args) self.addItem(item, *args)
def listDataItems(self):
"""Return a list of all data items (PlotDataItem, PlotCurveItem, ScatterPlotItem, etc)
contained in this PlotItem."""
return self.dataItems[:]
def addCurve(self, c, params=None): def addCurve(self, c, params=None):
print("PlotItem.addCurve is deprecated. Use addItem instead.") print("PlotItem.addCurve is deprecated. Use addItem instead.")
self.addItem(c, params) self.addItem(c, params)
def addLine(self, x=None, y=None, z=None, **kwds):
"""
Create an InfiniteLine and add to the plot.
If *x* is specified,
the line will be vertical. If *y* is specified, the line will be
horizontal. All extra keyword arguments are passed to
:func:`InfiniteLine.__init__() <pyqtgraph.InfiniteLine.__init__>`.
Returns the item created.
"""
angle = 0 if x is None else 90
pos = x if x is not None else y
line = InfiniteLine(pos, angle, **kwds)
self.addItem(line)
if z is not None:
line.setZValue(z)
return line
def removeItem(self, item): def removeItem(self, item):
""" """
Remove an item from the internal ViewBox. Remove an item from the internal ViewBox.
@ -1054,6 +1080,21 @@ class PlotItem(GraphicsWidget):
""" """
self.getAxis(axis).setLabel(text=text, units=units, **args) self.getAxis(axis).setLabel(text=text, units=units, **args)
def setLabels(self, **kwds):
"""
Convenience function allowing multiple labels and/or title to be set in one call.
Keyword arguments can be 'title', 'left', 'bottom', 'right', or 'top'.
Values may be strings or a tuple of arguments to pass to setLabel.
"""
for k,v in kwds.items():
if k == 'title':
self.setTitle(v)
else:
if isinstance(v, basestring):
v = (v,)
self.setLabel(k, *v)
def showLabel(self, axis, show=True): def showLabel(self, axis, show=True):
""" """
Show or hide one of the plot's axis labels (the axis itself will be unaffected). Show or hide one of the plot's axis labels (the axis itself will be unaffected).

View File

@ -1783,8 +1783,7 @@ class LineSegmentROI(ROI):
dh = h2-h1 dh = h2-h1
if dh.length() == 0: if dh.length() == 0:
return p return p
pxv = self.pixelVectors(h2-h1)[1] pxv = self.pixelVectors(dh)[1]
if pxv is None: if pxv is None:
return p return p
@ -1809,7 +1808,7 @@ class LineSegmentROI(ROI):
for i in range(len(imgPts)-1): for i in range(len(imgPts)-1):
d = Point(imgPts[i+1] - imgPts[i]) d = Point(imgPts[i+1] - imgPts[i])
o = Point(imgPts[i]) o = Point(imgPts[i])
r = fn.affineSlice(data, shape=(int(d.length()),), vectors=[d.norm()], origin=o, axes=axes, order=1) r = fn.affineSlice(data, shape=(int(d.length()),), vectors=[Point(d.norm())], origin=o, axes=axes, order=1)
rgns.append(r) rgns.append(r)
return np.concatenate(rgns, axis=axes[0]) return np.concatenate(rgns, axis=axes[0])

View File

@ -41,7 +41,7 @@ def drawSymbol(painter, symbol, size, pen, brush):
if isinstance(symbol, basestring): if isinstance(symbol, basestring):
symbol = Symbols[symbol] symbol = Symbols[symbol]
if np.isscalar(symbol): if np.isscalar(symbol):
symbol = Symbols.values()[symbol % len(Symbols)] symbol = list(Symbols.values())[symbol % len(Symbols)]
painter.drawPath(symbol) painter.drawPath(symbol)
@ -60,7 +60,7 @@ def renderSymbol(symbol, size, pen, brush, device=None):
#return SymbolPixmapCache[key] #return SymbolPixmapCache[key]
## Render a spot with the given parameters to a pixmap ## Render a spot with the given parameters to a pixmap
penPxWidth = max(np.ceil(pen.width()), 1) penPxWidth = max(np.ceil(pen.widthF()), 1)
image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32) image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32)
image.fill(0) image.fill(0)
p = QtGui.QPainter(image) p = QtGui.QPainter(image)
@ -115,7 +115,7 @@ class SymbolAtlas(object):
symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush'] symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush']
pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen
brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush
key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color())) key = (symbol, size, fn.colorTuple(pen.color()), pen.widthF(), pen.style(), fn.colorTuple(brush.color()))
if key not in self.symbolMap: if key not in self.symbolMap:
newCoords = SymbolAtlas.SymbolCoords() newCoords = SymbolAtlas.SymbolCoords()
self.symbolMap[key] = newCoords self.symbolMap[key] = newCoords
@ -384,7 +384,7 @@ class ScatterPlotItem(GraphicsObject):
for k in ['pen', 'brush', 'symbol', 'size']: for k in ['pen', 'brush', 'symbol', 'size']:
if k in kargs: if k in kargs:
setMethod = getattr(self, 'set' + k[0].upper() + k[1:]) setMethod = getattr(self, 'set' + k[0].upper() + k[1:])
setMethod(kargs[k], update=False, dataSet=newData) setMethod(kargs[k], update=False, dataSet=newData, mask=kargs.get('mask', None))
if 'data' in kargs: if 'data' in kargs:
self.setPointData(kargs['data'], dataSet=newData) self.setPointData(kargs['data'], dataSet=newData)
@ -425,6 +425,8 @@ class ScatterPlotItem(GraphicsObject):
if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
pens = args[0] pens = args[0]
if kargs['mask'] is not None:
pens = pens[kargs['mask']]
if len(pens) != len(dataSet): if len(pens) != len(dataSet):
raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet))) raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet)))
dataSet['pen'] = pens dataSet['pen'] = pens
@ -445,6 +447,8 @@ class ScatterPlotItem(GraphicsObject):
if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
brushes = args[0] brushes = args[0]
if kargs['mask'] is not None:
brushes = brushes[kargs['mask']]
if len(brushes) != len(dataSet): if len(brushes) != len(dataSet):
raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(dataSet))) raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(dataSet)))
#for i in xrange(len(brushes)): #for i in xrange(len(brushes)):
@ -458,7 +462,7 @@ class ScatterPlotItem(GraphicsObject):
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
def setSymbol(self, symbol, update=True, dataSet=None): def setSymbol(self, symbol, update=True, dataSet=None, mask=None):
"""Set the symbol(s) used to draw each spot. """Set the symbol(s) used to draw each spot.
If a list or array is provided, then the symbol for each spot will be set separately. If a list or array is provided, then the symbol for each spot will be set separately.
Otherwise, the argument will be used as the default symbol for Otherwise, the argument will be used as the default symbol for
@ -468,6 +472,8 @@ class ScatterPlotItem(GraphicsObject):
if isinstance(symbol, np.ndarray) or isinstance(symbol, list): if isinstance(symbol, np.ndarray) or isinstance(symbol, list):
symbols = symbol symbols = symbol
if mask is not None:
symbols = symbols[mask]
if len(symbols) != len(dataSet): if len(symbols) != len(dataSet):
raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(dataSet))) raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(dataSet)))
dataSet['symbol'] = symbols dataSet['symbol'] = symbols
@ -479,7 +485,7 @@ class ScatterPlotItem(GraphicsObject):
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
def setSize(self, size, update=True, dataSet=None): def setSize(self, size, update=True, dataSet=None, mask=None):
"""Set the size(s) used to draw each spot. """Set the size(s) used to draw each spot.
If a list or array is provided, then the size for each spot will be set separately. If a list or array is provided, then the size for each spot will be set separately.
Otherwise, the argument will be used as the default size for Otherwise, the argument will be used as the default size for
@ -489,6 +495,8 @@ class ScatterPlotItem(GraphicsObject):
if isinstance(size, np.ndarray) or isinstance(size, list): if isinstance(size, np.ndarray) or isinstance(size, list):
sizes = size sizes = size
if kargs['mask'] is not None:
sizes = sizes[kargs['mask']]
if len(sizes) != len(dataSet): if len(sizes) != len(dataSet):
raise Exception("Number of sizes does not match number of points (%d != %d)" % (len(sizes), len(dataSet))) raise Exception("Number of sizes does not match number of points (%d != %d)" % (len(sizes), len(dataSet)))
dataSet['size'] = sizes dataSet['size'] = sizes
@ -505,6 +513,8 @@ class ScatterPlotItem(GraphicsObject):
dataSet = self.data dataSet = self.data
if isinstance(data, np.ndarray) or isinstance(data, list): if isinstance(data, np.ndarray) or isinstance(data, list):
if kargs['mask'] is not None:
data = data[kargs['mask']]
if len(data) != len(dataSet): if len(data) != len(dataSet):
raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet))) raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet)))
@ -579,13 +589,13 @@ class ScatterPlotItem(GraphicsObject):
width = 0 width = 0
pxWidth = 0 pxWidth = 0
if self.opts['pxMode']: if self.opts['pxMode']:
pxWidth = size + pen.width() pxWidth = size + pen.widthF()
else: else:
width = size width = size
if pen.isCosmetic(): if pen.isCosmetic():
pxWidth += pen.width() pxWidth += pen.widthF()
else: else:
width += pen.width() width += pen.widthF()
self._maxSpotWidth = max(self._maxSpotWidth, width) self._maxSpotWidth = max(self._maxSpotWidth, width)
self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth) self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth)
self.bounds = [None, None] self.bounds = [None, None]
@ -599,7 +609,7 @@ class ScatterPlotItem(GraphicsObject):
self.invalidate() self.invalidate()
def dataBounds(self, ax, frac=1.0, orthoRange=None): def dataBounds(self, ax, frac=1.0, orthoRange=None):
if frac >= 1.0 and self.bounds[ax] is not None: if frac >= 1.0 and orthoRange is None and self.bounds[ax] is not None:
return self.bounds[ax] return self.bounds[ax]
#self.prepareGeometryChange() #self.prepareGeometryChange()
@ -619,28 +629,15 @@ class ScatterPlotItem(GraphicsObject):
d2 = d2[mask] d2 = d2[mask]
if frac >= 1.0: if frac >= 1.0:
## increase size of bounds based on spot size and pen width self.bounds[ax] = (d.min() - self._maxSpotWidth*0.7072, d.max() + self._maxSpotWidth*0.7072)
px = self.pixelLength(Point(1, 0) if ax == 0 else Point(0, 1)) ## determine length of pixel along this axis
if px is None:
px = 0
minIndex = np.argmin(d)
maxIndex = np.argmax(d)
minVal = d[minIndex]
maxVal = d[maxIndex]
spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth)
self.bounds[ax] = (minVal-spotSize, maxVal+spotSize)
return self.bounds[ax] return self.bounds[ax]
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))) return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
def pixelPadding(self):
#def defaultSpotPixmap(self): return self._maxSpotPxWidth*0.7072
### Return the default spot pixmap
#if self._spotPixmap is None:
#self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
#return self._spotPixmap
def boundingRect(self): def boundingRect(self):
(xmn, xmx) = self.dataBounds(ax=0) (xmn, xmx) = self.dataBounds(ax=0)
@ -651,7 +648,19 @@ class ScatterPlotItem(GraphicsObject):
if ymn is None or ymx is None: if ymn is None or ymx is None:
ymn = 0 ymn = 0
ymx = 0 ymx = 0
return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
px = py = 0.0
pxPad = self.pixelPadding()
if pxPad > 0:
# determine length of pixel in local x, y directions
px, py = self.pixelVectors()
px = 0 if px is None else px.length()
py = 0 if py is None else py.length()
# return bounds expanded by pixel size
px *= pxPad
py *= pxPad
return QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)
def viewTransformChanged(self): def viewTransformChanged(self):
self.prepareGeometryChange() self.prepareGeometryChange()
@ -668,6 +677,8 @@ class ScatterPlotItem(GraphicsObject):
pts[1] = self.data['y'] pts[1] = self.data['y']
pts = fn.transformCoordinates(tr, pts) pts = fn.transformCoordinates(tr, pts)
self.fragments = [] self.fragments = []
pts = np.clip(pts, -2**31, 2**31) ## prevent Qt segmentation fault.
## Still won't be able to render correctly, though.
for i in xrange(len(self.data)): for i in xrange(len(self.data)):
rec = self.data[i] rec = self.data[i]
pos = QtCore.QPointF(pts[0,i], pts[1,i]) pos = QtCore.QPointF(pts[0,i], pts[1,i])
@ -680,8 +691,10 @@ class ScatterPlotItem(GraphicsObject):
self.invalidate() self.invalidate()
def paint(self, p, *args): def paint(self, p, *args):
#p.setPen(fn.mkPen('r')) #p.setPen(fn.mkPen('r'))
#p.drawRect(self.boundingRect()) #p.drawRect(self.boundingRect())
if self._exportOpts is not False: if self._exportOpts is not False:
aa = self._exportOpts.get('antialias', True) aa = self._exportOpts.get('antialias', True)
scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed
@ -728,7 +741,6 @@ class ScatterPlotItem(GraphicsObject):
p2.end() p2.end()
self.picture.play(p) self.picture.play(p)
def points(self): def points(self):
for rec in self.data: for rec in self.data:
@ -875,7 +887,7 @@ class SpotItem(object):
def updateItem(self): def updateItem(self):
self._data['fragCoords'] = None self._data['fragCoords'] = None
self._plot.updateSpots([self._data]) self._plot.updateSpots(self._data.reshape(1))
self._plot.invalidate() self._plot.invalidate()
#class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem): #class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem):

View File

@ -138,7 +138,7 @@ class ViewBox(GraphicsWidget):
self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1))
self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100)) self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100))
self.rbScaleBox.hide() self.rbScaleBox.hide()
self.addItem(self.rbScaleBox) self.addItem(self.rbScaleBox, ignoreBounds=True)
self.axHistory = [] # maintain a history of zoom locations self.axHistory = [] # maintain a history of zoom locations
self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo" self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo"
@ -297,10 +297,11 @@ class ViewBox(GraphicsWidget):
def resizeEvent(self, ev): def resizeEvent(self, ev):
#self.setRange(self.range, padding=0) #self.setRange(self.range, padding=0)
#self.updateAutoRange() self.updateAutoRange()
self.updateMatrix() self.updateMatrix()
self.sigStateChanged.emit(self) self.sigStateChanged.emit(self)
self.background.setRect(self.rect()) self.background.setRect(self.rect())
#self._itemBoundsCache.clear()
#self.linkedXChanged() #self.linkedXChanged()
#self.linkedYChanged() #self.linkedYChanged()
@ -576,9 +577,12 @@ class ViewBox(GraphicsWidget):
w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2. w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2.
childRange[ax] = [x-w2, x+w2] childRange[ax] = [x-w2, x+w2]
else: else:
wp = (xr[1] - xr[0]) * 0.02 l = self.width() if ax==0 else self.height()
childRange[ax][0] -= wp if l > 0:
childRange[ax][1] += wp padding = np.clip(1./(l**0.5), 0.02, 0.1)
wp = (xr[1] - xr[0]) * padding
childRange[ax][0] -= wp
childRange[ax][1] += wp
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]
if len(args) == 0: if len(args) == 0:
@ -997,63 +1001,71 @@ class ViewBox(GraphicsWidget):
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) prof = debug.Profiler('updateAutoRange', disabled=True)
#items = self.allChildren()
items = self.addedItems items = self.addedItems
#if item is None: ## measure pixel dimensions in view box
##print "children bounding rect:" px, py = [v.length() if v is not None else 0 for v in self.childGroup.pixelVectors()]
#item = self.childGroup
## First collect all boundary information
range = [None, None] itemBounds = []
for item in items: for item in items:
if not item.isVisible(): if not item.isVisible():
continue continue
useX = True useX = True
useY = True useY = True
if hasattr(item, 'dataBounds'): if hasattr(item, 'dataBounds'):
bounds = self._itemBoundsCache.get(item, None) #bounds = self._itemBoundsCache.get(item, None)
if bounds is None: #if bounds is None:
if frac is None: if frac is None:
frac = (1.0, 1.0) frac = (1.0, 1.0)
xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1])
if xr is None or xr == (None, None): pxPad = 0 if not hasattr(item, 'pixelPadding') else item.pixelPadding()
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])
bounds = self.mapFromItemToView(item, bounds).boundingRect() bounds = self.mapFromItemToView(item, bounds).boundingRect()
self._itemBoundsCache[item] = (bounds, useX, useY)
else: if not any([useX, useY]):
bounds, useX, useY = bounds continue
## If we are ignoring only one axis, we need to check for rotations
if useX != useY: ## != means xor
ang = round(item.transformAngle())
if ang == 0 or ang == 180:
pass
elif ang == 90 or ang == 270:
useX, useY = useY, useX
else:
## Item is rotated at non-orthogonal angle, ignore bounds entirely.
## Not really sure what is the expected behavior in this case.
continue ## need to check for item rotations and decide how best to apply this boundary.
itemBounds.append((bounds, useX, useY, pxPad))
#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
else: else:
bounds = item.boundingRect() bounds = item.boundingRect()
bounds = self.mapFromItemToView(item, bounds).boundingRect() bounds = self.mapFromItemToView(item, bounds).boundingRect()
itemBounds.append((bounds, True, True, 0))
prof.mark('1')
#print itemBounds
if not any([useX, useY]):
continue ## determine tentative new range
range = [None, None]
if useX != useY: ## != means xor for bounds, useX, useY, px in itemBounds:
ang = item.transformAngle()
if ang == 0 or ang == 180:
pass
elif ang == 90 or ang == 270:
useX, useY = useY, useX
else:
continue ## need to check for item rotations and decide how best to apply this boundary.
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])]
@ -1065,7 +1077,32 @@ class ViewBox(GraphicsWidget):
else: else:
range[0] = [bounds.left(), bounds.right()] range[0] = [bounds.left(), bounds.right()]
prof.mark('2') prof.mark('2')
#print "range", range
## Now expand any bounds that have a pixel margin
## This must be done _after_ we have a good estimate of the new range
## to ensure that the pixel size is roughly accurate.
w = self.width()
h = self.height()
#print "w:", w, "h:", h
if w > 0 and range[0] is not None:
pxSize = (range[0][1] - range[0][0]) / w
for bounds, useX, useY, px in itemBounds:
if px == 0 or not useX:
continue
range[0][0] = min(range[0][0], bounds.left() - px*pxSize)
range[0][1] = max(range[0][1], bounds.right() + px*pxSize)
if h > 0 and range[1] is not None:
pxSize = (range[1][1] - range[1][0]) / h
for bounds, useX, useY, px in itemBounds:
if px == 0 or not useY:
continue
range[1][0] = min(range[1][0], bounds.top() - px*pxSize)
range[1][1] = max(range[1][1], bounds.bottom() + px*pxSize)
#print "final range", range
prof.finish() prof.finish()
return range return range
@ -1083,6 +1120,8 @@ class ViewBox(GraphicsWidget):
def updateMatrix(self, changed=None): def updateMatrix(self, changed=None):
## Make the childGroup's transform match the requested range.
if changed is None: if changed is None:
changed = [False, False] changed = [False, False]
changed = list(changed) changed = list(changed)
@ -1198,7 +1237,7 @@ class ViewBox(GraphicsWidget):
if ViewBox is None: ## can happen as python is shutting down if ViewBox is None: ## can happen as python is shutting down
return return
## Called with ID and name of view (the view itself is no longer available) ## Called with ID and name of view (the view itself is no longer available)
for v in ViewBox.AllViews.keys(): for v in list(ViewBox.AllViews.keys()):
if id(v) == vid: if id(v) == vid:
ViewBox.AllViews.pop(v) ViewBox.AllViews.pop(v)
break break

View File

@ -2,15 +2,28 @@
import sys, pickle, os import sys, pickle, os
if __name__ == '__main__': if __name__ == '__main__':
os.setpgrp() ## prevents signals (notably keyboard interrupt) being forwarded from parent to this process if hasattr(os, 'setpgrp'):
name, port, authkey, targetStr, path = pickle.load(sys.stdin) os.setpgrp() ## prevents signals (notably keyboard interrupt) being forwarded from parent to this process
if sys.version[0] == '3':
#name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin.buffer)
opts = pickle.load(sys.stdin.buffer)
else:
#name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin)
opts = pickle.load(sys.stdin)
#print "key:", ' '.join([str(ord(x)) for x in authkey])
path = opts.pop('path', None)
if path is not None: if path is not None:
## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list. ## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list.
while len(sys.path) > 0: while len(sys.path) > 0:
sys.path.pop() sys.path.pop()
sys.path.extend(path) sys.path.extend(path)
if opts.pop('pyside', False):
import PySide
#import pyqtgraph #import pyqtgraph
#import pyqtgraph.multiprocess.processes #import pyqtgraph.multiprocess.processes
targetStr = opts.pop('targetStr')
target = pickle.loads(targetStr) ## unpickling the target should import everything we need target = pickle.loads(targetStr) ## unpickling the target should import everything we need
target(name, port, authkey) #target(name, port, authkey, ppid)
target(**opts) ## Send all other options to the target function
sys.exit(0) sys.exit(0)

View File

@ -1,6 +1,6 @@
import os, sys, time, multiprocessing, re import os, sys, time, multiprocessing, re
from processes import ForkedProcess from .processes import ForkedProcess
from remoteproxy import ClosedError 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."""
@ -19,7 +19,7 @@ class Parallelize(object):
for task in tasks: for task in tasks:
result = processTask(task) result = processTask(task)
results.append(result) results.append(result)
print results print(results)
## Here is the parallelized version: ## Here is the parallelized version:
@ -30,7 +30,7 @@ class Parallelize(object):
for task in tasker: for task in tasker:
result = processTask(task) result = processTask(task)
tasker.results.append(result) tasker.results.append(result)
print results print(results)
The only major caveat is that *result* in the example above must be picklable, The only major caveat is that *result* in the example above must be picklable,

View File

@ -1,7 +1,11 @@
from remoteproxy import RemoteEventHandler, ClosedError, 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 multiprocessing.connection import multiprocessing.connection
from pyqtgraph.Qt import USE_PYSIDE
try:
import cPickle as pickle
except ImportError:
import pickle
__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ClosedError', 'NoResultError'] __all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ClosedError', 'NoResultError']
@ -31,7 +35,7 @@ class Process(RemoteEventHandler):
ProxyObject for more information. ProxyObject for more information.
""" """
def __init__(self, name=None, target=None, executable=None, copySysPath=True): def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False):
""" """
============ ============================================================= ============ =============================================================
Arguments: Arguments:
@ -42,7 +46,9 @@ class Process(RemoteEventHandler):
process to process requests from the parent process until it process to process requests from the parent process until it
is asked to quit. If you wish to specify a different target, is asked to quit. If you wish to specify a different target,
it must be picklable (bound methods are not). it must be picklable (bound methods are not).
copySysPath If true, copy the contents of sys.path to the remote process copySysPath If True, copy the contents of sys.path to the remote process
debug If True, print detailed information about communication
with the child process.
============ ============================================================= ============ =============================================================
""" """
@ -52,14 +58,16 @@ class Process(RemoteEventHandler):
name = str(self) name = str(self)
if executable is None: if executable is None:
executable = sys.executable executable = sys.executable
self.debug = debug
## random authentication key ## random authentication key
authkey = ''.join([chr(random.getrandbits(7)) for i in range(20)]) authkey = os.urandom(20)
#print "key:", ' '.join([str(ord(x)) for x in authkey])
## Listen for connection from remote process (and find free port number) ## Listen for connection from remote process (and find free port number)
port = 10000 port = 10000
while True: while True:
try: try:
## hmac authentication appears to be broken on windows (says AuthenticationError: digest received was wrong)
l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey) l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey)
break break
except socket.error as ex: except socket.error as ex:
@ -70,19 +78,46 @@ class Process(RemoteEventHandler):
## start remote process, instruct it to run target function ## start remote process, instruct it to run target function
sysPath = sys.path if copySysPath else None sysPath = sys.path if copySysPath else None
bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py')) bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py'))
self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap))
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE) self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE)
targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to
## set its sys.path properly before unpickling the target ## set its sys.path properly before unpickling the target
pickle.dump((name+'_child', port, authkey, targetStr, sysPath), self.proc.stdin) pid = os.getpid() # we must send pid to child because windows does not have getppid
pyside = USE_PYSIDE
## Send everything the remote process needs to start correctly
data = dict(
name=name+'_child',
port=port,
authkey=authkey,
ppid=pid,
targetStr=targetStr,
path=sysPath,
pyside=pyside,
debug=debug
)
pickle.dump(data, self.proc.stdin)
self.proc.stdin.close() self.proc.stdin.close()
## open connection for remote process ## open connection for remote process
conn = l.accept() self.debugMsg('Listening for child process..')
RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid) while True:
try:
conn = l.accept()
break
except IOError as err:
if err.errno == 4: # interrupted; try again
continue
else:
raise
RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid, debug=debug)
self.debugMsg('Connected to child process.')
atexit.register(self.join) atexit.register(self.join)
def join(self, timeout=10): def join(self, timeout=10):
self.debugMsg('Joining child process..')
if self.proc.poll() is None: if self.proc.poll() is None:
self.close() self.close()
start = time.time() start = time.time()
@ -90,12 +125,14 @@ class Process(RemoteEventHandler):
if timeout is not None and time.time() - start > timeout: if timeout is not None and time.time() - start > timeout:
raise Exception('Timed out waiting for remote process to end.') raise Exception('Timed out waiting for remote process to end.')
time.sleep(0.05) time.sleep(0.05)
self.debugMsg('Child process exited. (%d)' % self.proc.returncode)
def startEventLoop(name, port, authkey): def startEventLoop(name, port, authkey, ppid, debug=False):
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
global HANDLER global HANDLER
HANDLER = RemoteEventHandler(conn, name, os.getppid()) #ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
HANDLER = RemoteEventHandler(conn, name, ppid, debug=debug)
while True: while True:
try: try:
HANDLER.processRequests() # exception raised when the loop should exit HANDLER.processRequests() # exception raised when the loop should exit
@ -161,6 +198,7 @@ class ForkedProcess(RemoteEventHandler):
proxyId = LocalObjectProxy.registerObject(v) proxyId = LocalObjectProxy.registerObject(v)
proxyIDs[k] = proxyId proxyIDs[k] = proxyId
ppid = os.getpid() # write this down now; windows doesn't have getppid
pid = os.fork() pid = os.fork()
if pid == 0: if pid == 0:
self.isParent = False self.isParent = False
@ -200,9 +238,9 @@ class ForkedProcess(RemoteEventHandler):
if 'random' in sys.modules: if 'random' in sys.modules:
sys.modules['random'].seed(os.getpid() ^ int(time.time()*10000%10000)) sys.modules['random'].seed(os.getpid() ^ int(time.time()*10000%10000))
RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=os.getppid()) #ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=ppid)
ppid = os.getppid()
self.forkedProxies = {} self.forkedProxies = {}
for name, proxyId in proxyIDs.iteritems(): for name, proxyId in proxyIDs.iteritems():
self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name])) self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name]))
@ -228,7 +266,7 @@ class ForkedProcess(RemoteEventHandler):
except ClosedError: except ClosedError:
break break
except: except:
print "Error occurred in forked event loop:" print("Error occurred in forked event loop:")
sys.excepthook(*sys.exc_info()) sys.excepthook(*sys.exc_info())
sys.exit(0) sys.exit(0)
@ -293,7 +331,7 @@ class QtProcess(Process):
btn.show() btn.show()
def slot(): def slot():
print 'slot invoked on parent process' print('slot invoked on parent process')
btn.clicked.connect(proxy(slot)) # be sure to send a proxy of the slot btn.clicked.connect(proxy(slot)) # be sure to send a proxy of the slot
""" """
@ -318,7 +356,7 @@ class QtProcess(Process):
except ClosedError: except ClosedError:
self.timer.stop() self.timer.stop()
def startQtEventLoop(name, port, authkey): def startQtEventLoop(name, port, authkey, ppid, debug=False):
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore
#from PyQt4 import QtGui, QtCore #from PyQt4 import QtGui, QtCore
@ -330,7 +368,8 @@ def startQtEventLoop(name, port, authkey):
## until it is explicitly closed by the parent process. ## until it is explicitly closed by the parent process.
global HANDLER global HANDLER
HANDLER = RemoteQtEventHandler(conn, name, os.getppid()) #ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
HANDLER = RemoteQtEventHandler(conn, name, ppid, debug=debug)
HANDLER.startEventTimer() HANDLER.startEventTimer()
app.exec_() app.exec_()

View File

@ -1,6 +1,11 @@
import os, __builtin__, time, sys, traceback, weakref import os, time, sys, traceback, weakref
import cPickle as pickle
import numpy as np import numpy as np
try:
import __builtin__ as builtins
import cPickle as pickle
except ImportError:
import builtins
import pickle
class ClosedError(Exception): class ClosedError(Exception):
"""Raised when an event handler receives a request to close the connection """Raised when an event handler receives a request to close the connection
@ -37,7 +42,8 @@ class RemoteEventHandler(object):
handlers = {} ## maps {process ID : handler}. This allows unpickler to determine which process handlers = {} ## maps {process ID : handler}. This allows unpickler to determine which process
## an object proxy belongs to ## an object proxy belongs to
def __init__(self, connection, name, pid): def __init__(self, connection, name, pid, debug=False):
self.debug = debug
self.conn = connection self.conn = connection
self.name = name self.name = name
self.results = {} ## reqId: (status, result); cache of request results received from the remote process self.results = {} ## reqId: (status, result); cache of request results received from the remote process
@ -68,9 +74,14 @@ class RemoteEventHandler(object):
try: try:
return cls.handlers[pid] return cls.handlers[pid]
except: except:
print pid, cls.handlers print(pid, cls.handlers)
raise raise
def debugMsg(self, msg):
if not self.debug:
return
print("[%d] %s" % (os.getpid(), str(msg)))
def getProxyOption(self, opt): def getProxyOption(self, opt):
return self.proxyOptions[opt] return self.proxyOptions[opt]
@ -86,7 +97,9 @@ class RemoteEventHandler(object):
after no more events are immediately available. (non-blocking) after no more events are immediately available. (non-blocking)
Returns the number of events processed. Returns the number of events processed.
""" """
self.debugMsg('processRequests:')
if self.exited: if self.exited:
self.debugMsg(' processRequests: exited already; raise ClosedError.')
raise ClosedError() raise ClosedError()
numProcessed = 0 numProcessed = 0
@ -95,37 +108,64 @@ class RemoteEventHandler(object):
self.handleRequest() self.handleRequest()
numProcessed += 1 numProcessed += 1
except ClosedError: except ClosedError:
self.debugMsg(' processRequests: got ClosedError from handleRequest; setting exited=True.')
self.exited = True self.exited = True
raise raise
except IOError as err: #except IOError as err: ## let handleRequest take care of this.
if err.errno == 4: ## interrupted system call; try again #self.debugMsg(' got IOError from handleRequest; try again.')
continue #if err.errno == 4: ## interrupted system call; try again
else: #continue
raise #else:
#raise
except: except:
print "Error in process %s" % self.name print("Error in process %s" % self.name)
sys.excepthook(*sys.exc_info()) sys.excepthook(*sys.exc_info())
self.debugMsg(' processRequests: finished %d requests' % numProcessed)
return numProcessed return numProcessed
def handleRequest(self): def handleRequest(self):
"""Handle a single request from the remote process. """Handle a single request from the remote process.
Blocks until a request is available.""" Blocks until a request is available."""
result = None result = None
try: while True:
cmd, reqId, nByteMsgs, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails try:
except (EOFError, IOError): ## args, kwds are double-pickled to ensure this recv() call never fails
## remote process has shut down; end event loop cmd, reqId, nByteMsgs, optStr = self.conn.recv()
raise ClosedError() break
#print os.getpid(), "received request:", cmd, reqId except EOFError:
self.debugMsg(' handleRequest: got EOFError from recv; raise ClosedError.')
## remote process has shut down; end event loop
raise ClosedError()
except IOError as err:
if err.errno == 4: ## interrupted system call; try again
self.debugMsg(' handleRequest: got IOError 4 from recv; try again.')
continue
else:
self.debugMsg(' handleRequest: got IOError %d from recv (%s); raise ClosedError.' % (err.errno, err.strerror))
raise ClosedError()
self.debugMsg(" handleRequest: received %s %s" % (str(cmd), str(reqId)))
## read byte messages following the main request ## read byte messages following the main request
byteData = [] byteData = []
if nByteMsgs > 0:
self.debugMsg(" handleRequest: reading %d byte messages" % nByteMsgs)
for i in range(nByteMsgs): for i in range(nByteMsgs):
try: while True:
byteData.append(self.conn.recv_bytes()) try:
except (EOFError, IOError): byteData.append(self.conn.recv_bytes())
raise ClosedError() break
except EOFError:
self.debugMsg(" handleRequest: got EOF while reading byte messages; raise ClosedError.")
raise ClosedError()
except IOError as err:
if err.errno == 4:
self.debugMsg(" handleRequest: got IOError 4 while reading byte messages; try again.")
continue
else:
self.debugMsg(" handleRequest: got IOError while reading byte messages; raise ClosedError.")
raise ClosedError()
try: try:
@ -135,6 +175,7 @@ class RemoteEventHandler(object):
## (this is already a return from a previous request) ## (this is already a return from a previous request)
opts = pickle.loads(optStr) opts = pickle.loads(optStr)
self.debugMsg(" handleRequest: id=%s opts=%s" % (str(reqId), str(opts)))
#print os.getpid(), "received request:", cmd, reqId, opts #print os.getpid(), "received request:", cmd, reqId, opts
returnType = opts.get('returnType', 'auto') returnType = opts.get('returnType', 'auto')
@ -181,7 +222,7 @@ class RemoteEventHandler(object):
elif cmd == 'import': elif cmd == 'import':
name = opts['module'] name = opts['module']
fromlist = opts.get('fromlist', []) fromlist = opts.get('fromlist', [])
mod = __builtin__.__import__(name, fromlist=fromlist) mod = builtins.__import__(name, fromlist=fromlist)
if len(fromlist) == 0: if len(fromlist) == 0:
parts = name.lstrip('.').split('.') parts = name.lstrip('.').split('.')
@ -208,6 +249,7 @@ class RemoteEventHandler(object):
if reqId is not None: if reqId is not None:
if exc is None: if exc is None:
self.debugMsg(" handleRequest: sending return value for %d: %s" % (reqId, str(result)))
#print "returnValue:", returnValue, result #print "returnValue:", returnValue, result
if returnType == 'auto': if returnType == 'auto':
result = self.autoProxy(result, self.proxyOptions['noProxyTypes']) result = self.autoProxy(result, self.proxyOptions['noProxyTypes'])
@ -220,6 +262,7 @@ class RemoteEventHandler(object):
sys.excepthook(*sys.exc_info()) sys.excepthook(*sys.exc_info())
self.replyError(reqId, *sys.exc_info()) self.replyError(reqId, *sys.exc_info())
else: else:
self.debugMsg(" handleRequest: returning exception for %d" % reqId)
self.replyError(reqId, *exc) self.replyError(reqId, *exc)
elif exc is not None: elif exc is not None:
@ -239,7 +282,7 @@ class RemoteEventHandler(object):
self.send(request='result', reqId=reqId, callSync='off', opts=dict(result=result)) self.send(request='result', reqId=reqId, callSync='off', opts=dict(result=result))
def replyError(self, reqId, *exc): def replyError(self, reqId, *exc):
print "error:", self.name, reqId, exc[1] print("error: %s %s %s" % (self.name, str(reqId), str(exc[1])))
excStr = traceback.format_exception(*exc) excStr = traceback.format_exception(*exc)
try: try:
self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=exc[1], excString=excStr)) self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=exc[1], excString=excStr))
@ -352,9 +395,9 @@ class RemoteEventHandler(object):
try: try:
optStr = pickle.dumps(opts) optStr = pickle.dumps(opts)
except: except:
print "==== Error pickling this object: ====" print("==== Error pickling this object: ====")
print opts print(opts)
print "=======================================" print("=======================================")
raise raise
nByteMsgs = 0 nByteMsgs = 0
@ -363,13 +406,16 @@ class RemoteEventHandler(object):
## Send primary request ## Send primary request
request = (request, reqId, nByteMsgs, optStr) request = (request, reqId, nByteMsgs, optStr)
self.debugMsg('send request: cmd=%s nByteMsgs=%d id=%s opts=%s' % (str(request[0]), nByteMsgs, str(reqId), str(opts)))
self.conn.send(request) self.conn.send(request)
## follow up by sending byte messages ## follow up by sending byte messages
if byteData is not None: if byteData is not None:
for obj in byteData: ## Remote process _must_ be prepared to read the same number of byte messages! for obj in byteData: ## Remote process _must_ be prepared to read the same number of byte messages!
self.conn.send_bytes(obj) self.conn.send_bytes(obj)
self.debugMsg(' sent %d byte messages' % len(byteData))
self.debugMsg(' call sync: %s' % callSync)
if callSync == 'off': if callSync == 'off':
return return
@ -404,12 +450,12 @@ class RemoteEventHandler(object):
#print ''.join(result) #print ''.join(result)
exc, excStr = result exc, excStr = result
if exc is not None: if exc is not None:
print "===== Remote process raised exception on request: =====" print("===== Remote process raised exception on request: =====")
print ''.join(excStr) print(''.join(excStr))
print "===== Local Traceback to request follows: =====" print("===== Local Traceback to request follows: =====")
raise exc raise exc
else: else:
print ''.join(excStr) print(''.join(excStr))
raise Exception("Error getting result. See above for exception from remote process.") raise Exception("Error getting result. See above for exception from remote process.")
else: else:
@ -535,7 +581,7 @@ class Request(object):
raise ClosedError() 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: %s" % self.description)
import traceback import traceback
traceback.print_stack() traceback.print_stack()
raise NoResultError() raise NoResultError()

View File

@ -6,10 +6,10 @@ class GLTest(QtOpenGL.QGLWidget):
def __init__(self): def __init__(self):
QtOpenGL.QGLWidget.__init__(self) QtOpenGL.QGLWidget.__init__(self)
self.makeCurrent() self.makeCurrent()
print "GL version:", glGetString(GL_VERSION) print("GL version:" + glGetString(GL_VERSION))
print "MAX_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_TEXTURE_SIZE) print("MAX_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_TEXTURE_SIZE))
print "MAX_3D_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE) print("MAX_3D_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE))
print "Extensions:", glGetString(GL_EXTENSIONS) print("Extensions: " + glGetString(GL_EXTENSIONS))
GLTest() GLTest()

View File

@ -127,8 +127,8 @@ class GLSurfacePlotItem(GLMeshItem):
def generateFaces(self): def generateFaces(self):
cols = self._z.shape[0]-1 cols = self._z.shape[1]-1
rows = self._z.shape[1]-1 rows = self._z.shape[0]-1
faces = np.empty((cols*rows*2, 3), dtype=np.uint) faces = np.empty((cols*rows*2, 3), dtype=np.uint)
rowtemplate1 = np.arange(cols).reshape(cols, 1) + np.array([[0, 1, cols+1]]) rowtemplate1 = np.arange(cols).reshape(cols, 1) + np.array([[0, 1, cols+1]])
rowtemplate2 = np.arange(cols).reshape(cols, 1) + np.array([[cols+1, 1, cols+2]]) rowtemplate2 = np.arange(cols).reshape(cols, 1) + np.array([[cols+1, 1, cols+2]])

View File

@ -88,9 +88,10 @@ class Parameter(QtCore.QObject):
@staticmethod @staticmethod
def create(**opts): def create(**opts):
""" """
Create a new Parameter (or subclass) instance using opts['type'] to select the Static method that creates a new Parameter (or subclass) instance using
appropriate class. opts['type'] to select the appropriate class.
All options are passed directly to the new Parameter's __init__ method.
Use registerParameterType() to add new class types. Use registerParameterType() to add new class types.
""" """
typ = opts.get('type', None) typ = opts.get('type', None)
@ -101,6 +102,41 @@ class Parameter(QtCore.QObject):
return cls(**opts) return cls(**opts)
def __init__(self, **opts): def __init__(self, **opts):
"""
Initialize a Parameter object. Although it is rare to directly create a
Parameter instance, the options available to this method are also allowed
by most Parameter subclasses.
================= =========================================================
Keyword Arguments
name The name to give this Parameter. This is the name that
will appear in the left-most column of a ParameterTree
for this Parameter.
value The value to initially assign to this Parameter.
default The default value for this Parameter (most Parameters
provide an option to 'reset to default').
children A list of children for this Parameter. Children
may be given either as a Parameter instance or as a
dictionary to pass to Parameter.create(). In this way,
it is possible to specify complex hierarchies of
Parameters from a single nested data structure.
readonly If True, the user will not be allowed to edit this
Parameter. (default=False)
enabled If False, any widget(s) for this parameter will appear
disabled. (default=True)
visible If False, the Parameter will not appear when displayed
in a ParameterTree. (default=True)
renamable If True, the user may rename this Parameter.
(default=False)
removable If True, the user may remove this Parameter.
(default=False)
expanded If True, the Parameter will appear expanded when
displayed in a ParameterTree (its children will be
visible). (default=True)
================= =========================================================
"""
QtCore.QObject.__init__(self) QtCore.QObject.__init__(self)
self.opts = { self.opts = {
@ -111,6 +147,7 @@ class Parameter(QtCore.QObject):
'renamable': False, 'renamable': False,
'removable': False, 'removable': False,
'strictNaming': False, # forces name to be usable as a python variable 'strictNaming': False, # forces name to be usable as a python variable
'expanded': True,
#'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits. #'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits.
} }
self.opts.update(opts) self.opts.update(opts)
@ -148,6 +185,7 @@ class Parameter(QtCore.QObject):
#self.watchParam(self) ## emit treechange signals if our own state changes #self.watchParam(self) ## emit treechange signals if our own state changes
def name(self): def name(self):
"""Return the name of this Parameter."""
return self.opts['name'] return self.opts['name']
def setName(self, name): def setName(self, name):
@ -165,6 +203,7 @@ class Parameter(QtCore.QObject):
return name return name
def type(self): def type(self):
"""Return the type string for this Parameter."""
return self.opts['type'] return self.opts['type']
def isType(self, typ): def isType(self, typ):
@ -197,8 +236,10 @@ class Parameter(QtCore.QObject):
return path return path
def setValue(self, value, blockSignal=None): def setValue(self, value, blockSignal=None):
## return the actual value that was set """
## (this may be different from the value that was requested) Set the value of this Parameter; return the actual value that was set.
(this may be different from the value that was requested)
"""
try: try:
if blockSignal is not None: if blockSignal is not None:
self.sigValueChanged.disconnect(blockSignal) self.sigValueChanged.disconnect(blockSignal)
@ -213,6 +254,9 @@ class Parameter(QtCore.QObject):
return value return value
def value(self): def value(self):
"""
Return the value of this Parameter.
"""
return self.opts['value'] return self.opts['value']
def getValues(self): def getValues(self):
@ -352,9 +396,12 @@ class Parameter(QtCore.QObject):
return not self.opts.get('readonly', False) return not self.opts.get('readonly', False)
def setWritable(self, writable=True): def setWritable(self, writable=True):
"""Set whether this Parameter should be editable by the user. (This is
exactly the opposite of setReadonly)."""
self.setOpts(readonly=not writable) self.setOpts(readonly=not writable)
def setReadonly(self, readonly=True): def setReadonly(self, readonly=True):
"""Set whether this Parameter's value may be edited by the user."""
self.setOpts(readonly=readonly) self.setOpts(readonly=readonly)
def setOpts(self, **opts): def setOpts(self, **opts):
@ -362,7 +409,10 @@ class Parameter(QtCore.QObject):
Set any arbitrary options on this parameter. Set any arbitrary options on this parameter.
The exact behavior of this function will depend on the parameter type, but The exact behavior of this function will depend on the parameter type, but
most parameters will accept a common set of options: value, name, limits, most parameters will accept a common set of options: value, name, limits,
default, readonly, removable, renamable, visible, and enabled. default, readonly, removable, renamable, visible, enabled, and expanded.
See :func:`Parameter.__init__ <pyqtgraph.parametertree.Parameter.__init__>`
for more information on default options.
""" """
changed = OrderedDict() changed = OrderedDict()
for k in opts: for k in opts:
@ -390,7 +440,10 @@ class Parameter(QtCore.QObject):
self.emitTreeChanges() self.emitTreeChanges()
def makeTreeItem(self, depth): def makeTreeItem(self, depth):
"""Return a TreeWidgetItem suitable for displaying/controlling the content of this parameter. """
Return a TreeWidgetItem suitable for displaying/controlling the content of
this parameter. This is called automatically when a ParameterTree attempts
to display this Parameter.
Most subclasses will want to override this function. Most subclasses will want to override this function.
""" """
if hasattr(self, 'itemClass'): if hasattr(self, 'itemClass'):
@ -424,7 +477,8 @@ class Parameter(QtCore.QObject):
""" """
Insert a new child at pos. Insert a new child at pos.
If pos is a Parameter, then insert at the position of that Parameter. If pos is a Parameter, then insert at the position of that Parameter.
If child is a dict, then a parameter is constructed as Parameter(\*\*child) If child is a dict, then a parameter is constructed using
:func:`Parameter.create <pyqtgraph.parametertree.Parameter.create>`.
""" """
if isinstance(child, dict): if isinstance(child, dict):
child = Parameter.create(**child) child = Parameter.create(**child)
@ -471,11 +525,13 @@ class Parameter(QtCore.QObject):
self.removeChild(ch) self.removeChild(ch)
def children(self): def children(self):
"""Return a list of this parameter's children.""" """Return a list of this parameter's children.
## warning -- this overrides QObject.children Warning: this overrides QObject.children
"""
return self.childs[:] return self.childs[:]
def hasChildren(self): def hasChildren(self):
"""Return True if this Parameter has children."""
return len(self.childs) > 0 return len(self.childs) > 0
def parentChanged(self, parent): def parentChanged(self, parent):
@ -553,9 +609,13 @@ class Parameter(QtCore.QObject):
def __getattr__(self, attr): def __getattr__(self, attr):
## Leaving this undocumented because I might like to remove it in the future.. ## Leaving this undocumented because I might like to remove it in the future..
#print type(self), attr #print type(self), attr
if 'names' not in self.__dict__: if 'names' not in self.__dict__:
raise AttributeError(attr) raise AttributeError(attr)
if attr in self.names: if attr in self.names:
import traceback
traceback.print_stack()
print("Warning: Use of Parameter.subParam is deprecated. Use Parameter.param(name) instead.")
return self.param(attr) return self.param(attr)
else: else:
raise AttributeError(attr) raise AttributeError(attr)
@ -582,36 +642,6 @@ class Parameter(QtCore.QObject):
self.sigOptionsChanged.emit(self, {'visible': s}) self.sigOptionsChanged.emit(self, {'visible': s})
#def monitorChildren(self):
#if self.monitoringChildren:
#raise Exception("Already monitoring children.")
#self.watchParam(self)
#self.monitoringChildren = True
#def watchParam(self, param):
#param.sigChildAdded.connect(self.grandchildAdded)
#param.sigChildRemoved.connect(self.grandchildRemoved)
#param.sigStateChanged.connect(self.grandchildChanged)
#for ch in param:
#self.watchParam(ch)
#def unwatchParam(self, param):
#param.sigChildAdded.disconnect(self.grandchildAdded)
#param.sigChildRemoved.disconnect(self.grandchildRemoved)
#param.sigStateChanged.disconnect(self.grandchildChanged)
#for ch in param:
#self.unwatchParam(ch)
#def grandchildAdded(self, parent, child):
#self.watchParam(child)
#def grandchildRemoved(self, parent, child):
#self.unwatchParam(child)
#def grandchildChanged(self, param, change, data):
##self.sigTreeStateChanged.emit(self, param, change, data)
#self.emitTreeChange((param, change, data))
def treeChangeBlocker(self): def treeChangeBlocker(self):
""" """
Return an object that can be used to temporarily block and accumulate Return an object that can be used to temporarily block and accumulate

View File

@ -4,6 +4,7 @@ from .Parameter import Parameter, registerParameterType
from .ParameterItem import ParameterItem from .ParameterItem import ParameterItem
from pyqtgraph.widgets.SpinBox import SpinBox from pyqtgraph.widgets.SpinBox import SpinBox
from pyqtgraph.widgets.ColorButton import ColorButton from pyqtgraph.widgets.ColorButton import ColorButton
#from pyqtgraph.widgets.GradientWidget import GradientWidget ## creates import loop
import pyqtgraph as pg import pyqtgraph as pg
import pyqtgraph.pixmaps as pixmaps import pyqtgraph.pixmaps as pixmaps
import os import os
@ -13,10 +14,20 @@ class WidgetParameterItem(ParameterItem):
""" """
ParameterTree item with: ParameterTree item with:
- label in second column for displaying value * label in second column for displaying value
- simple widget for editing value (displayed instead of label when item is selected) * simple widget for editing value (displayed instead of label when item is selected)
- button that resets value to default * button that resets value to default
- provides SpinBox, CheckBox, LineEdit, and ColorButton types
================= =============================================================
Registered Types:
int Displays a :class:`SpinBox <pyqtgraph.SpinBox>` in integer
mode.
float Displays a :class:`SpinBox <pyqtgraph.SpinBox>`.
bool Displays a QCheckBox
str Displays a QLineEdit
color Displays a :class:`ColorButton <pyqtgraph.ColorButton>`
colormap Displays a :class:`GradientWidget <pyqtgraph.GradientWidget>`
================= =============================================================
This class can be subclassed by overriding makeWidget() to provide a custom widget. This class can be subclassed by overriding makeWidget() to provide a custom widget.
""" """
@ -61,7 +72,11 @@ class WidgetParameterItem(ParameterItem):
w.sigChanging.connect(self.widgetValueChanging) w.sigChanging.connect(self.widgetValueChanging)
## update value shown in widget. ## update value shown in widget.
self.valueChanged(self, opts['value'], force=True) if opts.get('value', None) is not None:
self.valueChanged(self, opts['value'], force=True)
else:
## no starting value was given; use whatever the widget has
self.widgetValueChanged()
def makeWidget(self): def makeWidget(self):
@ -125,6 +140,14 @@ class WidgetParameterItem(ParameterItem):
w.setValue = w.setColor w.setValue = w.setColor
self.hideWidget = False self.hideWidget = False
w.setFlat(True) w.setFlat(True)
elif t == 'colormap':
from pyqtgraph.widgets.GradientWidget import GradientWidget ## need this here to avoid import loop
w = GradientWidget(orientation='bottom')
w.sigChanged = w.sigGradientChangeFinished
w.sigChanging = w.sigGradientChanged
w.value = w.colorMap
w.setValue = w.setColorMap
self.hideWidget = False
else: else:
raise Exception("Unknown type '%s'" % asUnicode(t)) raise Exception("Unknown type '%s'" % asUnicode(t))
return w return w
@ -294,6 +317,7 @@ registerParameterType('float', SimpleParameter, override=True)
registerParameterType('bool', SimpleParameter, override=True) registerParameterType('bool', SimpleParameter, override=True)
registerParameterType('str', SimpleParameter, override=True) registerParameterType('str', SimpleParameter, override=True)
registerParameterType('color', SimpleParameter, override=True) registerParameterType('color', SimpleParameter, override=True)
registerParameterType('colormap', SimpleParameter, override=True)

View File

@ -20,7 +20,7 @@ def unixTime():
"""Return the current time in seconds with high precision (unix version, use Manager.time() to stay platform independent).""" """Return the current time in seconds with high precision (unix version, use Manager.time() to stay platform independent)."""
return systime.time() return systime.time()
if 'win' in sys.platform: if sys.platform.startswith('win'):
cstart = systime.clock() ### Required to start the clock in windows cstart = systime.clock() ### Required to start the clock in windows
START_TIME = systime.time() - cstart START_TIME = systime.time() - cstart

View File

@ -13,11 +13,11 @@ for path, sd, files in os.walk('.'):
ui = os.path.join(path, f) ui = os.path.join(path, f)
py = os.path.join(path, base + '_pyqt.py') py = os.path.join(path, base + '_pyqt.py')
if os.stat(ui).st_mtime > os.stat(py).st_mtime: if not os.path.exists(py) or os.stat(ui).st_mtime > os.stat(py).st_mtime:
os.system('%s %s > %s' % (pyqtuic, ui, py)) os.system('%s %s > %s' % (pyqtuic, ui, py))
print(py) print(py)
py = os.path.join(path, base + '_pyside.py') py = os.path.join(path, base + '_pyside.py')
if os.stat(ui).st_mtime > os.stat(py).st_mtime: if not os.path.exists(py) or os.stat(ui).st_mtime > os.stat(py).st_mtime:
os.system('%s %s > %s' % (pysideuic, ui, py)) os.system('%s %s > %s' % (pysideuic, ui, py))
print(py) print(py)

View File

@ -267,14 +267,14 @@ class A(object):
object.__init__(self) object.__init__(self)
self.msg = msg self.msg = msg
def fn(self, pfx = ""): def fn(self, pfx = ""):
print pfx+"A class:", self.__class__, id(self.__class__) print(pfx+"A class: %%s %%s" %% (str(self.__class__), str(id(self.__class__))))
print pfx+" %%s: %d" %% self.msg print(pfx+" %%s: %d" %% self.msg)
class B(A): class B(A):
def fn(self, pfx=""): def fn(self, pfx=""):
print pfx+"B class:", self.__class__, id(self.__class__) print(pfx+"B class:", self.__class__, id(self.__class__))
print pfx+" %%s: %d" %% self.msg print(pfx+" %%s: %d" %% self.msg)
print pfx+" calling superclass.. (%%s)" %% id(A) print(pfx+" calling superclass.. (%%s)" %% id(A) )
A.fn(self, " ") A.fn(self, " ")
""" """
@ -294,7 +294,7 @@ class C(A):
A.__init__(self, msg + "(init from C)") A.__init__(self, msg + "(init from C)")
def fn(): def fn():
print "fn: %s" print("fn: %s")
""" """
open(modFile1, 'w').write(modCode1%(1,1)) open(modFile1, 'w').write(modCode1%(1,1))

View File

@ -77,8 +77,14 @@ class ColorButton(QtGui.QPushButton):
def restoreState(self, state): def restoreState(self, state):
self.setColor(state) self.setColor(state)
def color(self): def color(self, mode='qcolor'):
return functions.mkColor(self._color) color = functions.mkColor(self._color)
if mode == 'qcolor':
return color
elif mode == 'byte':
return (color.red(), color.green(), color.blue(), color.alpha())
elif mode == 'float':
return (color.red()/255., color.green()/255., color.blue()/255., color.alpha()/255.)
def widgetGroupInterface(self): def widgetGroupInterface(self):
return (self.sigColorChanged, ColorButton.saveState, ColorButton.restoreState) return (self.sigColorChanged, ColorButton.saveState, ColorButton.restoreState)

View File

@ -0,0 +1,209 @@
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.parametertree as ptree
import numpy as np
from pyqtgraph.pgcollections import OrderedDict
import pyqtgraph.functions as fn
__all__ = ['ColorMapWidget']
class ColorMapWidget(ptree.ParameterTree):
"""
This class provides a widget allowing the user to customize color mapping
for multi-column data. Given a list of field names, the user may specify
multiple criteria for assigning colors to each record in a numpy record array.
Multiple criteria are evaluated and combined into a single color for each
record by user-defined compositing methods.
For simpler color mapping using a single gradient editor, see
:class:`GradientWidget <pyqtgraph.GradientWidget>`
"""
sigColorMapChanged = QtCore.Signal(object)
def __init__(self):
ptree.ParameterTree.__init__(self, showHeader=False)
self.params = ColorMapParameter()
self.setParameters(self.params)
self.params.sigTreeStateChanged.connect(self.mapChanged)
## wrap a couple methods
self.setFields = self.params.setFields
self.map = self.params.map
def mapChanged(self):
self.sigColorMapChanged.emit(self)
class ColorMapParameter(ptree.types.GroupParameter):
sigColorMapChanged = QtCore.Signal(object)
def __init__(self):
self.fields = {}
ptree.types.GroupParameter.__init__(self, name='Color Map', addText='Add Mapping..', addList=[])
self.sigTreeStateChanged.connect(self.mapChanged)
def mapChanged(self):
self.sigColorMapChanged.emit(self)
def addNew(self, name):
mode = self.fields[name].get('mode', 'range')
if mode == 'range':
self.addChild(RangeColorMapItem(name, self.fields[name]))
elif mode == 'enum':
self.addChild(EnumColorMapItem(name, self.fields[name]))
def fieldNames(self):
return self.fields.keys()
def setFields(self, fields):
"""
Set the list of fields to be used by the mapper.
The format of *fields* is::
[ (fieldName, {options}), ... ]
============== ============================================================
Field Options:
mode Either 'range' or 'enum' (default is range). For 'range',
The user may specify a gradient of colors to be applied
linearly across a specific range of values. For 'enum',
the user specifies a single color for each unique value
(see *values* option).
units String indicating the units of the data for this field.
values List of unique values for which the user may assign a
color when mode=='enum'.
============== ============================================================
"""
self.fields = OrderedDict(fields)
#self.fields = fields
#self.fields.sort()
names = self.fieldNames()
self.setAddList(names)
def map(self, data, mode='byte'):
"""
Return an array of colors corresponding to *data*.
========= =================================================================
Arguments
data A numpy record array where the fields in data.dtype match those
defined by a prior call to setFields().
mode Either 'byte' or 'float'. For 'byte', the method returns an array
of dtype ubyte with values scaled 0-255. For 'float', colors are
returned as 0.0-1.0 float values.
========= =================================================================
"""
colors = np.zeros((len(data),4))
for item in self.children():
if not item['Enabled']:
continue
chans = item.param('Channels..')
mask = np.empty((len(data), 4), dtype=bool)
for i,f in enumerate(['Red', 'Green', 'Blue', 'Alpha']):
mask[:,i] = chans[f]
colors2 = item.map(data)
op = item['Operation']
if op == 'Add':
colors[mask] = colors[mask] + colors2[mask]
elif op == 'Multiply':
colors[mask] *= colors2[mask]
elif op == 'Overlay':
a = colors2[:,3:4]
c3 = colors * (1-a) + colors2 * a
c3[:,3:4] = colors[:,3:4] + (1-colors[:,3:4]) * a
colors = c3
elif op == 'Set':
colors[mask] = colors2[mask]
colors = np.clip(colors, 0, 1)
if mode == 'byte':
colors = (colors * 255).astype(np.ubyte)
return colors
class RangeColorMapItem(ptree.types.SimpleParameter):
def __init__(self, name, opts):
self.fieldName = name
units = opts.get('units', '')
ptree.types.SimpleParameter.__init__(self,
name=name, autoIncrementName=True, type='colormap', removable=True, renamable=True,
children=[
#dict(name="Field", type='list', value=name, values=fields),
dict(name='Min', type='float', value=0.0, suffix=units, siPrefix=True),
dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True),
dict(name='Operation', type='list', value='Overlay', values=['Overlay', 'Add', 'Multiply', 'Set']),
dict(name='Channels..', type='group', expanded=False, children=[
dict(name='Red', type='bool', value=True),
dict(name='Green', type='bool', value=True),
dict(name='Blue', type='bool', value=True),
dict(name='Alpha', type='bool', value=True),
]),
dict(name='Enabled', type='bool', value=True),
dict(name='NaN', type='color'),
])
def map(self, data):
data = data[self.fieldName]
scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1)
cmap = self.value()
colors = cmap.map(scaled, mode='float')
mask = np.isnan(data) | np.isinf(data)
nanColor = self['NaN']
nanColor = (nanColor.red()/255., nanColor.green()/255., nanColor.blue()/255., nanColor.alpha()/255.)
colors[mask] = nanColor
return colors
class EnumColorMapItem(ptree.types.GroupParameter):
def __init__(self, name, opts):
self.fieldName = name
vals = opts.get('values', [])
childs = [{'name': v, 'type': 'color'} for v in vals]
ptree.types.GroupParameter.__init__(self,
name=name, autoIncrementName=True, removable=True, renamable=True,
children=[
dict(name='Values', type='group', children=childs),
dict(name='Operation', type='list', value='Overlay', values=['Overlay', 'Add', 'Multiply', 'Set']),
dict(name='Channels..', type='group', expanded=False, children=[
dict(name='Red', type='bool', value=True),
dict(name='Green', type='bool', value=True),
dict(name='Blue', type='bool', value=True),
dict(name='Alpha', type='bool', value=True),
]),
dict(name='Enabled', type='bool', value=True),
dict(name='Default', type='color'),
])
def map(self, data):
data = data[self.fieldName]
colors = np.empty((len(data), 4))
default = np.array(fn.colorTuple(self['Default'])) / 255.
colors[:] = default
for v in self.param('Values'):
n = v.name()
mask = data == n
c = np.array(fn.colorTuple(v.value())) / 255.
colors[mask] = c
#scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1)
#cmap = self.value()
#colors = cmap.map(scaled, mode='float')
#mask = np.isnan(data) | np.isinf(data)
#nanColor = self['NaN']
#nanColor = (nanColor.red()/255., nanColor.green()/255., nanColor.blue()/255., nanColor.alpha()/255.)
#colors[mask] = nanColor
return colors

View File

@ -0,0 +1,115 @@
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.parametertree as ptree
import numpy as np
from pyqtgraph.pgcollections import OrderedDict
__all__ = ['DataFilterWidget']
class DataFilterWidget(ptree.ParameterTree):
"""
This class allows the user to filter multi-column data sets by specifying
multiple criteria
"""
sigFilterChanged = QtCore.Signal(object)
def __init__(self):
ptree.ParameterTree.__init__(self, showHeader=False)
self.params = DataFilterParameter()
self.setParameters(self.params)
self.params.sigTreeStateChanged.connect(self.filterChanged)
self.setFields = self.params.setFields
self.filterData = self.params.filterData
def filterChanged(self):
self.sigFilterChanged.emit(self)
def parameters(self):
return self.params
class DataFilterParameter(ptree.types.GroupParameter):
sigFilterChanged = QtCore.Signal(object)
def __init__(self):
self.fields = {}
ptree.types.GroupParameter.__init__(self, name='Data Filter', addText='Add filter..', addList=[])
self.sigTreeStateChanged.connect(self.filterChanged)
def filterChanged(self):
self.sigFilterChanged.emit(self)
def addNew(self, name):
mode = self.fields[name].get('mode', 'range')
if mode == 'range':
self.addChild(RangeFilterItem(name, self.fields[name]))
elif mode == 'enum':
self.addChild(EnumFilterItem(name, self.fields[name]))
def fieldNames(self):
return self.fields.keys()
def setFields(self, fields):
self.fields = OrderedDict(fields)
names = self.fieldNames()
self.setAddList(names)
def filterData(self, data):
if len(data) == 0:
return data
return data[self.generateMask(data)]
def generateMask(self, data):
mask = np.ones(len(data), dtype=bool)
if len(data) == 0:
return mask
for fp in self:
if fp.value() is False:
continue
mask &= fp.generateMask(data)
#key, mn, mx = fp.fieldName, fp['Min'], fp['Max']
#vals = data[key]
#mask &= (vals >= mn)
#mask &= (vals < mx) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
return mask
class RangeFilterItem(ptree.types.SimpleParameter):
def __init__(self, name, opts):
self.fieldName = name
units = opts.get('units', '')
ptree.types.SimpleParameter.__init__(self,
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True,
children=[
#dict(name="Field", type='list', value=name, values=fields),
dict(name='Min', type='float', value=0.0, suffix=units, siPrefix=True),
dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True),
])
def generateMask(self, data):
vals = data[self.fieldName]
return (vals >= mn) & (vals < mx) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
class EnumFilterItem(ptree.types.SimpleParameter):
def __init__(self, name, opts):
self.fieldName = name
vals = opts.get('values', [])
childs = [{'name': v, 'type': 'bool', 'value': True} for v in vals]
ptree.types.SimpleParameter.__init__(self,
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True,
children=childs)
def generateMask(self, data):
vals = data[self.fieldName]
mask = np.ones(len(data), dtype=bool)
for c in self:
if c.value() is True:
continue
key = c.name()
mask &= vals != key
return mask

View File

@ -9,11 +9,27 @@ __all__ = ['TickSlider', 'GradientWidget', 'BlackWhiteSlider']
class GradientWidget(GraphicsView): class GradientWidget(GraphicsView):
"""
Widget displaying an editable color gradient. The user may add, move, recolor,
or remove colors from the gradient. Additionally, a context menu allows the
user to select from pre-defined gradients.
"""
sigGradientChanged = QtCore.Signal(object) sigGradientChanged = QtCore.Signal(object)
sigGradientChangeFinished = QtCore.Signal(object) sigGradientChangeFinished = QtCore.Signal(object)
def __init__(self, parent=None, orientation='bottom', *args, **kargs): def __init__(self, parent=None, orientation='bottom', *args, **kargs):
"""
The *orientation* argument may be 'bottom', 'top', 'left', or 'right'
indicating whether the gradient is displayed horizontally (top, bottom)
or vertically (left, right) and on what side of the gradient the editable
ticks will appear.
All other arguments are passed to
:func:`GradientEditorItem.__init__ <pyqtgraph.GradientEditorItem.__init__>`.
Note: For convenience, this class wraps methods from
:class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
"""
GraphicsView.__init__(self, parent, useOpenGL=False, background=None) GraphicsView.__init__(self, parent, useOpenGL=False, background=None)
self.maxDim = 31 self.maxDim = 31
kargs['tickPen'] = 'k' kargs['tickPen'] = 'k'
@ -32,6 +48,8 @@ class GradientWidget(GraphicsView):
#self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, True) #self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, True)
def setOrientation(self, ort): def setOrientation(self, ort):
"""Set the orientation of the widget. May be one of 'bottom', 'top',
'left', or 'right'."""
self.item.setOrientation(ort) self.item.setOrientation(ort)
self.orientation = ort self.orientation = ort
self.setMaxDim() self.setMaxDim()

View File

@ -1,9 +1,9 @@
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
import pyqtgraph.multiprocess as mp import pyqtgraph.multiprocess as mp
import pyqtgraph as pg import pyqtgraph as pg
from .GraphicsView import GraphicsView from .GraphicsView import GraphicsView
import numpy as np import numpy as np
import mmap, tempfile, ctypes, atexit import mmap, tempfile, ctypes, atexit, sys, random
__all__ = ['RemoteGraphicsView'] __all__ = ['RemoteGraphicsView']
@ -21,19 +21,22 @@ class RemoteGraphicsView(QtGui.QWidget):
self._sizeHint = (640,480) ## no clue why this is needed, but it seems to be the default sizeHint for GraphicsView. 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. ## 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(debug=False)
self.pg = self._proc._import('pyqtgraph') self.pg = self._proc._import('pyqtgraph')
self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS) self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS)
rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView')
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(QtCore.Qt.StrongFocus)
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
self.setMouseTracking(True) self.setMouseTracking(True)
self.shm = None
shmFileName = self._view.shmFileName() shmFileName = self._view.shmFileName()
self.shmFile = open(shmFileName, 'r') if sys.platform.startswith('win'):
self.shm = mmap.mmap(self.shmFile.fileno(), mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_READ) self.shmtag = shmFileName
else:
self.shmFile = open(shmFileName, 'r')
self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) #, callSync='off')) self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) #, callSync='off'))
## Note: we need synchronous signals ## Note: we need synchronous signals
@ -53,11 +56,16 @@ class RemoteGraphicsView(QtGui.QWidget):
return QtCore.QSize(*self._sizeHint) return QtCore.QSize(*self._sizeHint)
def remoteSceneChanged(self, data): def remoteSceneChanged(self, data):
w, h, size = data w, h, size, newfile = data
#self._sizeHint = (whint, hhint) #self._sizeHint = (whint, hhint)
if self.shm.size != size: if self.shm is None or self.shm.size != size:
self.shm.close() if self.shm is not None:
self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) self.shm.close()
if sys.platform.startswith('win'):
self.shmtag = newfile ## on windows, we create a new tag for every resize
self.shm = mmap.mmap(-1, size, self.shmtag) ## can't use tmpfile on windows because the file can only be opened once.
else:
self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ)
self.shm.seek(0) self.shm.seek(0)
self._img = QtGui.QImage(self.shm.read(w*h*4), w, h, QtGui.QImage.Format_ARGB32) self._img = QtGui.QImage(self.shm.read(w*h*4), w, h, QtGui.QImage.Format_ARGB32)
self.update() self.update()
@ -107,18 +115,20 @@ class RemoteGraphicsView(QtGui.QWidget):
return self._proc return self._proc
class Renderer(GraphicsView): class Renderer(GraphicsView):
## Created by the remote process to handle render requests
sceneRendered = QtCore.Signal(object) sceneRendered = QtCore.Signal(object)
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
## Create shared memory for rendered image ## Create shared memory for rendered image
#fd = os.open('/tmp/mmaptest', os.O_CREAT | os.O_TRUNC | os.O_RDWR) if sys.platform.startswith('win'):
#os.write(fd, '\x00' * mmap.PAGESIZE) self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)])
self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_') self.shm = mmap.mmap(-1, mmap.PAGESIZE, self.shmtag) # use anonymous mmap on windows
self.shmFile.write('\x00' * mmap.PAGESIZE) else:
#fh.flush() self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_')
fd = self.shmFile.fileno() self.shmFile.write('\x00' * mmap.PAGESIZE)
self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE) fd = self.shmFile.fileno()
self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE)
atexit.register(self.close) atexit.register(self.close)
GraphicsView.__init__(self, *args, **kwds) GraphicsView.__init__(self, *args, **kwds)
@ -130,10 +140,14 @@ class Renderer(GraphicsView):
def close(self): def close(self):
self.shm.close() self.shm.close()
self.shmFile.close() if sys.platform.startswith('win'):
self.shmFile.close()
def shmFileName(self): def shmFileName(self):
return self.shmFile.name if sys.platform.startswith('win'):
return self.shmtag
else:
return self.shmFile.name
def update(self): def update(self):
self.img = None self.img = None
@ -152,16 +166,28 @@ class Renderer(GraphicsView):
return 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) if sys.platform.startswith('win'):
## windows says "WindowsError: [Error 87] the parameter is incorrect" if we try to resize the mmap
self.shm.close()
## it also says (sometimes) 'access is denied' if we try to reuse the tag.
self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)])
self.shm = mmap.mmap(-1, size, self.shmtag)
else:
self.shm.resize(size)
address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0)) address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0))
## render the scene directly to shared memory ## render the scene directly to shared memory
self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32) if USE_PYSIDE:
ch = ctypes.c_char.from_buffer(self.shm, 0)
#ch = ctypes.c_char_p(address)
self.img = QtGui.QImage(ch, self.width(), self.height(), QtGui.QImage.Format_ARGB32)
else:
self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32)
self.img.fill(0xffffffff) self.img.fill(0xffffffff)
p = QtGui.QPainter(self.img) p = QtGui.QPainter(self.img)
self.render(p, self.viewRect(), self.rect()) self.render(p, self.viewRect(), self.rect())
p.end() p.end()
self.sceneRendered.emit((self.width(), self.height(), self.shm.size())) self.sceneRendered.emit((self.width(), self.height(), self.shm.size(), self.shmFileName()))
def mousePressEvent(self, typ, pos, gpos, btn, btns, mods): def mousePressEvent(self, typ, pos, gpos, btn, btns, mods):
typ = QtCore.QEvent.Type(typ) typ = QtCore.QEvent.Type(typ)
@ -202,4 +228,4 @@ class Renderer(GraphicsView):

View File

@ -0,0 +1,185 @@
from pyqtgraph.Qt import QtGui, QtCore
from .PlotWidget import PlotWidget
from .DataFilterWidget import DataFilterParameter
from .ColorMapWidget import ColorMapParameter
import pyqtgraph.parametertree as ptree
import pyqtgraph.functions as fn
import numpy as np
from pyqtgraph.pgcollections import OrderedDict
__all__ = ['ScatterPlotWidget']
class ScatterPlotWidget(QtGui.QSplitter):
"""
Given a record array, display a scatter plot of a specific set of data.
This widget includes controls for selecting the columns to plot,
filtering data, and determining symbol color and shape. This widget allows
the user to explore relationships between columns in a record array.
The widget consists of four components:
1) A list of column names from which the user may select 1 or 2 columns
to plot. If one column is selected, the data for that column will be
plotted in a histogram-like manner by using :func:`pseudoScatter()
<pyqtgraph.pseudoScatter>`. If two columns are selected, then the
scatter plot will be generated with x determined by the first column
that was selected and y by the second.
2) A DataFilter that allows the user to select a subset of the data by
specifying multiple selection criteria.
3) A ColorMap that allows the user to determine how points are colored by
specifying multiple criteria.
4) A PlotWidget for displaying the data.
"""
def __init__(self, parent=None):
QtGui.QSplitter.__init__(self, QtCore.Qt.Horizontal)
self.ctrlPanel = QtGui.QSplitter(QtCore.Qt.Vertical)
self.addWidget(self.ctrlPanel)
self.fieldList = QtGui.QListWidget()
self.fieldList.setSelectionMode(self.fieldList.ExtendedSelection)
self.ptree = ptree.ParameterTree(showHeader=False)
self.filter = DataFilterParameter()
self.colorMap = ColorMapParameter()
self.params = ptree.Parameter.create(name='params', type='group', children=[self.filter, self.colorMap])
self.ptree.setParameters(self.params, showTop=False)
self.plot = PlotWidget()
self.ctrlPanel.addWidget(self.fieldList)
self.ctrlPanel.addWidget(self.ptree)
self.addWidget(self.plot)
self.data = None
self.style = dict(pen=None, symbol='o')
self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged)
self.filter.sigFilterChanged.connect(self.filterChanged)
self.colorMap.sigColorMapChanged.connect(self.updatePlot)
def setFields(self, fields):
"""
Set the list of field names/units to be processed.
The format of *fields* is the same as used by
:func:`ColorMapWidget.setFields <pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.setFields>`
"""
self.fields = OrderedDict(fields)
self.fieldList.clear()
for f,opts in fields:
item = QtGui.QListWidgetItem(f)
item.opts = opts
item = self.fieldList.addItem(item)
self.filter.setFields(fields)
self.colorMap.setFields(fields)
def setData(self, data):
"""
Set the data to be processed and displayed.
Argument must be a numpy record array.
"""
self.data = data
self.filtered = None
self.updatePlot()
def fieldSelectionChanged(self):
sel = self.fieldList.selectedItems()
if len(sel) > 2:
self.fieldList.blockSignals(True)
try:
for item in sel[1:-1]:
item.setSelected(False)
finally:
self.fieldList.blockSignals(False)
self.updatePlot()
def filterChanged(self, f):
self.filtered = None
self.updatePlot()
def updatePlot(self):
self.plot.clear()
if self.data is None:
return
if self.filtered is None:
self.filtered = self.filter.filterData(self.data)
data = self.filtered
if len(data) == 0:
return
colors = np.array([fn.mkBrush(*x) for x in self.colorMap.map(data)])
style = self.style.copy()
## Look up selected columns and units
sel = list([str(item.text()) for item in self.fieldList.selectedItems()])
units = list([item.opts.get('units', '') for item in self.fieldList.selectedItems()])
if len(sel) == 0:
self.plot.setTitle('')
return
if len(sel) == 1:
self.plot.setLabels(left=('N', ''), bottom=(sel[0], units[0]), title='')
if len(data) == 0:
return
x = data[sel[0]]
#if x.dtype.kind == 'f':
#mask = ~np.isnan(x)
#else:
#mask = np.ones(len(x), dtype=bool)
#x = x[mask]
#style['symbolBrush'] = colors[mask]
y = None
elif len(sel) == 2:
self.plot.setLabels(left=(sel[1],units[1]), bottom=(sel[0],units[0]))
if len(data) == 0:
return
xydata = []
for ax in [0,1]:
d = data[sel[ax]]
## scatter catecorical values just a bit so they show up better in the scatter plot.
#if sel[ax] in ['MorphologyBSMean', 'MorphologyTDMean', 'FIType']:
#d += np.random.normal(size=len(cells), scale=0.1)
xydata.append(d)
x,y = xydata
#mask = np.ones(len(x), dtype=bool)
#if x.dtype.kind == 'f':
#mask |= ~np.isnan(x)
#if y.dtype.kind == 'f':
#mask |= ~np.isnan(y)
#x = x[mask]
#y = y[mask]
#style['symbolBrush'] = colors[mask]
## convert enum-type fields to float, set axis labels
xy = [x,y]
for i in [0,1]:
axis = self.plot.getAxis(['bottom', 'left'][i])
if xy[i] is not None and xy[i].dtype.kind in ('S', 'O'):
vals = self.fields[sel[i]].get('values', list(set(xy[i])))
xy[i] = np.array([vals.index(x) if x in vals else None for x in xy[i]], dtype=float)
axis.setTicks([list(enumerate(vals))])
else:
axis.setTicks(None) # reset to automatic ticking
x,y = xy
## mask out any nan values
mask = np.ones(len(x), dtype=bool)
if x.dtype.kind == 'f':
mask &= ~np.isnan(x)
if y is not None and y.dtype.kind == 'f':
mask &= ~np.isnan(y)
x = x[mask]
style['symbolBrush'] = colors[mask]
## Scatter y-values for a histogram-like appearance
if y is None:
y = fn.pseudoScatter(x)
else:
y = y[mask]
self.plot.plot(x, y, **style)