Merge branch 'develop' into pyside2

This commit is contained in:
Luke Campagnola 2018-05-22 15:22:54 -07:00
commit 66dcfc7c67
62 changed files with 912 additions and 346 deletions

View File

@ -7,6 +7,9 @@ pyqtgraph-0.11.0 (in development)
To mimic the old behavior, use ArrowItem.rotate() instead of the `angle` argument. To mimic the old behavior, use ArrowItem.rotate() instead of the `angle` argument.
- Deprecated graphicsWindow classes; these have been unnecessary for many years because - Deprecated graphicsWindow classes; these have been unnecessary for many years because
widgets can be placed into a new window just by calling show(). widgets can be placed into a new window just by calling show().
- Integer values in ParameterTree are now formatted as integer (%d) by default, rather than
scientific notation (%g). This can be overridden by providing `format={value:g}` when
creating the parameter.
pyqtgraph-0.10.0 pyqtgraph-0.10.0

View File

@ -6,80 +6,46 @@ PyQtGraph
A pure-Python graphics library for PyQt/PySide A pure-Python graphics library for PyQt/PySide
Copyright 2012 Luke Campagnola, University of North Carolina at Chapel Hill Copyright 2017 Luke Campagnola, University of North Carolina at Chapel Hill
<http://www.pyqtgraph.org> <http://www.pyqtgraph.org>
Maintainer PyQtGraph is intended for use in mathematics / scientific / engineering applications.
---------- Despite being written entirely in python, the library is fast due to its
heavy leverage of numpy for number crunching, Qt's GraphicsView framework for
2D display, and OpenGL for 3D display.
* Luke Campagnola <luke.campagnola@gmail.com>
Contributors
------------
* Megan Kratz
* Paul Manis
* Ingo Breßler
* Christian Gavin
* Michael Cristopher Hogg
* Ulrich Leutner
* Felix Schill
* Guillaume Poulin
* Antony Lee
* Mattias Põldaru
* Thomas S.
* Fabio Zadrozny
* Mikhail Terekhov
* Pietro Zambelli
* Stefan Holzmann
* Nicholas TJ
* John David Reaver
* David Kaplan
* Martin Fitzpatrick
* Daniel Lidstrom
* Eric Dill
* Vincent LeSaux
Requirements Requirements
------------ ------------
* PyQt 4.7+, PySide, or PyQt5 * PyQt 4.7+, PySide, or PyQt5
* python 2.6, 2.7, or 3.x * python 2.7, or 3.x
* NumPy * NumPy
* For 3D graphics: pyopengl and qt-opengl * For 3D graphics: pyopengl and qt-opengl
* Known to run on Windows, Linux, and Mac. * Known to run on Windows, Linux, and Mac.
Support Support
------- -------
Post at the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) * Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues)
* Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph)
Installation Methods Installation Methods
-------------------- --------------------
* From pypi:
`pip install pyqtgraph`
* To use with a specific project, simply copy the pyqtgraph subdirectory * To use with a specific project, simply copy the pyqtgraph subdirectory
anywhere that is importable from your project. PyQtGraph may also be anywhere that is importable from your project.
used as a git subtree by cloning the git-core repository from github.
* To install system-wide from source distribution: * To install system-wide from source distribution:
`$ python setup.py install` `$ python setup.py install`
* For installation packages, see the website (pyqtgraph.org) * For installation packages, see the website (pyqtgraph.org)
* On debian-like systems, pyqtgraph requires the following packages:
python-numpy, python-qt4 | python-pyside
For 3D support: python-opengl, python-qt4-gl | python-pyside.qtopengl
Documentation Documentation
------------- -------------
There are many examples; run `python -m pyqtgraph.examples` for a menu. The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu.
The official documentation lives at http://pyqtgraph.org/documentation
Some (incomplete) documentation exists at this time.
* Easiest place to get documentation is at <http://www.pyqtgraph.org/documentation>
* If you acquired this code as a .tar.gz file from the website, then you can also look in
doc/html.
* If you acquired this code via GitHub, then you can build the documentation using sphinx.
From the documentation directory, run:
`$ make html`
Please feel free to pester Luke or post to the forum if you need a specific
section of documentation to be expanded.

View File

@ -12,7 +12,7 @@ import numpy as np
# Enable antialiasing for prettier plots # Enable antialiasing for prettier plots
pg.setConfigOptions(antialias=True) pg.setConfigOptions(antialias=True)
w = pg.GraphicsWindow() w = pg.GraphicsLayoutWidget(show=True)
w.setWindowTitle('pyqtgraph example: CustomGraphItem') w.setWindowTitle('pyqtgraph example: CustomGraphItem')
v = w.addViewBox() v = w.addViewBox()
v.setAspectLocked() v.setAspectLocked()

View File

@ -11,15 +11,29 @@ from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
# for generating a traceback object to display
def some_func1():
return some_func2()
def some_func2():
try:
raise Exception()
except:
import sys
return sys.exc_info()[2]
app = QtGui.QApplication([]) app = QtGui.QApplication([])
d = { d = {
'list1': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], 'a list': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"],
'dict1': { 'a dict': {
'x': 1, 'x': 1,
'y': 2, 'y': 2,
'z': 'three' 'z': 'three'
}, },
'array1 (20x20)': np.ones((10,10)) 'an array': np.random.randint(10, size=(40,10)),
'a traceback': some_func1(),
'a function': some_func1,
'a class': pg.DataTreeWidget,
} }
tree = pg.DataTreeWidget(data=d) tree = pg.DataTreeWidget(data=d)

View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
"""
Simple use of DiffTreeWidget to display differences between structures of
nested dicts, lists, and arrays.
"""
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 = QtGui.QApplication([])
A = {
'a list': [1,2,2,4,5,6, {'nested1': 'aaaa', 'nested2': 'bbbbb'}, "seven"],
'a dict': {
'x': 1,
'y': 2,
'z': 'three'
},
'an array': np.random.randint(10, size=(40,10)),
#'a traceback': some_func1(),
#'a function': some_func1,
#'a class': pg.DataTreeWidget,
}
B = {
'a list': [1,2,3,4,5,5, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"],
'a dict': {
'x': 2,
'y': 2,
'z': 'three',
'w': 5
},
'another dict': {1:2, 2:3, 3:4},
'an array': np.random.randint(10, size=(40,10)),
}
tree = pg.DiffTreeWidget()
tree.setData(A, B)
tree.show()
tree.setWindowTitle('pyqtgraph example: DiffTreeWidget')
tree.resize(1000, 800)
## 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

@ -26,9 +26,9 @@ data += pg.gaussianFilter(np.random.normal(size=shape), (15,15,15))*15
## slice out three planes, convert to RGBA for OpenGL texture ## slice out three planes, convert to RGBA for OpenGL texture
levels = (-0.08, 0.08) levels = (-0.08, 0.08)
tex1 = pg.makeRGBA(data[shape[0]/2], levels=levels)[0] # yz plane tex1 = pg.makeRGBA(data[shape[0]//2], levels=levels)[0] # yz plane
tex2 = pg.makeRGBA(data[:,shape[1]/2], levels=levels)[0] # xz plane tex2 = pg.makeRGBA(data[:,shape[1]//2], levels=levels)[0] # xz plane
tex3 = pg.makeRGBA(data[:,:,shape[2]/2], levels=levels)[0] # xy plane tex3 = pg.makeRGBA(data[:,:,shape[2]//2], levels=levels)[0] # xy plane
#tex1[:,:,3] = 128 #tex1[:,:,3] = 128
#tex2[:,:,3] = 128 #tex2[:,:,3] = 128
#tex3[:,:,3] = 128 #tex3[:,:,3] = 128

View File

@ -13,7 +13,7 @@ import numpy as np
# Enable antialiasing for prettier plots # Enable antialiasing for prettier plots
pg.setConfigOptions(antialias=True) pg.setConfigOptions(antialias=True)
w = pg.GraphicsWindow() w = pg.GraphicsLayoutWidget(show=True)
w.setWindowTitle('pyqtgraph example: GraphItem') w.setWindowTitle('pyqtgraph example: GraphItem')
v = w.addViewBox() v = w.addViewBox()
v.setAspectLocked() v.setAspectLocked()

View File

@ -10,7 +10,7 @@ import pyqtgraph as pg
app = QtGui.QApplication([]) app = QtGui.QApplication([])
win = pg.GraphicsWindow(title="Plotting items examples") win = pg.GraphicsLayoutWidget(show=True, title="Plotting items examples")
win.resize(1000,600) win.resize(1000,600)
# Enable antialiasing for prettier plots # Enable antialiasing for prettier plots

View File

@ -12,7 +12,7 @@ import pyqtgraph as pg
app = QtGui.QApplication([]) app = QtGui.QApplication([])
win = pg.GraphicsWindow(title="Basic plotting examples") win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples")
win.resize(1000,600) win.resize(1000,600)
win.setWindowTitle('pyqtgraph example: LogPlotTest') win.setWindowTitle('pyqtgraph example: LogPlotTest')

View File

@ -12,32 +12,27 @@ from pyqtgraph.Qt import QtGui, QtCore
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.ptime import time from pyqtgraph.ptime import time
#QtGui.QApplication.setGraphicsSystem('raster')
app = QtGui.QApplication([]) app = QtGui.QApplication([])
#mw = QtGui.QMainWindow()
#mw.resize(800,800)
p = pg.plot() plot = pg.plot()
p.setWindowTitle('pyqtgraph example: MultiPlotSpeedTest') plot.setWindowTitle('pyqtgraph example: MultiPlotSpeedTest')
#p.setRange(QtCore.QRectF(0, -10, 5000, 20)) plot.setLabel('bottom', 'Index', units='B')
p.setLabel('bottom', 'Index', units='B')
nPlots = 100 nPlots = 100
nSamples = 500 nSamples = 500
#curves = [p.plot(pen=(i,nPlots*1.3)) for i in range(nPlots)]
curves = [] curves = []
for i in range(nPlots): for idx in range(nPlots):
c = pg.PlotCurveItem(pen=(i,nPlots*1.3)) curve = pg.PlotCurveItem(pen=(idx,nPlots*1.3))
p.addItem(c) plot.addItem(curve)
c.setPos(0,i*6) curve.setPos(0,idx*6)
curves.append(c) curves.append(curve)
p.setYRange(0, nPlots*6) plot.setYRange(0, nPlots*6)
p.setXRange(0, nSamples) plot.setXRange(0, nSamples)
p.resize(600,900) plot.resize(600,900)
rgn = pg.LinearRegionItem([nSamples/5.,nSamples/3.]) rgn = pg.LinearRegionItem([nSamples/5.,nSamples/3.])
p.addItem(rgn) plot.addItem(rgn)
data = np.random.normal(size=(nPlots*23,nSamples)) data = np.random.normal(size=(nPlots*23,nSamples))
@ -46,13 +41,12 @@ lastTime = time()
fps = None fps = None
count = 0 count = 0
def update(): def update():
global curve, data, ptr, p, lastTime, fps, nPlots, count global curve, data, ptr, plot, lastTime, fps, nPlots, count
count += 1 count += 1
#print "---------", count
for i in range(nPlots): for i in range(nPlots):
curves[i].setData(data[(ptr+i)%data.shape[0]]) curves[i].setData(data[(ptr+i)%data.shape[0]])
#print " setData done."
ptr += nPlots ptr += nPlots
now = time() now = time()
dt = now - lastTime dt = now - lastTime
@ -62,13 +56,11 @@ def update():
else: else:
s = np.clip(dt*3., 0, 1) s = np.clip(dt*3., 0, 1)
fps = fps * (1-s) + (1.0/dt) * s fps = fps * (1-s) + (1.0/dt) * s
p.setTitle('%0.2f fps' % fps) plot.setTitle('%0.2f fps' % fps)
#app.processEvents() ## force complete redraw for every plot #app.processEvents() ## force complete redraw for every plot
timer = QtCore.QTimer() timer = QtCore.QTimer()
timer.timeout.connect(update) timer.timeout.connect(update)
timer.start(0) timer.start(0)
## Start Qt event loop unless running in interactive mode. ## Start Qt event loop unless running in interactive mode.
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -9,7 +9,7 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: PanningPlot') win.setWindowTitle('pyqtgraph example: PanningPlot')
plt = win.addPlot() plt = win.addPlot()

View File

@ -16,7 +16,7 @@ app = QtGui.QApplication([])
#mw = QtGui.QMainWindow() #mw = QtGui.QMainWindow()
#mw.resize(800,800) #mw.resize(800,800)
win = pg.GraphicsWindow(title="Plot auto-range examples") win = pg.GraphicsLayoutWidget(show=True, title="Plot auto-range examples")
win.resize(800,600) win.resize(800,600)
win.setWindowTitle('pyqtgraph example: PlotAutoRange') win.setWindowTitle('pyqtgraph example: PlotAutoRange')

View File

@ -17,7 +17,7 @@ app = QtGui.QApplication([])
#mw = QtGui.QMainWindow() #mw = QtGui.QMainWindow()
#mw.resize(800,800) #mw.resize(800,800)
win = pg.GraphicsWindow(title="Basic plotting examples") win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples")
win.resize(1000,600) win.resize(1000,600)
win.setWindowTitle('pyqtgraph example: Plotting') win.setWindowTitle('pyqtgraph example: Plotting')

View File

@ -33,7 +33,7 @@ arr[8:13, 44:46] = 10
## create GUI ## create GUI
app = QtGui.QApplication([]) app = QtGui.QApplication([])
w = pg.GraphicsWindow(size=(1000,800), border=True) w = pg.GraphicsLayoutWidget(show=True, size=(1000,800), border=True)
w.setWindowTitle('pyqtgraph example: ROI Examples') w.setWindowTitle('pyqtgraph example: ROI Examples')
text = """Data Selection From Image.<br>\n text = """Data Selection From Image.<br>\n

View File

@ -13,7 +13,7 @@ pg.setConfigOptions(imageAxisOrder='row-major')
## create GUI ## create GUI
app = QtGui.QApplication([]) app = QtGui.QApplication([])
w = pg.GraphicsWindow(size=(800,800), border=True) w = pg.GraphicsLayoutWidget(show=True, size=(800,800), border=True)
v = w.addViewBox(colspan=2) v = w.addViewBox(colspan=2)
v.invertY(True) ## Images usually have their Y-axis pointing downward v.invertY(True) ## Images usually have their Y-axis pointing downward
v.setAspectLocked(True) v.setAspectLocked(True)

View File

@ -9,7 +9,7 @@ from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
pg.mkQApp() pg.mkQApp()
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: ScaleBar') win.setWindowTitle('pyqtgraph example: ScaleBar')
vb = win.addViewBox() vb = win.addViewBox()

View File

@ -28,7 +28,7 @@ pg.mkQApp()
# Make up some tabular data with structure # Make up some tabular data with structure
data = np.empty(1000, dtype=[('x_pos', float), ('y_pos', float), data = np.empty(1000, dtype=[('x_pos', float), ('y_pos', float),
('count', int), ('amplitude', float), ('count', int), ('amplitude', float),
('decay', float), ('type', 'S10')]) ('decay', float), ('type', 'U10')])
strings = ['Type-A', 'Type-B', 'Type-C', 'Type-D', 'Type-E'] strings = ['Type-A', 'Type-B', 'Type-C', 'Type-D', 'Type-E']
typeInds = np.random.randint(5, size=1000) typeInds = np.random.randint(5, size=1000)
data['type'] = np.array(strings)[typeInds] data['type'] = np.array(strings)[typeInds]

View File

@ -11,7 +11,7 @@ from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg import pyqtgraph as pg
app = QtGui.QApplication([]) app = QtGui.QApplication([])
win = pg.GraphicsWindow(title="Scatter Plot Symbols") win = pg.GraphicsLayoutWidget(show=True, title="Scatter Plot Symbols")
win.resize(1000,600) win.resize(1000,600)
pg.setConfigOptions(antialias=True) pg.setConfigOptions(antialias=True)

View File

@ -16,7 +16,7 @@ x = np.arange(1000, dtype=float)
y = np.random.normal(size=1000) y = np.random.normal(size=1000)
y += 5 * np.sin(x/100) y += 5 * np.sin(x/100)
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: ____') win.setWindowTitle('pyqtgraph example: ____')
win.resize(1000, 800) win.resize(1000, 800)
win.ci.setBorder((50, 50, 100)) win.ci.setBorder((50, 50, 100))

View File

@ -28,6 +28,7 @@ class ExampleLoader(QtGui.QMainWindow):
self.cw = QtGui.QWidget() self.cw = QtGui.QWidget()
self.setCentralWidget(self.cw) self.setCentralWidget(self.cw)
self.ui.setupUi(self.cw) self.ui.setupUi(self.cw)
self.setWindowTitle("PyQtGraph Examples")
self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeBtn = QtGui.QPushButton('Run Edited Code')
self.codeLayout = QtGui.QGridLayout() self.codeLayout = QtGui.QGridLayout()

View File

@ -14,7 +14,7 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: context menu') win.setWindowTitle('pyqtgraph example: context menu')

View File

@ -13,7 +13,7 @@ from pyqtgraph.Point import Point
#generate layout #generate layout
app = QtGui.QApplication([]) app = QtGui.QApplication([])
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: crosshair') win.setWindowTitle('pyqtgraph example: crosshair')
label = pg.LabelItem(justify='right') label = pg.LabelItem(justify='right')
win.addItem(label) win.addItem(label)

View File

@ -4,6 +4,7 @@ Displays an interactive Koch fractal
""" """
import initExample ## Add path to library (just for examples; you do not need this) import initExample ## Add path to library (just for examples; you do not need this)
from functools import reduce
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
@ -111,12 +112,4 @@ if __name__ == '__main__':
import sys import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_() QtGui.QApplication.instance().exec_()

View File

@ -8,7 +8,7 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.resize(800,350) win.resize(800,350)
win.setWindowTitle('pyqtgraph example: Histogram') win.setWindowTitle('pyqtgraph example: Histogram')
plt1 = win.addPlot() plt1 = win.addPlot()

View File

@ -17,10 +17,10 @@ app = QtGui.QApplication([])
frames = 200 frames = 200
data = np.random.normal(size=(frames,30,30), loc=0, scale=100) data = np.random.normal(size=(frames,30,30), loc=0, scale=100)
data = np.concatenate([data, data], axis=0) data = np.concatenate([data, data], axis=0)
data = pg.gaussianFilter(data, (10, 10, 10))[frames/2:frames + frames/2] data = pg.gaussianFilter(data, (10, 10, 10))[frames//2:frames + frames//2]
data[:, 15:16, 15:17] += 1 data[:, 15:16, 15:17] += 1
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: Isocurve') win.setWindowTitle('pyqtgraph example: Isocurve')
vb = win.addViewBox() vb = win.addViewBox()
img = pg.ImageItem(data[0]) img = pg.ImageItem(data[0])

View File

@ -20,7 +20,7 @@ app = QtGui.QApplication([])
x = np.linspace(-50, 50, 1000) x = np.linspace(-50, 50, 1000)
y = np.sin(x) / x y = np.sin(x) / x
win = pg.GraphicsWindow(title="pyqtgraph example: Linked Views") win = pg.GraphicsLayoutWidget(show=True, title="pyqtgraph example: Linked Views")
win.resize(800,600) win.resize(800,600)
win.addLabel("Linked Views", colspan=2) win.addLabel("Linked Views", colspan=2)

View File

@ -11,7 +11,7 @@ import pyqtgraph as pg
app = QtGui.QApplication([]) app = QtGui.QApplication([])
w = pg.GraphicsWindow() w = pg.GraphicsLayoutWidget(show=True)
w.setWindowTitle('pyqtgraph example: logAxis') w.setWindowTitle('pyqtgraph example: logAxis')
p1 = w.addPlot(0,0, title="X Semilog") p1 = w.addPlot(0,0, title="X Semilog")
p2 = w.addPlot(1,0, title="Y Semilog") p2 = w.addPlot(1,0, title="Y Semilog")

View File

@ -17,7 +17,7 @@ from pyqtgraph import Point
app = pg.QtGui.QApplication([]) app = pg.QtGui.QApplication([])
w = pg.GraphicsWindow(border=0.5) w = pg.GraphicsLayoutWidget(show=True, border=0.5)
w.resize(1000, 900) w.resize(1000, 900)
w.show() w.show()

View File

@ -8,7 +8,7 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: Scrolling Plots') win.setWindowTitle('pyqtgraph example: Scrolling Plots')

View File

@ -36,6 +36,18 @@ class GraphicsScene(QtGui.QGraphicsScene):
This lets us indicate unambiguously to the user which item they are about to click/drag on This lets us indicate unambiguously to the user which item they are about to click/drag on
* Eats mouseMove events that occur too soon after a mouse press. * Eats mouseMove events that occur too soon after a mouse press.
* Reimplements items() and itemAt() to circumvent PyQt bug * Reimplements items() and itemAt() to circumvent PyQt bug
====================== ==================================================================
**Signals**
sigMouseClicked(event) Emitted when the mouse is clicked over the scene. Use ev.pos() to
get the click position relative to the item that was clicked on,
or ev.scenePos() to get the click position in scene coordinates.
See :class:`pyqtgraph.GraphicsScene.MouseClickEvent`.
sigMouseMoved(pos) Emitted when the mouse cursor moves over the scene. The position
is given in scene coordinates.
sigMouseHover(items) Emitted when the mouse is moved over the scene. Items is a list
of items under the cursor.
====================== ==================================================================
Mouse interaction is as follows: Mouse interaction is as follows:

View File

@ -105,7 +105,13 @@ class Point(QtCore.QPointF):
def length(self): def length(self):
"""Returns the vector length of this Point.""" """Returns the vector length of this Point."""
return (self[0]**2 + self[1]**2) ** 0.5 try:
return (self[0]**2 + self[1]**2) ** 0.5
except OverflowError:
try:
return self[1] / np.sin(np.arctan2(self[1], self[0]))
except OverflowError:
return np.inf
def norm(self): def norm(self):
"""Returns a vector in the same direction with unit length.""" """Returns a vector in the same direction with unit length."""

View File

@ -319,3 +319,12 @@ m = re.match(r'(\d+)\.(\d+).*', QtVersion)
if m is not None and list(map(int, m.groups())) < versionReq: if m is not None and list(map(int, m.groups())) < versionReq:
print(list(map(int, m.groups()))) print(list(map(int, m.groups())))
raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion))
QAPP = None
def mkQApp():
global QAPP
QAPP = QtGui.QApplication.instance()
if QAPP is None:
QAPP = QtGui.QApplication([])
return QAPP

View File

@ -113,7 +113,7 @@ class SRTTransform3D(Transform3D):
def setFromMatrix(self, m): def setFromMatrix(self, m):
""" """
Set this transform mased on the elements of *m* Set this transform based on the elements of *m*
The input matrix must be affine AND have no shear, The input matrix must be affine AND have no shear,
otherwise the conversion will most likely fail. otherwise the conversion will most likely fail.
""" """

View File

@ -10,7 +10,7 @@ __version__ = '0.10.0'
## 'Qt' is a local module; it is intended mainly to cover up the differences ## 'Qt' is a local module; it is intended mainly to cover up the differences
## between PyQt4 and PySide. ## between PyQt4 and PySide.
from .Qt import QtGui from .Qt import QtGui, mkQApp
## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause) ## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause)
#if QtGui.QApplication.instance() is None: #if QtGui.QApplication.instance() is None:
@ -258,6 +258,7 @@ from .widgets.VerticalLabel import *
from .widgets.FeedbackButton import * from .widgets.FeedbackButton import *
from .widgets.ColorButton import * from .widgets.ColorButton import *
from .widgets.DataTreeWidget import * from .widgets.DataTreeWidget import *
from .widgets.DiffTreeWidget import *
from .widgets.GraphicsView import * from .widgets.GraphicsView import *
from .widgets.LayoutWidget import * from .widgets.LayoutWidget import *
from .widgets.TableWidget import * from .widgets.TableWidget import *
@ -466,14 +467,3 @@ def stack(*args, **kwds):
except NameError: except NameError:
consoles = [c] consoles = [c]
return c return c
def mkQApp():
global QAPP
inst = QtGui.QApplication.instance()
if inst is None:
QAPP = QtGui.QApplication([])
else:
QAPP = inst
return QAPP

View File

@ -361,7 +361,6 @@ class ConsoleWidget(QtGui.QWidget):
for index, line in enumerate(traceback.extract_stack(frame)): for index, line in enumerate(traceback.extract_stack(frame)):
# extract_stack return value changed in python 3.5 # extract_stack return value changed in python 3.5
if 'FrameSummary' in str(type(line)): if 'FrameSummary' in str(type(line)):
print(dir(line))
line = (line.filename, line.lineno, line.name, line._line) line = (line.filename, line.lineno, line.name, line._line)
self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line)
@ -382,7 +381,6 @@ class ConsoleWidget(QtGui.QWidget):
for index, line in enumerate(traceback.extract_tb(tb)): for index, line in enumerate(traceback.extract_tb(tb)):
# extract_stack return value changed in python 3.5 # extract_stack return value changed in python 3.5
if 'FrameSummary' in str(type(line)): if 'FrameSummary' in str(type(line)):
print(dir(line))
line = (line.filename, line.lineno, line.name, line._line) line = (line.filename, line.lineno, line.name, line._line)
self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line)

View File

@ -510,7 +510,7 @@ class Profiler(object):
try: try:
caller_object_type = type(caller_frame.f_locals["self"]) caller_object_type = type(caller_frame.f_locals["self"])
except KeyError: # we are in a regular function except KeyError: # we are in a regular function
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1] qualifier = caller_frame.f_globals["__name__"].split(".", 1)[-1]
else: # we are in a method else: # we are in a method
qualifier = caller_object_type.__name__ qualifier = caller_object_type.__name__
func_qualname = qualifier + "." + caller_frame.f_code.co_name func_qualname = qualifier + "." + caller_frame.f_code.co_name

View File

@ -23,7 +23,8 @@ class SVGExporter(Exporter):
#{'name': 'height', 'type': 'float', 'value': tr.height(), 'limits': (0, None)}, #{'name': 'height', 'type': 'float', 'value': tr.height(), 'limits': (0, None)},
#{'name': 'viewbox clipping', 'type': 'bool', 'value': True}, #{'name': 'viewbox clipping', 'type': 'bool', 'value': True},
#{'name': 'normalize coordinates', 'type': 'bool', 'value': True}, #{'name': 'normalize coordinates', 'type': 'bool', 'value': True},
#{'name': 'normalize line width', 'type': 'bool', 'value': True}, {'name': 'scaling stroke', 'type': 'bool', 'value': False, 'tip': "If False, strokes are non-scaling, "
"which means that they appear the same width on screen regardless of how they are scaled or how the view is zoomed."},
]) ])
#self.params.param('width').sigValueChanged.connect(self.widthChanged) #self.params.param('width').sigValueChanged.connect(self.widthChanged)
#self.params.param('height').sigValueChanged.connect(self.heightChanged) #self.params.param('height').sigValueChanged.connect(self.heightChanged)
@ -49,7 +50,8 @@ class SVGExporter(Exporter):
## Qt's SVG generator is not complete. (notably, it lacks clipping) ## Qt's SVG generator is not complete. (notably, it lacks clipping)
## Instead, we will use Qt to generate SVG for each item independently, ## Instead, we will use Qt to generate SVG for each item independently,
## then manually reconstruct the entire document. ## then manually reconstruct the entire document.
xml = generateSvg(self.item) options = {ch.name():ch.value() for ch in self.params.children()}
xml = generateSvg(self.item, options)
if toBytes: if toBytes:
return xml.encode('UTF-8') return xml.encode('UTF-8')
@ -69,10 +71,10 @@ xmlHeader = """\
<desc>Generated with Qt and pyqtgraph</desc> <desc>Generated with Qt and pyqtgraph</desc>
""" """
def generateSvg(item): def generateSvg(item, options={}):
global xmlHeader global xmlHeader
try: try:
node, defs = _generateItemSvg(item) node, defs = _generateItemSvg(item, options=options)
finally: finally:
## reset export mode for all items in the tree ## reset export mode for all items in the tree
if isinstance(item, QtGui.QGraphicsScene): if isinstance(item, QtGui.QGraphicsScene):
@ -94,7 +96,7 @@ def generateSvg(item):
return xmlHeader + defsXml + node.toprettyxml(indent=' ') + "\n</svg>\n" return xmlHeader + defsXml + node.toprettyxml(indent=' ') + "\n</svg>\n"
def _generateItemSvg(item, nodes=None, root=None): def _generateItemSvg(item, nodes=None, root=None, options={}):
## This function is intended to work around some issues with Qt's SVG generator ## This function is intended to work around some issues with Qt's SVG generator
## and SVG in general. ## and SVG in general.
## 1) Qt SVG does not implement clipping paths. This is absurd. ## 1) Qt SVG does not implement clipping paths. This is absurd.
@ -169,7 +171,7 @@ def _generateItemSvg(item, nodes=None, root=None):
buf = QtCore.QBuffer(arr) buf = QtCore.QBuffer(arr)
svg = QtSvg.QSvgGenerator() svg = QtSvg.QSvgGenerator()
svg.setOutputDevice(buf) svg.setOutputDevice(buf)
dpi = QtGui.QDesktopWidget().physicalDpiX() dpi = QtGui.QDesktopWidget().logicalDpiX()
svg.setResolution(dpi) svg.setResolution(dpi)
p = QtGui.QPainter() p = QtGui.QPainter()
@ -209,18 +211,8 @@ def _generateItemSvg(item, nodes=None, root=None):
## 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, defs, item) correctCoordinates(g1, defs, item, options)
profiler('correct') profiler('correct')
## make sure g1 has the transformation matrix
#m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32())
#g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m)
#print "=================",item,"====================="
#print g1.toprettyxml(indent=" ", newl='')
## Inkscape does not support non-scaling-stroke (this is SVG 1.2, inkscape supports 1.1)
## So we need to correct anything attempting to use this.
#correctStroke(g1, item, root)
## decide on a name for this item ## decide on a name for this item
baseName = item.__class__.__name__ baseName = item.__class__.__name__
@ -239,15 +231,10 @@ def _generateItemSvg(item, nodes=None, root=None):
## See if this item clips its children ## See if this item clips its children
if int(item.flags() & item.ItemClipsChildrenToShape) > 0: if int(item.flags() & item.ItemClipsChildrenToShape) > 0:
## Generate svg for just the path ## Generate svg for just the path
#if isinstance(root, QtGui.QGraphicsScene):
#path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape()))
#else:
#path = QtGui.QGraphicsPathItem(root.mapToParent(item.mapToItem(root, item.shape())))
path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape())) path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape()))
item.scene().addItem(path) item.scene().addItem(path)
try: try:
#pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0] pathNode = _generateItemSvg(path, root=root, options=options)[0].getElementsByTagName('path')[0]
pathNode = _generateItemSvg(path, root=root)[0].getElementsByTagName('path')[0]
# assume <defs> for this path is empty.. possibly problematic. # assume <defs> for this path is empty.. possibly problematic.
finally: finally:
item.scene().removeItem(path) item.scene().removeItem(path)
@ -267,17 +254,18 @@ def _generateItemSvg(item, nodes=None, root=None):
## 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:
csvg = _generateItemSvg(ch, nodes, root) csvg = _generateItemSvg(ch, nodes, root, options=options)
if csvg is None: if csvg is None:
continue continue
cg, cdefs = csvg cg, cdefs = csvg
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)
defs.extend(cdefs) defs.extend(cdefs)
profiler('children') profiler('children')
return g1, defs return g1, defs
def correctCoordinates(node, defs, item):
def correctCoordinates(node, defs, item, options):
# TODO: correct gradient coordinates inside defs # TODO: correct gradient coordinates inside defs
## Remove transformation matrices from <g> tags by applying matrix to coordinates inside. ## Remove transformation matrices from <g> tags by applying matrix to coordinates inside.
@ -344,6 +332,10 @@ def correctCoordinates(node, defs, item):
t = '' t = ''
nc = fn.transformCoordinates(tr, np.array([[float(x),float(y)]]), transpose=True) nc = fn.transformCoordinates(tr, np.array([[float(x),float(y)]]), transpose=True)
newCoords += t+str(nc[0,0])+','+str(nc[0,1])+' ' newCoords += t+str(nc[0,0])+','+str(nc[0,1])+' '
# If coords start with L instead of M, then the entire path will not be rendered.
# (This can happen if the first point had nan values in it--Qt will skip it on export)
if newCoords[0] != 'M':
newCoords = 'M' + newCoords[1:]
ch.setAttribute('d', newCoords) ch.setAttribute('d', newCoords)
elif ch.tagName == 'text': elif ch.tagName == 'text':
removeTransform = False removeTransform = False
@ -372,12 +364,16 @@ def correctCoordinates(node, defs, item):
ch.setAttribute('font-family', ', '.join([f if ' ' not in f else '"%s"'%f for f in families])) ch.setAttribute('font-family', ', '.join([f if ' ' not in f else '"%s"'%f for f in families]))
## correct line widths if needed ## correct line widths if needed
if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke': if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke' and grp.getAttribute('stroke-width') != '':
w = float(grp.getAttribute('stroke-width')) w = float(grp.getAttribute('stroke-width'))
s = fn.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True) s = fn.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True)
w = ((s[0]-s[1])**2).sum()**0.5 w = ((s[0]-s[1])**2).sum()**0.5
ch.setAttribute('stroke-width', str(w)) ch.setAttribute('stroke-width', str(w))
# Remove non-scaling-stroke if requested
if options.get('scaling stroke') is True and ch.getAttribute('vector-effect') == 'non-scaling-stroke':
ch.removeAttribute('vector-effect')
if removeTransform: if removeTransform:
grp.removeAttribute('transform') grp.removeAttribute('transform')

View File

@ -14,7 +14,8 @@ import sys, struct
from .python2_3 import asUnicode, basestring from .python2_3 import asUnicode, basestring
from .Qt import QtGui, QtCore, QT_LIB from .Qt import QtGui, QtCore, QT_LIB
from . import getConfigOption, setConfigOptions from . import getConfigOption, setConfigOptions
from . import debug from . import debug, reload
from .reload import getPreviousVersion
from .metaarray import MetaArray from .metaarray import MetaArray
@ -2172,7 +2173,7 @@ def isosurface(data, level):
## compute lookup table of index: vertexes mapping ## compute lookup table of index: vertexes mapping
faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte) faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte)
faceTableInds = np.argwhere(nTableFaces == i) faceTableInds = np.argwhere(nTableFaces == i)
faceTableI[faceTableInds[:,0]] = np.array([triTable[j] for j in faceTableInds]) faceTableI[faceTableInds[:,0]] = np.array([triTable[j[0]] for j in faceTableInds])
faceTableI = faceTableI.reshape((len(triTable), i, 3)) faceTableI = faceTableI.reshape((len(triTable), i, 3))
faceShiftTables.append(edgeShifts[faceTableI]) faceShiftTables.append(edgeShifts[faceTableI])
@ -2428,3 +2429,45 @@ def toposort(deps, nodes=None, seen=None, stack=None, depth=0):
sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1)) sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1))
sorted.append(n) sorted.append(n)
return sorted return sorted
def disconnect(signal, slot):
"""Disconnect a Qt signal from a slot.
This method augments Qt's Signal.disconnect():
* Return bool indicating whether disconnection was successful, rather than
raising an exception
* Attempt to disconnect prior versions of the slot when using pg.reload
"""
while True:
try:
signal.disconnect(slot)
return True
except (TypeError, RuntimeError):
slot = reload.getPreviousVersion(slot)
if slot is None:
return False
class SignalBlock(object):
"""Class used to temporarily block a Qt signal connection::
with SignalBlock(signal, slot):
# do something that emits a signal; it will
# not be delivered to slot
"""
def __init__(self, signal, slot):
self.signal = signal
self.slot = slot
def __enter__(self):
self.reconnect = disconnect(self.signal, self.slot)
return self
def __exit__(self, *args):
if self.reconnect:
self.signal.connect(self.slot)

View File

@ -379,6 +379,10 @@ class ImageItem(GraphicsObject):
image = fn.downsample(self.image, xds, axis=axes[0]) image = fn.downsample(self.image, xds, axis=axes[0])
image = fn.downsample(image, yds, axis=axes[1]) image = fn.downsample(image, yds, axis=axes[1])
self._lastDownsample = (xds, yds) self._lastDownsample = (xds, yds)
# Check if downsampling reduced the image size to zero due to inf values.
if image.size == 0:
return
else: else:
image = self.image image = self.image
@ -465,11 +469,11 @@ class ImageItem(GraphicsObject):
This method is also used when automatically computing levels. This method is also used when automatically computing levels.
""" """
if self.image is None: if self.image is None or self.image.size == 0:
return None,None return None,None
if step == 'auto': if step == 'auto':
step = (int(np.ceil(self.image.shape[0] / targetImageSize)), step = (max(1, int(np.ceil(self.image.shape[0] / targetImageSize))),
int(np.ceil(self.image.shape[1] / targetImageSize))) max(1, int(np.ceil(self.image.shape[1] / targetImageSize))))
if np.isscalar(step): if np.isscalar(step):
step = (step, step) step = (step, step)
stepData = self.image[::step[0], ::step[1]] stepData = self.image[::step[0], ::step[1]]

View File

@ -110,7 +110,8 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
#print("-------") #print("-------")
for sample, label in self.items: for sample, label in self.items:
height += max(sample.height(), label.height()) + 3 height += max(sample.height(), label.height()) + 3
width = max(width, sample.width()+label.width()) width = max(width, (sample.sizeHint(QtCore.Qt.MinimumSize, sample.size()).width() +
label.sizeHint(QtCore.Qt.MinimumSize, label.size()).width()))
#print(width, height) #print(width, height)
#print width, height #print width, height
self.setGeometry(0, 0, width+25, height) self.setGeometry(0, 0, width+25, height)

View File

@ -649,6 +649,9 @@ class ScatterPlotItem(GraphicsObject):
d = d[mask] d = d[mask]
d2 = d2[mask] d2 = d2[mask]
if d.size == 0:
return (None, None)
if frac >= 1.0: if frac >= 1.0:
self.bounds[ax] = (np.nanmin(d) - self._maxSpotWidth*0.7072, np.nanmax(d) + self._maxSpotWidth*0.7072) self.bounds[ax] = (np.nanmin(d) - self._maxSpotWidth*0.7072, np.nanmax(d) + self._maxSpotWidth*0.7072)
return self.bounds[ax] return self.bounds[ax]
@ -701,16 +704,12 @@ class ScatterPlotItem(GraphicsObject):
GraphicsObject.setExportMode(self, *args, **kwds) GraphicsObject.setExportMode(self, *args, **kwds)
self.invalidate() self.invalidate()
def mapPointsToDevice(self, pts): def mapPointsToDevice(self, pts):
# Map point locations to device # Map point locations to device
tr = self.deviceTransform() tr = self.deviceTransform()
if tr is None: if tr is None:
return None return None
#pts = np.empty((2,len(self.data['x'])))
#pts[0] = self.data['x']
#pts[1] = self.data['y']
pts = fn.transformCoordinates(tr, pts) pts = fn.transformCoordinates(tr, pts)
pts -= self.data['width'] pts -= self.data['width']
pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
@ -731,7 +730,6 @@ class ScatterPlotItem(GraphicsObject):
(pts[1] - w < viewBounds.bottom())) ## remove out of view points (pts[1] - w < viewBounds.bottom())) ## remove out of view points
return mask return mask
@debug.warnOnException ## raising an exception here causes crash @debug.warnOnException ## raising an exception here causes crash
def paint(self, p, *args): def paint(self, p, *args):
cmode = self.opts.get('compositionMode', None) cmode = self.opts.get('compositionMode', None)
@ -758,8 +756,6 @@ class ScatterPlotItem(GraphicsObject):
# Cull points that are outside view # Cull points that are outside view
viewMask = self.getViewMask(pts) viewMask = self.getViewMask(pts)
#pts = pts[:,mask]
#data = self.data[mask]
if self.opts['useCache'] and self._exportOpts is False: if self.opts['useCache'] and self._exportOpts is False:
# Draw symbols from pre-rendered atlas # Draw symbols from pre-rendered atlas
@ -804,9 +800,9 @@ class ScatterPlotItem(GraphicsObject):
self.picture.play(p) self.picture.play(p)
def points(self): def points(self):
for rec in self.data: for i,rec in enumerate(self.data):
if rec['item'] is None: if rec['item'] is None:
rec['item'] = SpotItem(rec, self) rec['item'] = SpotItem(rec, self, i)
return self.data['item'] return self.data['item']
def pointsAt(self, pos): def pointsAt(self, pos):
@ -854,18 +850,26 @@ class SpotItem(object):
by connecting to the ScatterPlotItem's click signals. by connecting to the ScatterPlotItem's click signals.
""" """
def __init__(self, data, plot): def __init__(self, data, plot, index):
#GraphicsItem.__init__(self, register=False)
self._data = data self._data = data
self._plot = plot self._index = index
#self.setParentItem(plot) # SpotItems are kept in plot.data["items"] numpy object array which
#self.setPos(QtCore.QPointF(data['x'], data['y'])) # does not support cyclic garbage collection (numpy issue 6581).
#self.updateItem() # Keeping a strong ref to plot here would leak the cycle
self.__plot_ref = weakref.ref(plot)
@property
def _plot(self):
return self.__plot_ref()
def data(self): def data(self):
"""Return the user data associated with this spot.""" """Return the user data associated with this spot."""
return self._data['data'] return self._data['data']
def index(self):
"""Return the index of this point as given in the scatter plot data."""
return self._index
def size(self): def size(self):
"""Return the size of this spot. """Return the size of this spot.
If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" If the spot has no explicit size set, then return the ScatterPlotItem's default size instead."""
@ -949,37 +953,3 @@ class SpotItem(object):
self._data['sourceRect'] = None self._data['sourceRect'] = None
self._plot.updateSpots(self._data.reshape(1)) self._plot.updateSpots(self._data.reshape(1))
self._plot.invalidate() self._plot.invalidate()
#class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem):
#def __init__(self, data, plot):
#QtGui.QGraphicsPixmapItem.__init__(self)
#self.setFlags(self.flags() | self.ItemIgnoresTransformations)
#SpotItem.__init__(self, data, plot)
#def setPixmap(self, pixmap):
#QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap)
#self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.)
#def updateItem(self):
#symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol'])
### If all symbol options are default, use default pixmap
#if symbolOpts == (None, None, -1, ''):
#pixmap = self._plot.defaultSpotPixmap()
#else:
#pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol())
#self.setPixmap(pixmap)
#class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem):
#def __init__(self, data, plot):
#QtGui.QGraphicsPathItem.__init__(self)
#SpotItem.__init__(self, data, plot)
#def updateItem(self):
#QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()])
#QtGui.QGraphicsPathItem.setPen(self, self.pen())
#QtGui.QGraphicsPathItem.setBrush(self, self.brush())
#size = self.size()
#self.resetTransform()
#self.scale(size, size)

View File

@ -6,17 +6,11 @@ it is possible to place any widget into its own window by simply calling its
show() method. show() method.
""" """
from .Qt import QtCore, QtGui from .Qt import QtCore, QtGui, mkQApp
from .widgets.PlotWidget import * from .widgets.PlotWidget import *
from .imageview import * from .imageview import *
from .widgets.GraphicsLayoutWidget import GraphicsLayoutWidget from .widgets.GraphicsLayoutWidget import GraphicsLayoutWidget
from .widgets.GraphicsView import GraphicsView from .widgets.GraphicsView import GraphicsView
QAPP = None
def mkQApp():
if QtGui.QApplication.instance() is None:
global QAPP
QAPP = QtGui.QApplication([])
class GraphicsWindow(GraphicsLayoutWidget): class GraphicsWindow(GraphicsLayoutWidget):

View File

@ -637,8 +637,12 @@ class ImageView(QtGui.QWidget):
cax = self.axes['c'] cax = self.axes['c']
if cax is None: if cax is None:
if data.size == 0:
return [(0, 0)]
return [(float(nanmin(data)), float(nanmax(data)))] return [(float(nanmin(data)), float(nanmax(data)))]
else: else:
if data.size == 0:
return [(0, 0)] * data.shape[-1]
return [(float(nanmin(data.take(i, axis=cax))), return [(float(nanmin(data.take(i, axis=cax))),
float(nanmax(data.take(i, axis=cax)))) for i in range(data.shape[-1])] float(nanmax(data.take(i, axis=cax)))) for i in range(data.shape[-1])]

View File

@ -195,6 +195,8 @@ class Parallelize(object):
finally: finally:
if self.showProgress: if self.showProgress:
self.progressDlg.__exit__(None, None, None) self.progressDlg.__exit__(None, None, None)
for ch in self.childs:
ch.join()
if len(self.exitCodes) < len(self.childs): if len(self.exitCodes) < len(self.childs):
raise Exception("Parallelizer started %d processes but only received exit codes from %d." % (len(self.childs), len(self.exitCodes))) raise Exception("Parallelizer started %d processes but only received exit codes from %d." % (len(self.childs), len(self.exitCodes)))
for code in self.exitCodes: for code in self.exitCodes:

View File

@ -165,6 +165,7 @@ 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.conn.close()
self.debugMsg('Child process exited. (%d)' % self.proc.returncode) self.debugMsg('Child process exited. (%d)' % self.proc.returncode)
def debugMsg(self, msg, *args): def debugMsg(self, msg, *args):
@ -341,6 +342,7 @@ class ForkedProcess(RemoteEventHandler):
except OSError: ## probably remote process has already quit except OSError: ## probably remote process has already quit
pass pass
self.conn.close() # don't leak file handles!
self.hasJoined = True self.hasJoined = True
def kill(self): def kill(self):

View File

@ -16,9 +16,13 @@ class GLViewWidget(QtOpenGL.QGLWidget):
- Axis/grid display - Axis/grid display
- Export options - Export options
High-DPI displays: Qt5 should automatically detect the correct resolution.
For Qt4, specify the ``devicePixelRatio`` argument when initializing the
widget (usually this value is 1-2).
""" """
def __init__(self, parent=None): def __init__(self, parent=None, devicePixelRatio=None):
global ShareWidget global ShareWidget
if ShareWidget is None: if ShareWidget is None:
@ -37,6 +41,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
'azimuth': 45, ## camera's azimuthal angle in degrees 'azimuth': 45, ## camera's azimuthal angle in degrees
## (rotation around z-axis 0 points along x-axis) ## (rotation around z-axis 0 points along x-axis)
'viewport': None, ## glViewport params; None == whole widget 'viewport': None, ## glViewport params; None == whole widget
'devicePixelRatio': devicePixelRatio,
} }
self.setBackgroundColor('k') self.setBackgroundColor('k')
self.items = [] self.items = []
@ -79,10 +84,21 @@ class GLViewWidget(QtOpenGL.QGLWidget):
def getViewport(self): def getViewport(self):
vp = self.opts['viewport'] vp = self.opts['viewport']
dpr = self.devicePixelRatio()
if vp is None: if vp is None:
return (0, 0, self.width(), self.height()) return (0, 0, int(self.width() * dpr), int(self.height() * dpr))
else: else:
return vp return tuple([int(x * dpr) for x in vp])
def devicePixelRatio(self):
dpr = self.opts['devicePixelRatio']
if dpr is not None:
return dpr
if hasattr(QtOpenGL.QGLWidget, 'devicePixelRatio'):
return QtOpenGL.QGLWidget.devicePixelRatio(self)
else:
return 1.0
def resizeGL(self, w, h): def resizeGL(self, w, h):
pass pass
@ -99,7 +115,8 @@ class GLViewWidget(QtOpenGL.QGLWidget):
def projectionMatrix(self, region=None): def projectionMatrix(self, region=None):
# Xw = (Xnd + 1) * width/2 + X # Xw = (Xnd + 1) * width/2 + X
if region is None: if region is None:
region = (0, 0, self.width(), self.height()) dpr = self.devicePixelRatio()
region = (0, 0, self.width() * dpr, self.height() * dpr)
x0, y0, w, h = self.getViewport() x0, y0, w, h = self.getViewport()
dist = self.opts['distance'] dist = self.opts['distance']

View File

@ -485,7 +485,7 @@ class MeshData(object):
if isinstance(radius, int): if isinstance(radius, int):
radius = [radius, radius] # convert to list radius = [radius, radius] # convert to list
## compute vertexes ## compute vertexes
th = np.linspace(2 * np.pi, 0, cols).reshape(1, cols) th = np.linspace(2 * np.pi, (2 * np.pi)/cols, cols).reshape(1, cols)
r = np.linspace(radius[0],radius[1],num=rows+1, endpoint=True).reshape(rows+1, 1) # radius as a function of z r = np.linspace(radius[0],radius[1],num=rows+1, endpoint=True).reshape(rows+1, 1) # radius as a function of z
verts[...,2] = np.linspace(0, length, num=rows+1, endpoint=True).reshape(rows+1, 1) # z verts[...,2] = np.linspace(0, length, num=rows+1, endpoint=True).reshape(rows+1, 1) # z
if offset: if offset:

View File

@ -10,10 +10,10 @@ class GLGridItem(GLGraphicsItem):
""" """
**Bases:** :class:`GLGraphicsItem <pyqtgraph.opengl.GLGraphicsItem>` **Bases:** :class:`GLGraphicsItem <pyqtgraph.opengl.GLGraphicsItem>`
Displays a wire-grame grid. Displays a wire-frame grid.
""" """
def __init__(self, size=None, color=None, antialias=True, glOptions='translucent'): def __init__(self, size=None, color=(1, 1, 1, .3), antialias=True, glOptions='translucent'):
GLGraphicsItem.__init__(self) GLGraphicsItem.__init__(self)
self.setGLOptions(glOptions) self.setGLOptions(glOptions)
self.antialias = antialias self.antialias = antialias
@ -21,6 +21,7 @@ class GLGridItem(GLGraphicsItem):
size = QtGui.QVector3D(20,20,1) size = QtGui.QVector3D(20,20,1)
self.setSize(size=size) self.setSize(size=size)
self.setSpacing(1, 1, 1) self.setSpacing(1, 1, 1)
self.color = color
def setSize(self, x=None, y=None, z=None, size=None): def setSize(self, x=None, y=None, z=None, size=None):
""" """
@ -66,8 +67,8 @@ class GLGridItem(GLGraphicsItem):
x,y,z = self.size() x,y,z = self.size()
xs,ys,zs = self.spacing() xs,ys,zs = self.spacing()
xvals = np.arange(-x/2., x/2. + xs*0.001, xs) xvals = np.arange(-x/2., x/2. + xs*0.001, xs)
yvals = np.arange(-y/2., y/2. + ys*0.001, ys) yvals = np.arange(-y/2., y/2. + ys*0.001, ys)
glColor4f(1, 1, 1, .3) glColor4f(*self.color)
for x in xvals: for x in xvals:
glVertex3f(x, yvals[0], 0) glVertex3f(x, yvals[0], 0)
glVertex3f(x, yvals[-1], 0) glVertex3f(x, yvals[-1], 0)

View File

@ -152,7 +152,9 @@ class GLScatterPlotItem(GLGraphicsItem):
glDisableClientState(GL_VERTEX_ARRAY) glDisableClientState(GL_VERTEX_ARRAY)
glDisableClientState(GL_COLOR_ARRAY) glDisableClientState(GL_COLOR_ARRAY)
#posVBO.unbind() #posVBO.unbind()
##fixes #145
glDisable( GL_TEXTURE_2D )
#for i in range(len(self.pos)): #for i in range(len(self.pos)):
#pos = self.pos[i] #pos = self.pos[i]

View File

@ -105,6 +105,7 @@ class WidgetParameterItem(ParameterItem):
if t == 'int': if t == 'int':
defs['int'] = True defs['int'] = True
defs['minStep'] = 1.0 defs['minStep'] = 1.0
defs['format'] = '{value:d}'
for k in defs: for k in defs:
if k in opts: if k in opts:
defs[k] = opts[k] defs[k] = opts[k]

View File

@ -21,13 +21,17 @@ Does NOT:
print module.someObject print module.someObject
""" """
from __future__ import print_function
import inspect, os, sys, gc, traceback import inspect, os, sys, gc, traceback, types
try:
import __builtin__ as builtins
except ImportError:
import builtins
from .debug import printExc from .debug import printExc
try:
from importlib import reload as orig_reload
except ImportError:
orig_reload = reload
py3 = sys.version_info >= (3,)
def reloadAll(prefix=None, debug=False): def reloadAll(prefix=None, debug=False):
"""Automatically reload everything whose __file__ begins with prefix. """Automatically reload everything whose __file__ begins with prefix.
@ -79,7 +83,7 @@ def reload(module, debug=False, lists=False, dicts=False):
## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison ## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison
oldDict = module.__dict__.copy() oldDict = module.__dict__.copy()
builtins.reload(module) orig_reload(module)
newDict = module.__dict__ newDict = module.__dict__
## Allow modules access to the old dictionary after they reload ## Allow modules access to the old dictionary after they reload
@ -97,7 +101,9 @@ def reload(module, debug=False, lists=False, dicts=False):
if debug: if debug:
print(" Updating class %s.%s (0x%x -> 0x%x)" % (module.__name__, k, id(old), id(new))) print(" Updating class %s.%s (0x%x -> 0x%x)" % (module.__name__, k, id(old), id(new)))
updateClass(old, new, debug) updateClass(old, new, debug)
# don't put this inside updateClass because it is reentrant.
new.__previous_reload_version__ = old
elif inspect.isfunction(old): elif inspect.isfunction(old):
depth = updateFunction(old, new, debug) depth = updateFunction(old, new, debug)
if debug: if debug:
@ -127,6 +133,9 @@ def updateFunction(old, new, debug, depth=0, visited=None):
old.__code__ = new.__code__ old.__code__ = new.__code__
old.__defaults__ = new.__defaults__ old.__defaults__ = new.__defaults__
if hasattr(old, '__kwdefaults'):
old.__kwdefaults__ = new.__kwdefaults__
old.__doc__ = new.__doc__
if visited is None: if visited is None:
visited = [] visited = []
@ -151,8 +160,9 @@ def updateFunction(old, new, debug, depth=0, visited=None):
## For classes: ## For classes:
## 1) find all instances of the old class and set instance.__class__ to the new class ## 1) find all instances of the old class and set instance.__class__ to the new class
## 2) update all old class methods to use code from the new class methods ## 2) update all old class methods to use code from the new class methods
def updateClass(old, new, debug):
def updateClass(old, new, debug):
## Track town all instances and subclasses of old ## Track town all instances and subclasses of old
refs = gc.get_referrers(old) refs = gc.get_referrers(old)
for ref in refs: for ref in refs:
@ -174,13 +184,20 @@ def updateClass(old, new, debug):
## This seems to work. Is there any reason not to? ## This seems to work. Is there any reason not to?
## Note that every time we reload, the class hierarchy becomes more complex. ## Note that every time we reload, the class hierarchy becomes more complex.
## (and I presume this may slow things down?) ## (and I presume this may slow things down?)
ref.__bases__ = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:] newBases = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:]
try:
ref.__bases__ = newBases
except TypeError:
print(" Error setting bases for class %s" % ref)
print(" old bases: %s" % repr(ref.__bases__))
print(" new bases: %s" % repr(newBases))
raise
if debug: if debug:
print(" Changed superclass for %s" % safeStr(ref)) print(" Changed superclass for %s" % safeStr(ref))
#else: #else:
#if debug: #if debug:
#print " Ignoring reference", type(ref) #print " Ignoring reference", type(ref)
except: except Exception:
print("Error updating reference (%s) for class change (%s -> %s)" % (safeStr(ref), safeStr(old), safeStr(new))) print("Error updating reference (%s) for class change (%s -> %s)" % (safeStr(ref), safeStr(old), safeStr(new)))
raise raise
@ -189,7 +206,8 @@ def updateClass(old, new, debug):
## but it fixes a few specific cases (pyqt signals, for one) ## but it fixes a few specific cases (pyqt signals, for one)
for attr in dir(old): for attr in dir(old):
oa = getattr(old, attr) oa = getattr(old, attr)
if inspect.ismethod(oa): if (py3 and inspect.isfunction(oa)) or inspect.ismethod(oa):
# note python2 has unbound methods, whereas python3 just uses plain functions
try: try:
na = getattr(new, attr) na = getattr(new, attr)
except AttributeError: except AttributeError:
@ -197,9 +215,14 @@ def updateClass(old, new, debug):
print(" Skipping method update for %s; new class does not have this attribute" % attr) print(" Skipping method update for %s; new class does not have this attribute" % attr)
continue continue
if hasattr(oa, 'im_func') and hasattr(na, 'im_func') and oa.__func__ is not na.__func__: ofunc = getattr(oa, '__func__', oa) # in py2 we have to get the __func__ from unbound method,
depth = updateFunction(oa.__func__, na.__func__, debug) nfunc = getattr(na, '__func__', na) # in py3 the attribute IS the function
#oa.im_class = new ## bind old method to new class ## not allowed
if ofunc is not nfunc:
depth = updateFunction(ofunc, nfunc, debug)
if not hasattr(nfunc, '__previous_reload_method__'):
nfunc.__previous_reload_method__ = oa # important for managing signal connection
#oa.__class__ = new ## bind old method to new class ## not allowed
if debug: if debug:
extra = "" extra = ""
if depth > 0: if depth > 0:
@ -208,6 +231,8 @@ def updateClass(old, new, debug):
## And copy in new functions that didn't exist previously ## And copy in new functions that didn't exist previously
for attr in dir(new): for attr in dir(new):
if attr == '__previous_reload_version__':
continue
if not hasattr(old, attr): if not hasattr(old, attr):
if debug: if debug:
print(" Adding missing attribute %s" % attr) print(" Adding missing attribute %s" % attr)
@ -223,14 +248,37 @@ def updateClass(old, new, debug):
def safeStr(obj): def safeStr(obj):
try: try:
s = str(obj) s = str(obj)
except: except Exception:
try: try:
s = repr(obj) s = repr(obj)
except: except Exception:
s = "<instance of %s at 0x%x>" % (safeStr(type(obj)), id(obj)) s = "<instance of %s at 0x%x>" % (safeStr(type(obj)), id(obj))
return s return s
def getPreviousVersion(obj):
"""Return the previous version of *obj*, or None if this object has not
been reloaded.
"""
if isinstance(obj, type) or inspect.isfunction(obj):
return getattr(obj, '__previous_reload_version__', None)
elif inspect.ismethod(obj):
if obj.__self__ is None:
# unbound method
return getattr(obj.__func__, '__previous_reload_method__', None)
else:
oldmethod = getattr(obj.__func__, '__previous_reload_method__', None)
if oldmethod is None:
return None
self = obj.__self__
oldfunc = getattr(oldmethod, '__func__', oldmethod)
if hasattr(oldmethod, 'im_class'):
# python 2
cls = oldmethod.im_class
return types.MethodType(oldfunc, self, cls)
else:
# python 3
return types.MethodType(oldfunc, self)

View File

@ -0,0 +1,116 @@
import tempfile, os, sys, shutil
import pyqtgraph as pg
import pyqtgraph.reload
pgpath = os.path.join(os.path.dirname(pg.__file__), '..')
# make temporary directory to write module code
path = None
def setup_module():
# make temporary directory to write module code
global path
path = tempfile.mkdtemp()
sys.path.insert(0, path)
def teardown_module():
global path
shutil.rmtree(path)
sys.path.remove(path)
code = """
import sys
sys.path.append('{path}')
import pyqtgraph as pg
class C(pg.QtCore.QObject):
sig = pg.QtCore.Signal()
def fn(self):
print("{msg}")
"""
def remove_cache(mod):
if os.path.isfile(mod+'c'):
os.remove(mod+'c')
cachedir = os.path.join(os.path.dirname(mod), '__pycache__')
if os.path.isdir(cachedir):
shutil.rmtree(cachedir)
def test_reload():
py3 = sys.version_info >= (3,)
# write a module
mod = os.path.join(path, 'reload_test_mod.py')
print("\nRELOAD FILE:", mod)
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version1"))
# import the new module
import reload_test_mod
print("RELOAD MOD:", reload_test_mod.__file__)
c = reload_test_mod.C()
c.sig.connect(c.fn)
if py3:
v1 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__)
else:
v1 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__)
# write again and reload
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2"))
remove_cache(mod)
pg.reload.reloadAll(path, debug=True)
if py3:
v2 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__)
else:
v2 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__)
if not py3:
assert c.fn.im_class is v2[0]
oldcfn = pg.reload.getPreviousVersion(c.fn)
if oldcfn is None:
# Function did not reload; are we using pytest's assertion rewriting?
raise Exception("Function did not reload. (This can happen when using py.test"
" with assertion rewriting; use --assert=plain for this test.)")
if py3:
assert oldcfn.__func__ is v1[2]
else:
assert oldcfn.im_class is v1[0]
assert oldcfn.__func__ is v1[2].__func__
assert oldcfn.__self__ is c
# write again and reload
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2"))
remove_cache(mod)
pg.reload.reloadAll(path, debug=True)
if py3:
v3 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__)
else:
v3 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__)
#for i in range(len(old)):
#print id(old[i]), id(new1[i]), id(new2[i]), old[i], new1[i]
cfn1 = pg.reload.getPreviousVersion(c.fn)
cfn2 = pg.reload.getPreviousVersion(cfn1)
if py3:
assert cfn1.__func__ is v2[2]
assert cfn2.__func__ is v1[2]
else:
assert cfn1.__func__ is v2[2].__func__
assert cfn2.__func__ is v1[2].__func__
assert cfn1.im_class is v2[0]
assert cfn2.im_class is v1[0]
assert cfn1.__self__ is c
assert cfn2.__self__ is c
pg.functions.disconnect(c.sig, c.fn)

View File

@ -9,16 +9,22 @@ class BusyCursor(object):
with pyqtgraph.BusyCursor(): with pyqtgraph.BusyCursor():
doLongOperation() doLongOperation()
May be nested. May be nested. If called from a non-gui thread, then the cursor will not be affected.
""" """
active = [] active = []
def __enter__(self): def __enter__(self):
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) app = QtCore.QCoreApplication.instance()
BusyCursor.active.append(self) isGuiThread = (app is not None) and (QtCore.QThread.currentThread() == app.thread())
if isGuiThread and QtGui.QApplication.instance() is not None:
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
BusyCursor.active.append(self)
self._active = True
else:
self._active = False
def __exit__(self, *args): def __exit__(self, *args):
BusyCursor.active.pop(-1) if self._active:
if len(BusyCursor.active) == 0: BusyCursor.active.pop(-1)
QtGui.QApplication.restoreOverrideCursor() QtGui.QApplication.restoreOverrideCursor()

View File

@ -42,6 +42,11 @@ class ColorMapWidget(ptree.ParameterTree):
def restoreState(self, state): def restoreState(self, state):
self.params.restoreState(state) self.params.restoreState(state)
def addColorMap(self, name):
"""Add a new color mapping and return the created parameter.
"""
return self.params.addNew(name)
class ColorMapParameter(ptree.types.GroupParameter): class ColorMapParameter(ptree.types.GroupParameter):
sigColorMapChanged = QtCore.Signal(object) sigColorMapChanged = QtCore.Signal(object)

View File

@ -3,13 +3,18 @@ from .. import parametertree as ptree
import numpy as np import numpy as np
from ..pgcollections import OrderedDict from ..pgcollections import OrderedDict
from .. import functions as fn from .. import functions as fn
from ..python2_3 import basestring
__all__ = ['DataFilterWidget'] __all__ = ['DataFilterWidget']
class DataFilterWidget(ptree.ParameterTree): class DataFilterWidget(ptree.ParameterTree):
""" """
This class allows the user to filter multi-column data sets by specifying This class allows the user to filter multi-column data sets by specifying
multiple criteria multiple criteria
Wraps methods from DataFilterParameter: setFields, generateMask,
filterData, and describe.
""" """
sigFilterChanged = QtCore.Signal(object) sigFilterChanged = QtCore.Signal(object)
@ -22,6 +27,7 @@ class DataFilterWidget(ptree.ParameterTree):
self.params.sigTreeStateChanged.connect(self.filterChanged) self.params.sigTreeStateChanged.connect(self.filterChanged)
self.setFields = self.params.setFields self.setFields = self.params.setFields
self.generateMask = self.params.generateMask
self.filterData = self.params.filterData self.filterData = self.params.filterData
self.describe = self.params.describe self.describe = self.params.describe
@ -30,10 +36,16 @@ class DataFilterWidget(ptree.ParameterTree):
def parameters(self): def parameters(self):
return self.params return self.params
def addFilter(self, name):
"""Add a new filter and return the created parameter item.
"""
return self.params.addNew(name)
class DataFilterParameter(ptree.types.GroupParameter): class DataFilterParameter(ptree.types.GroupParameter):
"""A parameter group that specifies a set of filters to apply to tabular data.
"""
sigFilterChanged = QtCore.Signal(object) sigFilterChanged = QtCore.Signal(object)
def __init__(self): def __init__(self):
@ -47,18 +59,36 @@ class DataFilterParameter(ptree.types.GroupParameter):
def addNew(self, name): def addNew(self, name):
mode = self.fields[name].get('mode', 'range') mode = self.fields[name].get('mode', 'range')
if mode == 'range': if mode == 'range':
self.addChild(RangeFilterItem(name, self.fields[name])) child = self.addChild(RangeFilterItem(name, self.fields[name]))
elif mode == 'enum': elif mode == 'enum':
self.addChild(EnumFilterItem(name, self.fields[name])) child = self.addChild(EnumFilterItem(name, self.fields[name]))
return child
def fieldNames(self): def fieldNames(self):
return self.fields.keys() return self.fields.keys()
def setFields(self, fields): def setFields(self, fields):
"""Set the list of fields that are available to be filtered.
*fields* must be a dict or list of tuples that maps field names
to a specification describing the field. Each specification is
itself a dict with either ``'mode':'range'`` or ``'mode':'enum'``::
filter.setFields([
('field1', {'mode': 'range'}),
('field2', {'mode': 'enum', 'values': ['val1', 'val2', 'val3']}),
('field3', {'mode': 'enum', 'values': {'val1':True, 'val2':False, 'val3':True}}),
])
"""
self.fields = OrderedDict(fields) self.fields = OrderedDict(fields)
names = self.fieldNames() names = self.fieldNames()
self.setAddList(names) self.setAddList(names)
# update any existing filters
for ch in self.children():
name = ch.fieldName
if name in fields:
ch.updateFilter(fields[name])
def filterData(self, data): def filterData(self, data):
if len(data) == 0: if len(data) == 0:
@ -66,6 +96,9 @@ class DataFilterParameter(ptree.types.GroupParameter):
return data[self.generateMask(data)] return data[self.generateMask(data)]
def generateMask(self, data): def generateMask(self, data):
"""Return a boolean mask indicating whether each item in *data* passes
the filter critera.
"""
mask = np.ones(len(data), dtype=bool) mask = np.ones(len(data), dtype=bool)
if len(data) == 0: if len(data) == 0:
return mask return mask
@ -89,6 +122,7 @@ class DataFilterParameter(ptree.types.GroupParameter):
desc.append(fp.describe()) desc.append(fp.describe())
return desc return desc
class RangeFilterItem(ptree.types.SimpleParameter): class RangeFilterItem(ptree.types.SimpleParameter):
def __init__(self, name, opts): def __init__(self, name, opts):
self.fieldName = name self.fieldName = name
@ -109,25 +143,17 @@ class RangeFilterItem(ptree.types.SimpleParameter):
def describe(self): def describe(self):
return "%s < %s < %s" % (fn.siFormat(self['Min'], suffix=self.units), self.fieldName, fn.siFormat(self['Max'], suffix=self.units)) return "%s < %s < %s" % (fn.siFormat(self['Min'], suffix=self.units), self.fieldName, fn.siFormat(self['Max'], suffix=self.units))
def updateFilter(self, opts):
pass
class EnumFilterItem(ptree.types.SimpleParameter): class EnumFilterItem(ptree.types.SimpleParameter):
def __init__(self, name, opts): def __init__(self, name, opts):
self.fieldName = name self.fieldName = name
vals = opts.get('values', [])
childs = []
if isinstance(vals, list):
vals = OrderedDict([(v,str(v)) for v in vals])
for val,vname in vals.items():
ch = ptree.Parameter.create(name=vname, type='bool', value=True)
ch.maskValue = val
childs.append(ch)
ch = ptree.Parameter.create(name='(other)', type='bool', value=True)
ch.maskValue = '__other__'
childs.append(ch)
ptree.types.SimpleParameter.__init__(self, ptree.types.SimpleParameter.__init__(self,
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True, name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True)
children=childs) self.setEnumVals(opts)
def generateMask(self, data, startMask): def generateMask(self, data, startMask):
vals = data[self.fieldName][startMask] vals = data[self.fieldName][startMask]
@ -147,4 +173,38 @@ class EnumFilterItem(ptree.types.SimpleParameter):
def describe(self): def describe(self):
vals = [ch.name() for ch in self if ch.value() is True] vals = [ch.name() for ch in self if ch.value() is True]
return "%s: %s" % (self.fieldName, ', '.join(vals)) return "%s: %s" % (self.fieldName, ', '.join(vals))
def updateFilter(self, opts):
self.setEnumVals(opts)
def setEnumVals(self, opts):
vals = opts.get('values', {})
prevState = {}
for ch in self.children():
prevState[ch.name()] = ch.value()
self.removeChild(ch)
if not isinstance(vals, dict):
vals = OrderedDict([(v,(str(v), True)) for v in vals])
# Each filterable value can come with either (1) a string name, (2) a bool
# indicating whether the value is enabled by default, or (3) a tuple providing
# both.
for val,valopts in vals.items():
if isinstance(valopts, bool):
enabled = valopts
vname = str(val)
elif isinstance(valopts, basestring):
enabled = True
vname = valopts
elif isinstance(valopts, tuple):
vname, enabled = valopts
ch = ptree.Parameter.create(name=vname, type='bool', value=prevState.get(vname, enabled))
ch.maskValue = val
self.addChild(ch)
ch = ptree.Parameter.create(name='(other)', type='bool', value=prevState.get('(other)', True))
ch.maskValue = '__other__'
self.addChild(ch)

View File

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
from ..pgcollections import OrderedDict from ..pgcollections import OrderedDict
from .TableWidget import TableWidget
from ..python2_3 import asUnicode
import types, traceback import types, traceback
import numpy as np import numpy as np
@ -17,67 +19,106 @@ class DataTreeWidget(QtGui.QTreeWidget):
Widget for displaying hierarchical python data structures Widget for displaying hierarchical python data structures
(eg, nested dicts, lists, and arrays) (eg, nested dicts, lists, and arrays)
""" """
def __init__(self, parent=None, data=None): def __init__(self, parent=None, data=None):
QtGui.QTreeWidget.__init__(self, parent) QtGui.QTreeWidget.__init__(self, parent)
self.setVerticalScrollMode(self.ScrollPerPixel) self.setVerticalScrollMode(self.ScrollPerPixel)
self.setData(data) self.setData(data)
self.setColumnCount(3) self.setColumnCount(3)
self.setHeaderLabels(['key / index', 'type', 'value']) self.setHeaderLabels(['key / index', 'type', 'value'])
self.setAlternatingRowColors(True)
def setData(self, data, hideRoot=False): def setData(self, data, hideRoot=False):
"""data should be a dictionary.""" """data should be a dictionary."""
self.clear() self.clear()
self.widgets = []
self.nodes = {}
self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot) self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot)
#node = self.mkNode('', data)
#while node.childCount() > 0:
#c = node.child(0)
#node.removeChild(c)
#self.invisibleRootItem().addChild(c)
self.expandToDepth(3) self.expandToDepth(3)
self.resizeColumnToContents(0) self.resizeColumnToContents(0)
def buildTree(self, data, parent, name='', hideRoot=False): def buildTree(self, data, parent, name='', hideRoot=False, path=()):
if hideRoot: if hideRoot:
node = parent node = parent
else: else:
typeStr = type(data).__name__ node = QtGui.QTreeWidgetItem([name, "", ""])
if typeStr == 'instance':
typeStr += ": " + data.__class__.__name__
node = QtGui.QTreeWidgetItem([name, typeStr, ""])
parent.addChild(node) parent.addChild(node)
if isinstance(data, types.TracebackType): ## convert traceback to a list of strings # record the path to the node so it can be retrieved later
data = list(map(str.strip, traceback.format_list(traceback.extract_tb(data)))) # (this is used by DiffTreeWidget)
elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): self.nodes[path] = node
data = {
'data': data.view(np.ndarray), typeStr, desc, childs, widget = self.parse(data)
'meta': data.infoCopy() node.setText(1, typeStr)
} node.setText(2, desc)
# Truncate description and add text box if needed
if len(desc) > 100:
desc = desc[:97] + '...'
if widget is None:
widget = QtGui.QPlainTextEdit(asUnicode(data))
widget.setMaximumHeight(200)
widget.setReadOnly(True)
# Add widget to new subnode
if widget is not None:
self.widgets.append(widget)
subnode = QtGui.QTreeWidgetItem(["", "", ""])
node.addChild(subnode)
self.setItemWidget(subnode, 0, widget)
self.setFirstItemColumnSpanned(subnode, True)
# recurse to children
for key, data in childs.items():
self.buildTree(data, node, asUnicode(key), path=path+(key,))
def parse(self, data):
"""
Given any python object, return:
* type
* a short string representation
* a dict of sub-objects to be parsed
* optional widget to display as sub-node
"""
# defaults for all objects
typeStr = type(data).__name__
if typeStr == 'instance':
typeStr += ": " + data.__class__.__name__
widget = None
desc = ""
childs = {}
# type-specific changes
if isinstance(data, dict): if isinstance(data, dict):
for k in data.keys(): desc = "length=%d" % len(data)
self.buildTree(data[k], node, str(k)) if isinstance(data, OrderedDict):
elif isinstance(data, list) or isinstance(data, tuple): childs = data
for i in range(len(data)): else:
self.buildTree(data[i], node, str(i)) childs = OrderedDict(sorted(data.items()))
elif isinstance(data, (list, tuple)):
desc = "length=%d" % len(data)
childs = OrderedDict(enumerate(data))
elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
childs = OrderedDict([
('data', data.view(np.ndarray)),
('meta', data.infoCopy())
])
elif isinstance(data, np.ndarray):
desc = "shape=%s dtype=%s" % (data.shape, data.dtype)
table = TableWidget()
table.setData(data)
table.setMaximumHeight(200)
widget = table
elif isinstance(data, types.TracebackType): ## convert traceback to a list of strings
frames = list(map(str.strip, traceback.format_list(traceback.extract_tb(data))))
#childs = OrderedDict([
#(i, {'file': child[0], 'line': child[1], 'function': child[2], 'code': child[3]})
#for i, child in enumerate(frames)])
#childs = OrderedDict([(i, ch) for i,ch in enumerate(frames)])
widget = QtGui.QPlainTextEdit(asUnicode('\n'.join(frames)))
widget.setMaximumHeight(200)
widget.setReadOnly(True)
else: else:
node.setText(2, str(data)) desc = asUnicode(data)
#def mkNode(self, name, v):
#if type(v) is list and len(v) > 0 and isinstance(v[0], dict):
#inds = map(unicode, range(len(v)))
#v = OrderedDict(zip(inds, v))
#if isinstance(v, dict):
##print "\nadd tree", k, v
#node = QtGui.QTreeWidgetItem([name])
#for k in v:
#newNode = self.mkNode(k, v[k])
#node.addChild(newNode)
#else:
##print "\nadd value", k, str(v)
#node = QtGui.QTreeWidgetItem([unicode(name), unicode(v)])
#return node
return typeStr, desc, childs, widget

View File

@ -0,0 +1,164 @@
# -*- coding: utf-8 -*-
from ..Qt import QtGui, QtCore
from ..pgcollections import OrderedDict
from .DataTreeWidget import DataTreeWidget
from .. import functions as fn
import types, traceback
import numpy as np
__all__ = ['DiffTreeWidget']
class DiffTreeWidget(QtGui.QWidget):
"""
Widget for displaying differences between hierarchical python data structures
(eg, nested dicts, lists, and arrays)
"""
def __init__(self, parent=None, a=None, b=None):
QtGui.QWidget.__init__(self, parent)
self.layout = QtGui.QHBoxLayout()
self.setLayout(self.layout)
self.trees = [DataTreeWidget(self), DataTreeWidget(self)]
for t in self.trees:
self.layout.addWidget(t)
if a is not None:
self.setData(a, b)
def setData(self, a, b):
"""
Set the data to be compared in this widget.
"""
self.data = (a, b)
self.trees[0].setData(a)
self.trees[1].setData(b)
return self.compare(a, b)
def compare(self, a, b, path=()):
"""
Compare data structure *a* to structure *b*.
Return True if the objects match completely.
Otherwise, return a structure that describes the differences:
{ 'type': bool
'len': bool,
'str': bool,
'shape': bool,
'dtype': bool,
'mask': array,
}
"""
bad = (255, 200, 200)
diff = []
# generate typestr, desc, childs for each object
typeA, descA, childsA, _ = self.trees[0].parse(a)
typeB, descB, childsB, _ = self.trees[1].parse(b)
if typeA != typeB:
self.setColor(path, 1, bad)
if descA != descB:
self.setColor(path, 2, bad)
if isinstance(a, dict) and isinstance(b, dict):
keysA = set(a.keys())
keysB = set(b.keys())
for key in keysA - keysB:
self.setColor(path+(key,), 0, bad, tree=0)
for key in keysB - keysA:
self.setColor(path+(key,), 0, bad, tree=1)
for key in keysA & keysB:
self.compare(a[key], b[key], path+(key,))
elif isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
for i in range(max(len(a), len(b))):
if len(a) <= i:
self.setColor(path+(i,), 0, bad, tree=1)
elif len(b) <= i:
self.setColor(path+(i,), 0, bad, tree=0)
else:
self.compare(a[i], b[i], path+(i,))
elif isinstance(a, np.ndarray) and isinstance(b, np.ndarray) and a.shape == b.shape:
tableNodes = [tree.nodes[path].child(0) for tree in self.trees]
if a.dtype.fields is None and b.dtype.fields is None:
eq = self.compareArrays(a, b)
if not np.all(eq):
for n in tableNodes:
n.setBackground(0, fn.mkBrush(bad))
#for i in np.argwhere(~eq):
else:
if a.dtype == b.dtype:
for i,k in enumerate(a.dtype.fields.keys()):
eq = self.compareArrays(a[k], b[k])
if not np.all(eq):
for n in tableNodes:
n.setBackground(0, fn.mkBrush(bad))
#for j in np.argwhere(~eq):
# dict: compare keys, then values where keys match
# list:
# array: compare elementwise for same shape
def compareArrays(self, a, b):
intnan = -9223372036854775808 # happens when np.nan is cast to int
anans = np.isnan(a) | (a == intnan)
bnans = np.isnan(b) | (b == intnan)
eq = anans == bnans
mask = ~anans
eq[mask] = np.allclose(a[mask], b[mask])
return eq
def setColor(self, path, column, color, tree=None):
brush = fn.mkBrush(color)
# Color only one tree if specified.
if tree is None:
trees = self.trees
else:
trees = [self.trees[tree]]
for tree in trees:
item = tree.nodes[path]
item.setBackground(column, brush)
def _compare(self, a, b):
"""
Compare data structure *a* to structure *b*.
"""
# Check test structures are the same
assert type(info) is type(expect)
if hasattr(info, '__len__'):
assert len(info) == len(expect)
if isinstance(info, dict):
for k in info:
assert k in expect
for k in expect:
assert k in info
self.compare_results(info[k], expect[k])
elif isinstance(info, list):
for i in range(len(info)):
self.compare_results(info[i], expect[i])
elif isinstance(info, np.ndarray):
assert info.shape == expect.shape
assert info.dtype == expect.dtype
if info.dtype.fields is None:
intnan = -9223372036854775808 # happens when np.nan is cast to int
inans = np.isnan(info) | (info == intnan)
enans = np.isnan(expect) | (expect == intnan)
assert np.all(inans == enans)
mask = ~inans
assert np.allclose(info[mask], expect[mask])
else:
for k in info.dtype.fields.keys():
self.compare_results(info[k], expect[k])
else:
try:
assert info == expect
except Exception:
raise NotImplementedError("Cannot compare objects of type %s" % type(info))

View File

@ -1,4 +1,4 @@
from ..Qt import QtGui from ..Qt import QtGui, mkQApp
from ..graphicsItems.GraphicsLayout import GraphicsLayout from ..graphicsItems.GraphicsLayout import GraphicsLayout
from .GraphicsView import GraphicsView from .GraphicsView import GraphicsView
@ -48,6 +48,7 @@ class GraphicsLayoutWidget(GraphicsView):
:func:`clear <pyqtgraph.GraphicsLayout.clear>` :func:`clear <pyqtgraph.GraphicsLayout.clear>`
""" """
def __init__(self, parent=None, show=False, size=None, title=None, **kargs): def __init__(self, parent=None, show=False, size=None, title=None, **kargs):
mkQApp()
GraphicsView.__init__(self, parent) GraphicsView.__init__(self, parent)
self.ci = GraphicsLayout(**kargs) self.ci = GraphicsLayout(**kargs)
for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']: for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']:

View File

@ -122,7 +122,7 @@ if HAVE_OPENGL:
if not self.uploaded: if not self.uploaded:
self.uploadTexture() self.uploadTexture()
glViewport(0, 0, self.width(), self.height()) glViewport(0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio())
glEnable(GL_TEXTURE_2D) glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, self.texture) glBindTexture(GL_TEXTURE_2D, self.texture)
glColor4f(1,1,1,1) glColor4f(1,1,1,1)

View File

@ -52,16 +52,23 @@ class ScatterPlotWidget(QtGui.QSplitter):
self.ctrlPanel.addWidget(self.ptree) self.ctrlPanel.addWidget(self.ptree)
self.addWidget(self.plot) self.addWidget(self.plot)
bg = fn.mkColor(getConfigOption('background')) fg = fn.mkColor(getConfigOption('foreground'))
bg.setAlpha(150) fg.setAlpha(150)
self.filterText = TextItem(border=getConfigOption('foreground'), color=bg) self.filterText = TextItem(border=getConfigOption('foreground'), color=fg)
self.filterText.setPos(60,20) self.filterText.setPos(60,20)
self.filterText.setParentItem(self.plot.plotItem) self.filterText.setParentItem(self.plot.plotItem)
self.data = None self.data = None
self.indices = None
self.mouseOverField = None self.mouseOverField = None
self.scatterPlot = None self.scatterPlot = None
self.selectionScatter = None
self.selectedIndices = []
self.style = dict(pen=None, symbol='o') self.style = dict(pen=None, symbol='o')
self._visibleXY = None # currently plotted points
self._visibleData = None # currently plotted records
self._visibleIndices = None
self._indexMap = None
self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged) self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged)
self.filter.sigFilterChanged.connect(self.filterChanged) self.filter.sigFilterChanged.connect(self.filterChanged)
@ -83,16 +90,45 @@ class ScatterPlotWidget(QtGui.QSplitter):
item = self.fieldList.addItem(item) item = self.fieldList.addItem(item)
self.filter.setFields(fields) self.filter.setFields(fields)
self.colorMap.setFields(fields) self.colorMap.setFields(fields)
def setSelectedFields(self, *fields):
self.fieldList.itemSelectionChanged.disconnect(self.fieldSelectionChanged)
try:
self.fieldList.clearSelection()
for f in fields:
i = self.fields.keys().index(f)
item = self.fieldList.item(i)
item.setSelected(True)
finally:
self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged)
self.fieldSelectionChanged()
def setData(self, data): def setData(self, data):
""" """
Set the data to be processed and displayed. Set the data to be processed and displayed.
Argument must be a numpy record array. Argument must be a numpy record array.
""" """
self.data = data self.data = data
self.indices = np.arange(len(data))
self.filtered = None self.filtered = None
self.filteredIndices = None
self.updatePlot() self.updatePlot()
def setSelectedIndices(self, inds):
"""Mark the specified indices as selected.
Must be a sequence of integers that index into the array given in setData().
"""
self.selectedIndices = inds
self.updateSelected()
def setSelectedPoints(self, points):
"""Mark the specified points as selected.
Must be a list of points as generated by the sigScatterPlotClicked signal.
"""
self.setSelectedIndices([pt.originalIndex for pt in points])
def fieldSelectionChanged(self): def fieldSelectionChanged(self):
sel = self.fieldList.selectedItems() sel = self.fieldList.selectedItems()
if len(sel) > 2: if len(sel) > 2:
@ -114,15 +150,16 @@ class ScatterPlotWidget(QtGui.QSplitter):
else: else:
self.filterText.setText('\n'.join(desc)) self.filterText.setText('\n'.join(desc))
self.filterText.setVisible(True) self.filterText.setVisible(True)
def updatePlot(self): def updatePlot(self):
self.plot.clear() self.plot.clear()
if self.data is None: if self.data is None or len(self.data) == 0:
return return
if self.filtered is None: if self.filtered is None:
self.filtered = self.filter.filterData(self.data) mask = self.filter.generateMask(self.data)
self.filtered = self.data[mask]
self.filteredIndices = self.indices[mask]
data = self.filtered data = self.filtered
if len(data) == 0: if len(data) == 0:
return return
@ -177,12 +214,14 @@ class ScatterPlotWidget(QtGui.QSplitter):
## mask out any nan values ## mask out any nan values
mask = np.ones(len(xy[0]), dtype=bool) mask = np.ones(len(xy[0]), dtype=bool)
if xy[0].dtype.kind == 'f': if xy[0].dtype.kind == 'f':
mask &= ~np.isnan(xy[0]) mask &= np.isfinite(xy[0])
if xy[1] is not None and xy[1].dtype.kind == 'f': if xy[1] is not None and xy[1].dtype.kind == 'f':
mask &= ~np.isnan(xy[1]) mask &= np.isfinite(xy[1])
xy[0] = xy[0][mask] xy[0] = xy[0][mask]
style['symbolBrush'] = colors[mask] style['symbolBrush'] = colors[mask]
data = data[mask]
indices = self.filteredIndices[mask]
## Scatter y-values for a histogram-like appearance ## Scatter y-values for a histogram-like appearance
if xy[1] is None: if xy[1] is None:
@ -204,16 +243,44 @@ class ScatterPlotWidget(QtGui.QSplitter):
if smax != 0: if smax != 0:
scatter *= 0.2 / smax scatter *= 0.2 / smax
xy[ax][keymask] += scatter xy[ax][keymask] += scatter
if self.scatterPlot is not None: if self.scatterPlot is not None:
try: try:
self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked) self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked)
except: except:
pass pass
self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data[mask], **style)
self.scatterPlot.sigPointsClicked.connect(self.plotClicked)
self._visibleXY = xy
self._visibleData = data
self._visibleIndices = indices
self._indexMap = None
self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data, **style)
self.scatterPlot.sigPointsClicked.connect(self.plotClicked)
self.updateSelected()
def updateSelected(self):
if self._visibleXY is None:
return
# map from global index to visible index
indMap = self._getIndexMap()
inds = [indMap[i] for i in self.selectedIndices if i in indMap]
x,y = self._visibleXY[0][inds], self._visibleXY[1][inds]
if self.selectionScatter is not None:
self.plot.plotItem.removeItem(self.selectionScatter)
if len(x) == 0:
return
self.selectionScatter = self.plot.plot(x, y, pen=None, symbol='s', symbolSize=12, symbolBrush=None, symbolPen='y')
def _getIndexMap(self):
# mapping from original data index to visible point index
if self._indexMap is None:
self._indexMap = {j:i for i,j in enumerate(self._visibleIndices)}
return self._indexMap
def plotClicked(self, plot, points): def plotClicked(self, plot, points):
# Tag each point with its index into the original dataset
for pt in points:
pt.originalIndex = self._visibleIndices[pt.index()]
self.sigScatterPlotClicked.emit(self, points) self.sigScatterPlotClicked.emit(self, points)

View File

@ -146,7 +146,8 @@ class TableWidget(QtGui.QTableWidget):
i += 1 i += 1
self.setRow(i, [x for x in fn1(row)]) self.setRow(i, [x for x in fn1(row)])
if self._sorting and self.horizontalHeader().sortIndicatorSection() >= self.columnCount(): if (self._sorting and self.horizontalHeadersSet and
self.horizontalHeader().sortIndicatorSection() >= self.columnCount()):
self.sortByColumn(0, QtCore.Qt.AscendingOrder) self.sortByColumn(0, QtCore.Qt.AscendingOrder)
def setEditable(self, editable=True): def setEditable(self, editable=True):
@ -216,6 +217,8 @@ class TableWidget(QtGui.QTableWidget):
return self.iterate, list(map(asUnicode, data.dtype.names)) return self.iterate, list(map(asUnicode, data.dtype.names))
elif data is None: elif data is None:
return (None,None) return (None,None)
elif np.isscalar(data):
return self.iterateScalar, None
else: else:
msg = "Don't know how to iterate over data type: {!s}".format(type(data)) msg = "Don't know how to iterate over data type: {!s}".format(type(data))
raise TypeError(msg) raise TypeError(msg)
@ -230,6 +233,9 @@ class TableWidget(QtGui.QTableWidget):
for x in data: for x in data:
yield x yield x
def iterateScalar(self, data):
yield data
def appendRow(self, data): def appendRow(self, data):
self.appendData([data]) self.appendData([data])

View File

@ -1,21 +0,0 @@
## just import everything from sub-modules
#import os
#d = os.path.split(__file__)[0]
#files = []
#for f in os.listdir(d):
#if os.path.isdir(os.path.join(d, f)):
#files.append(f)
#elif f[-3:] == '.py' and f != '__init__.py':
#files.append(f[:-3])
#for modName in files:
#mod = __import__(modName, globals(), locals(), fromlist=['*'])
#if hasattr(mod, '__all__'):
#names = mod.__all__
#else:
#names = [n for n in dir(mod) if n[0] != '_']
#for k in names:
#print modName, k
#globals()[k] = getattr(mod, k)