Merge branch 'develop' into pyside2
This commit is contained in:
commit
66dcfc7c67
@ -7,6 +7,9 @@ pyqtgraph-0.11.0 (in development)
|
||||
To mimic the old behavior, use ArrowItem.rotate() instead of the `angle` argument.
|
||||
- Deprecated graphicsWindow classes; these have been unnecessary for many years because
|
||||
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
|
||||
|
64
README.md
64
README.md
@ -6,80 +6,46 @@ PyQtGraph
|
||||
|
||||
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>
|
||||
|
||||
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
|
||||
------------
|
||||
|
||||
* PyQt 4.7+, PySide, or PyQt5
|
||||
* python 2.6, 2.7, or 3.x
|
||||
* python 2.7, or 3.x
|
||||
* NumPy
|
||||
* For 3D graphics: pyopengl and qt-opengl
|
||||
* Known to run on Windows, Linux, and Mac.
|
||||
|
||||
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
|
||||
--------------------
|
||||
|
||||
* From pypi:
|
||||
`pip install pyqtgraph`
|
||||
* To use with a specific project, simply copy the pyqtgraph subdirectory
|
||||
anywhere that is importable from your project. PyQtGraph may also be
|
||||
used as a git subtree by cloning the git-core repository from github.
|
||||
anywhere that is importable from your project.
|
||||
* To install system-wide from source distribution:
|
||||
`$ python setup.py install`
|
||||
* 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
|
||||
-------------
|
||||
|
||||
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.
|
||||
|
@ -12,7 +12,7 @@ import numpy as np
|
||||
# Enable antialiasing for prettier plots
|
||||
pg.setConfigOptions(antialias=True)
|
||||
|
||||
w = pg.GraphicsWindow()
|
||||
w = pg.GraphicsLayoutWidget(show=True)
|
||||
w.setWindowTitle('pyqtgraph example: CustomGraphItem')
|
||||
v = w.addViewBox()
|
||||
v.setAspectLocked()
|
||||
|
@ -11,15 +11,29 @@ from pyqtgraph.Qt import QtCore, QtGui
|
||||
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([])
|
||||
d = {
|
||||
'list1': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"],
|
||||
'dict1': {
|
||||
'a list': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"],
|
||||
'a dict': {
|
||||
'x': 1,
|
||||
'y': 2,
|
||||
'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)
|
||||
|
52
examples/DiffTreeWidget.py
Normal file
52
examples/DiffTreeWidget.py
Normal 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_()
|
@ -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
|
||||
levels = (-0.08, 0.08)
|
||||
tex1 = pg.makeRGBA(data[shape[0]/2], levels=levels)[0] # yz 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
|
||||
tex1 = pg.makeRGBA(data[shape[0]//2], levels=levels)[0] # yz 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
|
||||
#tex1[:,:,3] = 128
|
||||
#tex2[:,:,3] = 128
|
||||
#tex3[:,:,3] = 128
|
||||
|
@ -13,7 +13,7 @@ import numpy as np
|
||||
# Enable antialiasing for prettier plots
|
||||
pg.setConfigOptions(antialias=True)
|
||||
|
||||
w = pg.GraphicsWindow()
|
||||
w = pg.GraphicsLayoutWidget(show=True)
|
||||
w.setWindowTitle('pyqtgraph example: GraphItem')
|
||||
v = w.addViewBox()
|
||||
v.setAspectLocked()
|
||||
|
@ -10,7 +10,7 @@ import pyqtgraph as pg
|
||||
|
||||
|
||||
app = QtGui.QApplication([])
|
||||
win = pg.GraphicsWindow(title="Plotting items examples")
|
||||
win = pg.GraphicsLayoutWidget(show=True, title="Plotting items examples")
|
||||
win.resize(1000,600)
|
||||
|
||||
# Enable antialiasing for prettier plots
|
||||
|
@ -12,7 +12,7 @@ import pyqtgraph as pg
|
||||
|
||||
app = QtGui.QApplication([])
|
||||
|
||||
win = pg.GraphicsWindow(title="Basic plotting examples")
|
||||
win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples")
|
||||
win.resize(1000,600)
|
||||
win.setWindowTitle('pyqtgraph example: LogPlotTest')
|
||||
|
||||
|
@ -12,32 +12,27 @@ from pyqtgraph.Qt import QtGui, QtCore
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.ptime import time
|
||||
#QtGui.QApplication.setGraphicsSystem('raster')
|
||||
app = QtGui.QApplication([])
|
||||
#mw = QtGui.QMainWindow()
|
||||
#mw.resize(800,800)
|
||||
|
||||
p = pg.plot()
|
||||
p.setWindowTitle('pyqtgraph example: MultiPlotSpeedTest')
|
||||
#p.setRange(QtCore.QRectF(0, -10, 5000, 20))
|
||||
p.setLabel('bottom', 'Index', units='B')
|
||||
plot = pg.plot()
|
||||
plot.setWindowTitle('pyqtgraph example: MultiPlotSpeedTest')
|
||||
plot.setLabel('bottom', 'Index', units='B')
|
||||
|
||||
nPlots = 100
|
||||
nSamples = 500
|
||||
#curves = [p.plot(pen=(i,nPlots*1.3)) for i in range(nPlots)]
|
||||
curves = []
|
||||
for i in range(nPlots):
|
||||
c = pg.PlotCurveItem(pen=(i,nPlots*1.3))
|
||||
p.addItem(c)
|
||||
c.setPos(0,i*6)
|
||||
curves.append(c)
|
||||
for idx in range(nPlots):
|
||||
curve = pg.PlotCurveItem(pen=(idx,nPlots*1.3))
|
||||
plot.addItem(curve)
|
||||
curve.setPos(0,idx*6)
|
||||
curves.append(curve)
|
||||
|
||||
p.setYRange(0, nPlots*6)
|
||||
p.setXRange(0, nSamples)
|
||||
p.resize(600,900)
|
||||
plot.setYRange(0, nPlots*6)
|
||||
plot.setXRange(0, nSamples)
|
||||
plot.resize(600,900)
|
||||
|
||||
rgn = pg.LinearRegionItem([nSamples/5.,nSamples/3.])
|
||||
p.addItem(rgn)
|
||||
plot.addItem(rgn)
|
||||
|
||||
|
||||
data = np.random.normal(size=(nPlots*23,nSamples))
|
||||
@ -46,13 +41,12 @@ lastTime = time()
|
||||
fps = None
|
||||
count = 0
|
||||
def update():
|
||||
global curve, data, ptr, p, lastTime, fps, nPlots, count
|
||||
global curve, data, ptr, plot, lastTime, fps, nPlots, count
|
||||
count += 1
|
||||
#print "---------", count
|
||||
|
||||
for i in range(nPlots):
|
||||
curves[i].setData(data[(ptr+i)%data.shape[0]])
|
||||
|
||||
#print " setData done."
|
||||
|
||||
ptr += nPlots
|
||||
now = time()
|
||||
dt = now - lastTime
|
||||
@ -62,13 +56,11 @@ def update():
|
||||
else:
|
||||
s = np.clip(dt*3., 0, 1)
|
||||
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
|
||||
timer = QtCore.QTimer()
|
||||
timer.timeout.connect(update)
|
||||
timer.start(0)
|
||||
|
||||
|
||||
|
||||
## Start Qt event loop unless running in interactive mode.
|
||||
if __name__ == '__main__':
|
||||
|
@ -9,7 +9,7 @@ import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
import numpy as np
|
||||
|
||||
win = pg.GraphicsWindow()
|
||||
win = pg.GraphicsLayoutWidget(show=True)
|
||||
win.setWindowTitle('pyqtgraph example: PanningPlot')
|
||||
|
||||
plt = win.addPlot()
|
||||
|
@ -16,7 +16,7 @@ app = QtGui.QApplication([])
|
||||
#mw = QtGui.QMainWindow()
|
||||
#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.setWindowTitle('pyqtgraph example: PlotAutoRange')
|
||||
|
||||
|
@ -17,7 +17,7 @@ app = QtGui.QApplication([])
|
||||
#mw = QtGui.QMainWindow()
|
||||
#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.setWindowTitle('pyqtgraph example: Plotting')
|
||||
|
||||
|
@ -33,7 +33,7 @@ arr[8:13, 44:46] = 10
|
||||
|
||||
## create GUI
|
||||
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')
|
||||
|
||||
text = """Data Selection From Image.<br>\n
|
||||
|
@ -13,7 +13,7 @@ pg.setConfigOptions(imageAxisOrder='row-major')
|
||||
## create GUI
|
||||
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.invertY(True) ## Images usually have their Y-axis pointing downward
|
||||
v.setAspectLocked(True)
|
||||
|
@ -9,7 +9,7 @@ from pyqtgraph.Qt import QtCore, QtGui
|
||||
import numpy as np
|
||||
|
||||
pg.mkQApp()
|
||||
win = pg.GraphicsWindow()
|
||||
win = pg.GraphicsLayoutWidget(show=True)
|
||||
win.setWindowTitle('pyqtgraph example: ScaleBar')
|
||||
|
||||
vb = win.addViewBox()
|
||||
|
@ -28,7 +28,7 @@ pg.mkQApp()
|
||||
# Make up some tabular data with structure
|
||||
data = np.empty(1000, dtype=[('x_pos', float), ('y_pos', float),
|
||||
('count', int), ('amplitude', float),
|
||||
('decay', float), ('type', 'S10')])
|
||||
('decay', float), ('type', 'U10')])
|
||||
strings = ['Type-A', 'Type-B', 'Type-C', 'Type-D', 'Type-E']
|
||||
typeInds = np.random.randint(5, size=1000)
|
||||
data['type'] = np.array(strings)[typeInds]
|
||||
|
@ -11,7 +11,7 @@ from pyqtgraph.Qt import QtGui, QtCore
|
||||
import pyqtgraph as pg
|
||||
|
||||
app = QtGui.QApplication([])
|
||||
win = pg.GraphicsWindow(title="Scatter Plot Symbols")
|
||||
win = pg.GraphicsLayoutWidget(show=True, title="Scatter Plot Symbols")
|
||||
win.resize(1000,600)
|
||||
|
||||
pg.setConfigOptions(antialias=True)
|
||||
|
@ -16,7 +16,7 @@ x = np.arange(1000, dtype=float)
|
||||
y = np.random.normal(size=1000)
|
||||
y += 5 * np.sin(x/100)
|
||||
|
||||
win = pg.GraphicsWindow()
|
||||
win = pg.GraphicsLayoutWidget(show=True)
|
||||
win.setWindowTitle('pyqtgraph example: ____')
|
||||
win.resize(1000, 800)
|
||||
win.ci.setBorder((50, 50, 100))
|
||||
|
@ -28,6 +28,7 @@ class ExampleLoader(QtGui.QMainWindow):
|
||||
self.cw = QtGui.QWidget()
|
||||
self.setCentralWidget(self.cw)
|
||||
self.ui.setupUi(self.cw)
|
||||
self.setWindowTitle("PyQtGraph Examples")
|
||||
|
||||
self.codeBtn = QtGui.QPushButton('Run Edited Code')
|
||||
self.codeLayout = QtGui.QGridLayout()
|
||||
|
@ -14,7 +14,7 @@ import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
import numpy as np
|
||||
|
||||
win = pg.GraphicsWindow()
|
||||
win = pg.GraphicsLayoutWidget(show=True)
|
||||
win.setWindowTitle('pyqtgraph example: context menu')
|
||||
|
||||
|
||||
|
@ -13,7 +13,7 @@ from pyqtgraph.Point import Point
|
||||
|
||||
#generate layout
|
||||
app = QtGui.QApplication([])
|
||||
win = pg.GraphicsWindow()
|
||||
win = pg.GraphicsLayoutWidget(show=True)
|
||||
win.setWindowTitle('pyqtgraph example: crosshair')
|
||||
label = pg.LabelItem(justify='right')
|
||||
win.addItem(label)
|
||||
|
@ -4,6 +4,7 @@ Displays an interactive Koch fractal
|
||||
"""
|
||||
import initExample ## Add path to library (just for examples; you do not need this)
|
||||
|
||||
from functools import reduce
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
import numpy as np
|
||||
@ -111,12 +112,4 @@ if __name__ == '__main__':
|
||||
import sys
|
||||
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
|
||||
QtGui.QApplication.instance().exec_()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -8,7 +8,7 @@ import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
import numpy as np
|
||||
|
||||
win = pg.GraphicsWindow()
|
||||
win = pg.GraphicsLayoutWidget(show=True)
|
||||
win.resize(800,350)
|
||||
win.setWindowTitle('pyqtgraph example: Histogram')
|
||||
plt1 = win.addPlot()
|
||||
|
@ -17,10 +17,10 @@ app = QtGui.QApplication([])
|
||||
frames = 200
|
||||
data = np.random.normal(size=(frames,30,30), loc=0, scale=100)
|
||||
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
|
||||
|
||||
win = pg.GraphicsWindow()
|
||||
win = pg.GraphicsLayoutWidget(show=True)
|
||||
win.setWindowTitle('pyqtgraph example: Isocurve')
|
||||
vb = win.addViewBox()
|
||||
img = pg.ImageItem(data[0])
|
||||
|
@ -20,7 +20,7 @@ app = QtGui.QApplication([])
|
||||
x = np.linspace(-50, 50, 1000)
|
||||
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.addLabel("Linked Views", colspan=2)
|
||||
|
@ -11,7 +11,7 @@ import pyqtgraph as pg
|
||||
|
||||
app = QtGui.QApplication([])
|
||||
|
||||
w = pg.GraphicsWindow()
|
||||
w = pg.GraphicsLayoutWidget(show=True)
|
||||
w.setWindowTitle('pyqtgraph example: logAxis')
|
||||
p1 = w.addPlot(0,0, title="X Semilog")
|
||||
p2 = w.addPlot(1,0, title="Y Semilog")
|
||||
|
@ -17,7 +17,7 @@ from pyqtgraph import Point
|
||||
|
||||
app = pg.QtGui.QApplication([])
|
||||
|
||||
w = pg.GraphicsWindow(border=0.5)
|
||||
w = pg.GraphicsLayoutWidget(show=True, border=0.5)
|
||||
w.resize(1000, 900)
|
||||
w.show()
|
||||
|
||||
|
@ -8,7 +8,7 @@ import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
import numpy as np
|
||||
|
||||
win = pg.GraphicsWindow()
|
||||
win = pg.GraphicsLayoutWidget(show=True)
|
||||
win.setWindowTitle('pyqtgraph example: Scrolling Plots')
|
||||
|
||||
|
||||
|
@ -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
|
||||
* Eats mouseMove events that occur too soon after a mouse press.
|
||||
* 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:
|
||||
|
||||
|
@ -105,7 +105,13 @@ class Point(QtCore.QPointF):
|
||||
|
||||
def length(self):
|
||||
"""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):
|
||||
"""Returns a vector in the same direction with unit length."""
|
||||
|
@ -319,3 +319,12 @@ m = re.match(r'(\d+)\.(\d+).*', QtVersion)
|
||||
if m is not None and list(map(int, m.groups())) < versionReq:
|
||||
print(list(map(int, m.groups())))
|
||||
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
|
||||
|
@ -113,7 +113,7 @@ class SRTTransform3D(Transform3D):
|
||||
|
||||
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,
|
||||
otherwise the conversion will most likely fail.
|
||||
"""
|
||||
|
@ -10,7 +10,7 @@ __version__ = '0.10.0'
|
||||
|
||||
## 'Qt' is a local module; it is intended mainly to cover up the differences
|
||||
## 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)
|
||||
#if QtGui.QApplication.instance() is None:
|
||||
@ -258,6 +258,7 @@ from .widgets.VerticalLabel import *
|
||||
from .widgets.FeedbackButton import *
|
||||
from .widgets.ColorButton import *
|
||||
from .widgets.DataTreeWidget import *
|
||||
from .widgets.DiffTreeWidget import *
|
||||
from .widgets.GraphicsView import *
|
||||
from .widgets.LayoutWidget import *
|
||||
from .widgets.TableWidget import *
|
||||
@ -466,14 +467,3 @@ def stack(*args, **kwds):
|
||||
except NameError:
|
||||
consoles = [c]
|
||||
return c
|
||||
|
||||
|
||||
def mkQApp():
|
||||
global QAPP
|
||||
inst = QtGui.QApplication.instance()
|
||||
if inst is None:
|
||||
QAPP = QtGui.QApplication([])
|
||||
else:
|
||||
QAPP = inst
|
||||
return QAPP
|
||||
|
||||
|
@ -361,7 +361,6 @@ class ConsoleWidget(QtGui.QWidget):
|
||||
for index, line in enumerate(traceback.extract_stack(frame)):
|
||||
# extract_stack return value changed in python 3.5
|
||||
if 'FrameSummary' in str(type(line)):
|
||||
print(dir(line))
|
||||
line = (line.filename, line.lineno, line.name, line._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)):
|
||||
# extract_stack return value changed in python 3.5
|
||||
if 'FrameSummary' in str(type(line)):
|
||||
print(dir(line))
|
||||
line = (line.filename, line.lineno, line.name, line._line)
|
||||
|
||||
self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line)
|
||||
|
@ -510,7 +510,7 @@ class Profiler(object):
|
||||
try:
|
||||
caller_object_type = type(caller_frame.f_locals["self"])
|
||||
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
|
||||
qualifier = caller_object_type.__name__
|
||||
func_qualname = qualifier + "." + caller_frame.f_code.co_name
|
||||
|
@ -23,7 +23,8 @@ class SVGExporter(Exporter):
|
||||
#{'name': 'height', 'type': 'float', 'value': tr.height(), 'limits': (0, None)},
|
||||
#{'name': 'viewbox clipping', '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('height').sigValueChanged.connect(self.heightChanged)
|
||||
@ -49,7 +50,8 @@ class SVGExporter(Exporter):
|
||||
## Qt's SVG generator is not complete. (notably, it lacks clipping)
|
||||
## Instead, we will use Qt to generate SVG for each item independently,
|
||||
## 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:
|
||||
return xml.encode('UTF-8')
|
||||
@ -69,10 +71,10 @@ xmlHeader = """\
|
||||
<desc>Generated with Qt and pyqtgraph</desc>
|
||||
"""
|
||||
|
||||
def generateSvg(item):
|
||||
def generateSvg(item, options={}):
|
||||
global xmlHeader
|
||||
try:
|
||||
node, defs = _generateItemSvg(item)
|
||||
node, defs = _generateItemSvg(item, options=options)
|
||||
finally:
|
||||
## reset export mode for all items in the tree
|
||||
if isinstance(item, QtGui.QGraphicsScene):
|
||||
@ -94,7 +96,7 @@ def generateSvg(item):
|
||||
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
|
||||
## and SVG in general.
|
||||
## 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)
|
||||
svg = QtSvg.QSvgGenerator()
|
||||
svg.setOutputDevice(buf)
|
||||
dpi = QtGui.QDesktopWidget().physicalDpiX()
|
||||
dpi = QtGui.QDesktopWidget().logicalDpiX()
|
||||
svg.setResolution(dpi)
|
||||
|
||||
p = QtGui.QPainter()
|
||||
@ -209,18 +211,8 @@ def _generateItemSvg(item, nodes=None, root=None):
|
||||
|
||||
## Get rid of group transformation matrices by applying
|
||||
## transformation to inner coordinates
|
||||
correctCoordinates(g1, defs, item)
|
||||
correctCoordinates(g1, defs, item, options)
|
||||
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
|
||||
baseName = item.__class__.__name__
|
||||
@ -239,15 +231,10 @@ def _generateItemSvg(item, nodes=None, root=None):
|
||||
## See if this item clips its children
|
||||
if int(item.flags() & item.ItemClipsChildrenToShape) > 0:
|
||||
## 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()))
|
||||
item.scene().addItem(path)
|
||||
try:
|
||||
#pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0]
|
||||
pathNode = _generateItemSvg(path, root=root)[0].getElementsByTagName('path')[0]
|
||||
pathNode = _generateItemSvg(path, root=root, options=options)[0].getElementsByTagName('path')[0]
|
||||
# assume <defs> for this path is empty.. possibly problematic.
|
||||
finally:
|
||||
item.scene().removeItem(path)
|
||||
@ -267,17 +254,18 @@ def _generateItemSvg(item, nodes=None, root=None):
|
||||
## Add all child items as sub-elements.
|
||||
childs.sort(key=lambda c: c.zValue())
|
||||
for ch in childs:
|
||||
csvg = _generateItemSvg(ch, nodes, root)
|
||||
csvg = _generateItemSvg(ch, nodes, root, options=options)
|
||||
if csvg is None:
|
||||
continue
|
||||
cg, cdefs = csvg
|
||||
childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now)
|
||||
defs.extend(cdefs)
|
||||
|
||||
|
||||
profiler('children')
|
||||
return g1, defs
|
||||
|
||||
def correctCoordinates(node, defs, item):
|
||||
|
||||
def correctCoordinates(node, defs, item, options):
|
||||
# TODO: correct gradient coordinates inside defs
|
||||
|
||||
## Remove transformation matrices from <g> tags by applying matrix to coordinates inside.
|
||||
@ -344,6 +332,10 @@ def correctCoordinates(node, defs, item):
|
||||
t = ''
|
||||
nc = fn.transformCoordinates(tr, np.array([[float(x),float(y)]]), transpose=True)
|
||||
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)
|
||||
elif ch.tagName == 'text':
|
||||
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]))
|
||||
|
||||
## 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'))
|
||||
s = fn.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True)
|
||||
w = ((s[0]-s[1])**2).sum()**0.5
|
||||
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:
|
||||
grp.removeAttribute('transform')
|
||||
|
||||
|
@ -14,7 +14,8 @@ import sys, struct
|
||||
from .python2_3 import asUnicode, basestring
|
||||
from .Qt import QtGui, QtCore, QT_LIB
|
||||
from . import getConfigOption, setConfigOptions
|
||||
from . import debug
|
||||
from . import debug, reload
|
||||
from .reload import getPreviousVersion
|
||||
from .metaarray import MetaArray
|
||||
|
||||
|
||||
@ -2172,7 +2173,7 @@ def isosurface(data, level):
|
||||
## compute lookup table of index: vertexes mapping
|
||||
faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte)
|
||||
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))
|
||||
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.append(n)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
@ -379,6 +379,10 @@ class ImageItem(GraphicsObject):
|
||||
image = fn.downsample(self.image, xds, axis=axes[0])
|
||||
image = fn.downsample(image, yds, axis=axes[1])
|
||||
self._lastDownsample = (xds, yds)
|
||||
|
||||
# Check if downsampling reduced the image size to zero due to inf values.
|
||||
if image.size == 0:
|
||||
return
|
||||
else:
|
||||
image = self.image
|
||||
|
||||
@ -465,11 +469,11 @@ class ImageItem(GraphicsObject):
|
||||
|
||||
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
|
||||
if step == 'auto':
|
||||
step = (int(np.ceil(self.image.shape[0] / targetImageSize)),
|
||||
int(np.ceil(self.image.shape[1] / targetImageSize)))
|
||||
step = (max(1, int(np.ceil(self.image.shape[0] / targetImageSize))),
|
||||
max(1, int(np.ceil(self.image.shape[1] / targetImageSize))))
|
||||
if np.isscalar(step):
|
||||
step = (step, step)
|
||||
stepData = self.image[::step[0], ::step[1]]
|
||||
|
@ -110,7 +110,8 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
||||
#print("-------")
|
||||
for sample, label in self.items:
|
||||
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
|
||||
self.setGeometry(0, 0, width+25, height)
|
||||
|
@ -649,6 +649,9 @@ class ScatterPlotItem(GraphicsObject):
|
||||
d = d[mask]
|
||||
d2 = d2[mask]
|
||||
|
||||
if d.size == 0:
|
||||
return (None, None)
|
||||
|
||||
if frac >= 1.0:
|
||||
self.bounds[ax] = (np.nanmin(d) - self._maxSpotWidth*0.7072, np.nanmax(d) + self._maxSpotWidth*0.7072)
|
||||
return self.bounds[ax]
|
||||
@ -701,16 +704,12 @@ class ScatterPlotItem(GraphicsObject):
|
||||
GraphicsObject.setExportMode(self, *args, **kwds)
|
||||
self.invalidate()
|
||||
|
||||
|
||||
def mapPointsToDevice(self, pts):
|
||||
# Map point locations to device
|
||||
tr = self.deviceTransform()
|
||||
if tr is 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 -= self.data['width']
|
||||
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
|
||||
return mask
|
||||
|
||||
|
||||
@debug.warnOnException ## raising an exception here causes crash
|
||||
def paint(self, p, *args):
|
||||
cmode = self.opts.get('compositionMode', None)
|
||||
@ -758,8 +756,6 @@ class ScatterPlotItem(GraphicsObject):
|
||||
|
||||
# Cull points that are outside view
|
||||
viewMask = self.getViewMask(pts)
|
||||
#pts = pts[:,mask]
|
||||
#data = self.data[mask]
|
||||
|
||||
if self.opts['useCache'] and self._exportOpts is False:
|
||||
# Draw symbols from pre-rendered atlas
|
||||
@ -804,9 +800,9 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.picture.play(p)
|
||||
|
||||
def points(self):
|
||||
for rec in self.data:
|
||||
for i,rec in enumerate(self.data):
|
||||
if rec['item'] is None:
|
||||
rec['item'] = SpotItem(rec, self)
|
||||
rec['item'] = SpotItem(rec, self, i)
|
||||
return self.data['item']
|
||||
|
||||
def pointsAt(self, pos):
|
||||
@ -854,18 +850,26 @@ class SpotItem(object):
|
||||
by connecting to the ScatterPlotItem's click signals.
|
||||
"""
|
||||
|
||||
def __init__(self, data, plot):
|
||||
#GraphicsItem.__init__(self, register=False)
|
||||
def __init__(self, data, plot, index):
|
||||
self._data = data
|
||||
self._plot = plot
|
||||
#self.setParentItem(plot)
|
||||
#self.setPos(QtCore.QPointF(data['x'], data['y']))
|
||||
#self.updateItem()
|
||||
self._index = index
|
||||
# SpotItems are kept in plot.data["items"] numpy object array which
|
||||
# does not support cyclic garbage collection (numpy issue 6581).
|
||||
# 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):
|
||||
"""Return the user data associated with this spot."""
|
||||
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):
|
||||
"""Return the size of this spot.
|
||||
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._plot.updateSpots(self._data.reshape(1))
|
||||
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)
|
||||
|
@ -6,17 +6,11 @@ it is possible to place any widget into its own window by simply calling its
|
||||
show() method.
|
||||
"""
|
||||
|
||||
from .Qt import QtCore, QtGui
|
||||
from .Qt import QtCore, QtGui, mkQApp
|
||||
from .widgets.PlotWidget import *
|
||||
from .imageview import *
|
||||
from .widgets.GraphicsLayoutWidget import GraphicsLayoutWidget
|
||||
from .widgets.GraphicsView import GraphicsView
|
||||
QAPP = None
|
||||
|
||||
def mkQApp():
|
||||
if QtGui.QApplication.instance() is None:
|
||||
global QAPP
|
||||
QAPP = QtGui.QApplication([])
|
||||
|
||||
|
||||
class GraphicsWindow(GraphicsLayoutWidget):
|
||||
|
@ -637,8 +637,12 @@ class ImageView(QtGui.QWidget):
|
||||
|
||||
cax = self.axes['c']
|
||||
if cax is None:
|
||||
if data.size == 0:
|
||||
return [(0, 0)]
|
||||
return [(float(nanmin(data)), float(nanmax(data)))]
|
||||
else:
|
||||
if data.size == 0:
|
||||
return [(0, 0)] * data.shape[-1]
|
||||
return [(float(nanmin(data.take(i, axis=cax))),
|
||||
float(nanmax(data.take(i, axis=cax)))) for i in range(data.shape[-1])]
|
||||
|
||||
|
@ -195,6 +195,8 @@ class Parallelize(object):
|
||||
finally:
|
||||
if self.showProgress:
|
||||
self.progressDlg.__exit__(None, None, None)
|
||||
for ch in self.childs:
|
||||
ch.join()
|
||||
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)))
|
||||
for code in self.exitCodes:
|
||||
|
@ -165,6 +165,7 @@ class Process(RemoteEventHandler):
|
||||
if timeout is not None and time.time() - start > timeout:
|
||||
raise Exception('Timed out waiting for remote process to end.')
|
||||
time.sleep(0.05)
|
||||
self.conn.close()
|
||||
self.debugMsg('Child process exited. (%d)' % self.proc.returncode)
|
||||
|
||||
def debugMsg(self, msg, *args):
|
||||
@ -341,6 +342,7 @@ class ForkedProcess(RemoteEventHandler):
|
||||
except OSError: ## probably remote process has already quit
|
||||
pass
|
||||
|
||||
self.conn.close() # don't leak file handles!
|
||||
self.hasJoined = True
|
||||
|
||||
def kill(self):
|
||||
|
@ -16,9 +16,13 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
- Axis/grid display
|
||||
- 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
|
||||
|
||||
if ShareWidget is None:
|
||||
@ -37,6 +41,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
'azimuth': 45, ## camera's azimuthal angle in degrees
|
||||
## (rotation around z-axis 0 points along x-axis)
|
||||
'viewport': None, ## glViewport params; None == whole widget
|
||||
'devicePixelRatio': devicePixelRatio,
|
||||
}
|
||||
self.setBackgroundColor('k')
|
||||
self.items = []
|
||||
@ -79,10 +84,21 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
|
||||
def getViewport(self):
|
||||
vp = self.opts['viewport']
|
||||
dpr = self.devicePixelRatio()
|
||||
if vp is None:
|
||||
return (0, 0, self.width(), self.height())
|
||||
return (0, 0, int(self.width() * dpr), int(self.height() * dpr))
|
||||
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):
|
||||
pass
|
||||
@ -99,7 +115,8 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
def projectionMatrix(self, region=None):
|
||||
# Xw = (Xnd + 1) * width/2 + X
|
||||
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()
|
||||
dist = self.opts['distance']
|
||||
|
@ -485,7 +485,7 @@ class MeshData(object):
|
||||
if isinstance(radius, int):
|
||||
radius = [radius, radius] # convert to list
|
||||
## 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
|
||||
verts[...,2] = np.linspace(0, length, num=rows+1, endpoint=True).reshape(rows+1, 1) # z
|
||||
if offset:
|
||||
|
@ -10,10 +10,10 @@ class GLGridItem(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)
|
||||
self.setGLOptions(glOptions)
|
||||
self.antialias = antialias
|
||||
@ -21,6 +21,7 @@ class GLGridItem(GLGraphicsItem):
|
||||
size = QtGui.QVector3D(20,20,1)
|
||||
self.setSize(size=size)
|
||||
self.setSpacing(1, 1, 1)
|
||||
self.color = color
|
||||
|
||||
def setSize(self, x=None, y=None, z=None, size=None):
|
||||
"""
|
||||
@ -66,8 +67,8 @@ class GLGridItem(GLGraphicsItem):
|
||||
x,y,z = self.size()
|
||||
xs,ys,zs = self.spacing()
|
||||
xvals = np.arange(-x/2., x/2. + xs*0.001, xs)
|
||||
yvals = np.arange(-y/2., y/2. + ys*0.001, ys)
|
||||
glColor4f(1, 1, 1, .3)
|
||||
yvals = np.arange(-y/2., y/2. + ys*0.001, ys)
|
||||
glColor4f(*self.color)
|
||||
for x in xvals:
|
||||
glVertex3f(x, yvals[0], 0)
|
||||
glVertex3f(x, yvals[-1], 0)
|
||||
|
@ -152,7 +152,9 @@ class GLScatterPlotItem(GLGraphicsItem):
|
||||
glDisableClientState(GL_VERTEX_ARRAY)
|
||||
glDisableClientState(GL_COLOR_ARRAY)
|
||||
#posVBO.unbind()
|
||||
|
||||
##fixes #145
|
||||
glDisable( GL_TEXTURE_2D )
|
||||
|
||||
#for i in range(len(self.pos)):
|
||||
#pos = self.pos[i]
|
||||
|
||||
|
@ -105,6 +105,7 @@ class WidgetParameterItem(ParameterItem):
|
||||
if t == 'int':
|
||||
defs['int'] = True
|
||||
defs['minStep'] = 1.0
|
||||
defs['format'] = '{value:d}'
|
||||
for k in defs:
|
||||
if k in opts:
|
||||
defs[k] = opts[k]
|
||||
|
@ -21,13 +21,17 @@ Does NOT:
|
||||
print module.someObject
|
||||
"""
|
||||
|
||||
|
||||
import inspect, os, sys, gc, traceback
|
||||
try:
|
||||
import __builtin__ as builtins
|
||||
except ImportError:
|
||||
import builtins
|
||||
from __future__ import print_function
|
||||
import inspect, os, sys, gc, traceback, types
|
||||
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):
|
||||
"""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
|
||||
oldDict = module.__dict__.copy()
|
||||
builtins.reload(module)
|
||||
orig_reload(module)
|
||||
newDict = module.__dict__
|
||||
|
||||
## 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:
|
||||
print(" Updating class %s.%s (0x%x -> 0x%x)" % (module.__name__, k, id(old), id(new)))
|
||||
updateClass(old, new, debug)
|
||||
|
||||
# don't put this inside updateClass because it is reentrant.
|
||||
new.__previous_reload_version__ = old
|
||||
|
||||
elif inspect.isfunction(old):
|
||||
depth = updateFunction(old, new, debug)
|
||||
if debug:
|
||||
@ -127,6 +133,9 @@ def updateFunction(old, new, debug, depth=0, visited=None):
|
||||
|
||||
old.__code__ = new.__code__
|
||||
old.__defaults__ = new.__defaults__
|
||||
if hasattr(old, '__kwdefaults'):
|
||||
old.__kwdefaults__ = new.__kwdefaults__
|
||||
old.__doc__ = new.__doc__
|
||||
|
||||
if visited is None:
|
||||
visited = []
|
||||
@ -151,8 +160,9 @@ def updateFunction(old, new, debug, depth=0, visited=None):
|
||||
## For classes:
|
||||
## 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
|
||||
def updateClass(old, new, debug):
|
||||
|
||||
|
||||
def updateClass(old, new, debug):
|
||||
## Track town all instances and subclasses of old
|
||||
refs = gc.get_referrers(old)
|
||||
for ref in refs:
|
||||
@ -174,13 +184,20 @@ def updateClass(old, new, debug):
|
||||
## This seems to work. Is there any reason not to?
|
||||
## Note that every time we reload, the class hierarchy becomes more complex.
|
||||
## (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:
|
||||
print(" Changed superclass for %s" % safeStr(ref))
|
||||
#else:
|
||||
#if debug:
|
||||
#print " Ignoring reference", type(ref)
|
||||
except:
|
||||
except Exception:
|
||||
print("Error updating reference (%s) for class change (%s -> %s)" % (safeStr(ref), safeStr(old), safeStr(new)))
|
||||
raise
|
||||
|
||||
@ -189,7 +206,8 @@ def updateClass(old, new, debug):
|
||||
## but it fixes a few specific cases (pyqt signals, for one)
|
||||
for attr in dir(old):
|
||||
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:
|
||||
na = getattr(new, attr)
|
||||
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)
|
||||
continue
|
||||
|
||||
if hasattr(oa, 'im_func') and hasattr(na, 'im_func') and oa.__func__ is not na.__func__:
|
||||
depth = updateFunction(oa.__func__, na.__func__, debug)
|
||||
#oa.im_class = new ## bind old method to new class ## not allowed
|
||||
ofunc = getattr(oa, '__func__', oa) # in py2 we have to get the __func__ from unbound method,
|
||||
nfunc = getattr(na, '__func__', na) # in py3 the attribute IS the function
|
||||
|
||||
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:
|
||||
extra = ""
|
||||
if depth > 0:
|
||||
@ -208,6 +231,8 @@ def updateClass(old, new, debug):
|
||||
|
||||
## And copy in new functions that didn't exist previously
|
||||
for attr in dir(new):
|
||||
if attr == '__previous_reload_version__':
|
||||
continue
|
||||
if not hasattr(old, attr):
|
||||
if debug:
|
||||
print(" Adding missing attribute %s" % attr)
|
||||
@ -223,14 +248,37 @@ def updateClass(old, new, debug):
|
||||
def safeStr(obj):
|
||||
try:
|
||||
s = str(obj)
|
||||
except:
|
||||
except Exception:
|
||||
try:
|
||||
s = repr(obj)
|
||||
except:
|
||||
except Exception:
|
||||
s = "<instance of %s at 0x%x>" % (safeStr(type(obj)), id(obj))
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
116
pyqtgraph/tests/test_reload.py
Normal file
116
pyqtgraph/tests/test_reload.py
Normal 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)
|
||||
|
@ -9,16 +9,22 @@ class BusyCursor(object):
|
||||
with pyqtgraph.BusyCursor():
|
||||
doLongOperation()
|
||||
|
||||
May be nested.
|
||||
May be nested. If called from a non-gui thread, then the cursor will not be affected.
|
||||
"""
|
||||
active = []
|
||||
|
||||
def __enter__(self):
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
BusyCursor.active.append(self)
|
||||
app = QtCore.QCoreApplication.instance()
|
||||
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):
|
||||
BusyCursor.active.pop(-1)
|
||||
if len(BusyCursor.active) == 0:
|
||||
if self._active:
|
||||
BusyCursor.active.pop(-1)
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
@ -42,6 +42,11 @@ class ColorMapWidget(ptree.ParameterTree):
|
||||
def restoreState(self, 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):
|
||||
sigColorMapChanged = QtCore.Signal(object)
|
||||
|
@ -3,13 +3,18 @@ from .. import parametertree as ptree
|
||||
import numpy as np
|
||||
from ..pgcollections import OrderedDict
|
||||
from .. import functions as fn
|
||||
from ..python2_3 import basestring
|
||||
|
||||
__all__ = ['DataFilterWidget']
|
||||
|
||||
|
||||
class DataFilterWidget(ptree.ParameterTree):
|
||||
"""
|
||||
This class allows the user to filter multi-column data sets by specifying
|
||||
multiple criteria
|
||||
|
||||
Wraps methods from DataFilterParameter: setFields, generateMask,
|
||||
filterData, and describe.
|
||||
"""
|
||||
|
||||
sigFilterChanged = QtCore.Signal(object)
|
||||
@ -22,6 +27,7 @@ class DataFilterWidget(ptree.ParameterTree):
|
||||
self.params.sigTreeStateChanged.connect(self.filterChanged)
|
||||
|
||||
self.setFields = self.params.setFields
|
||||
self.generateMask = self.params.generateMask
|
||||
self.filterData = self.params.filterData
|
||||
self.describe = self.params.describe
|
||||
|
||||
@ -30,10 +36,16 @@ class DataFilterWidget(ptree.ParameterTree):
|
||||
|
||||
def parameters(self):
|
||||
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):
|
||||
|
||||
"""A parameter group that specifies a set of filters to apply to tabular data.
|
||||
"""
|
||||
sigFilterChanged = QtCore.Signal(object)
|
||||
|
||||
def __init__(self):
|
||||
@ -47,18 +59,36 @@ class DataFilterParameter(ptree.types.GroupParameter):
|
||||
def addNew(self, name):
|
||||
mode = self.fields[name].get('mode', 'range')
|
||||
if mode == 'range':
|
||||
self.addChild(RangeFilterItem(name, self.fields[name]))
|
||||
child = self.addChild(RangeFilterItem(name, self.fields[name]))
|
||||
elif mode == 'enum':
|
||||
self.addChild(EnumFilterItem(name, self.fields[name]))
|
||||
|
||||
child = self.addChild(EnumFilterItem(name, self.fields[name]))
|
||||
return child
|
||||
|
||||
def fieldNames(self):
|
||||
return self.fields.keys()
|
||||
|
||||
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)
|
||||
names = self.fieldNames()
|
||||
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):
|
||||
if len(data) == 0:
|
||||
@ -66,6 +96,9 @@ class DataFilterParameter(ptree.types.GroupParameter):
|
||||
return data[self.generateMask(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)
|
||||
if len(data) == 0:
|
||||
return mask
|
||||
@ -89,6 +122,7 @@ class DataFilterParameter(ptree.types.GroupParameter):
|
||||
desc.append(fp.describe())
|
||||
return desc
|
||||
|
||||
|
||||
class RangeFilterItem(ptree.types.SimpleParameter):
|
||||
def __init__(self, name, opts):
|
||||
self.fieldName = name
|
||||
@ -109,25 +143,17 @@ class RangeFilterItem(ptree.types.SimpleParameter):
|
||||
|
||||
def describe(self):
|
||||
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):
|
||||
def __init__(self, name, opts):
|
||||
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,
|
||||
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True,
|
||||
children=childs)
|
||||
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True)
|
||||
self.setEnumVals(opts)
|
||||
|
||||
def generateMask(self, data, startMask):
|
||||
vals = data[self.fieldName][startMask]
|
||||
@ -147,4 +173,38 @@ class EnumFilterItem(ptree.types.SimpleParameter):
|
||||
|
||||
def describe(self):
|
||||
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)
|
||||
|
@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtGui, QtCore
|
||||
from ..pgcollections import OrderedDict
|
||||
from .TableWidget import TableWidget
|
||||
from ..python2_3 import asUnicode
|
||||
import types, traceback
|
||||
import numpy as np
|
||||
|
||||
@ -17,67 +19,106 @@ class DataTreeWidget(QtGui.QTreeWidget):
|
||||
Widget for displaying hierarchical python data structures
|
||||
(eg, nested dicts, lists, and arrays)
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, parent=None, data=None):
|
||||
QtGui.QTreeWidget.__init__(self, parent)
|
||||
self.setVerticalScrollMode(self.ScrollPerPixel)
|
||||
self.setData(data)
|
||||
self.setColumnCount(3)
|
||||
self.setHeaderLabels(['key / index', 'type', 'value'])
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
def setData(self, data, hideRoot=False):
|
||||
"""data should be a dictionary."""
|
||||
self.clear()
|
||||
self.widgets = []
|
||||
self.nodes = {}
|
||||
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.resizeColumnToContents(0)
|
||||
|
||||
def buildTree(self, data, parent, name='', hideRoot=False):
|
||||
def buildTree(self, data, parent, name='', hideRoot=False, path=()):
|
||||
if hideRoot:
|
||||
node = parent
|
||||
else:
|
||||
typeStr = type(data).__name__
|
||||
if typeStr == 'instance':
|
||||
typeStr += ": " + data.__class__.__name__
|
||||
node = QtGui.QTreeWidgetItem([name, typeStr, ""])
|
||||
node = QtGui.QTreeWidgetItem([name, "", ""])
|
||||
parent.addChild(node)
|
||||
|
||||
if isinstance(data, types.TracebackType): ## convert traceback to a list of strings
|
||||
data = list(map(str.strip, traceback.format_list(traceback.extract_tb(data))))
|
||||
elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||
data = {
|
||||
'data': data.view(np.ndarray),
|
||||
'meta': data.infoCopy()
|
||||
}
|
||||
# record the path to the node so it can be retrieved later
|
||||
# (this is used by DiffTreeWidget)
|
||||
self.nodes[path] = node
|
||||
|
||||
typeStr, desc, childs, widget = self.parse(data)
|
||||
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):
|
||||
for k in data.keys():
|
||||
self.buildTree(data[k], node, str(k))
|
||||
elif isinstance(data, list) or isinstance(data, tuple):
|
||||
for i in range(len(data)):
|
||||
self.buildTree(data[i], node, str(i))
|
||||
desc = "length=%d" % len(data)
|
||||
if isinstance(data, OrderedDict):
|
||||
childs = data
|
||||
else:
|
||||
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:
|
||||
node.setText(2, str(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
|
||||
desc = asUnicode(data)
|
||||
|
||||
return typeStr, desc, childs, widget
|
||||
|
164
pyqtgraph/widgets/DiffTreeWidget.py
Normal file
164
pyqtgraph/widgets/DiffTreeWidget.py
Normal 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))
|
||||
|
@ -1,4 +1,4 @@
|
||||
from ..Qt import QtGui
|
||||
from ..Qt import QtGui, mkQApp
|
||||
from ..graphicsItems.GraphicsLayout import GraphicsLayout
|
||||
from .GraphicsView import GraphicsView
|
||||
|
||||
@ -48,6 +48,7 @@ class GraphicsLayoutWidget(GraphicsView):
|
||||
:func:`clear <pyqtgraph.GraphicsLayout.clear>`
|
||||
"""
|
||||
def __init__(self, parent=None, show=False, size=None, title=None, **kargs):
|
||||
mkQApp()
|
||||
GraphicsView.__init__(self, parent)
|
||||
self.ci = GraphicsLayout(**kargs)
|
||||
for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']:
|
||||
|
@ -122,7 +122,7 @@ if HAVE_OPENGL:
|
||||
if not self.uploaded:
|
||||
self.uploadTexture()
|
||||
|
||||
glViewport(0, 0, self.width(), self.height())
|
||||
glViewport(0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio())
|
||||
glEnable(GL_TEXTURE_2D)
|
||||
glBindTexture(GL_TEXTURE_2D, self.texture)
|
||||
glColor4f(1,1,1,1)
|
||||
|
@ -52,16 +52,23 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
||||
self.ctrlPanel.addWidget(self.ptree)
|
||||
self.addWidget(self.plot)
|
||||
|
||||
bg = fn.mkColor(getConfigOption('background'))
|
||||
bg.setAlpha(150)
|
||||
self.filterText = TextItem(border=getConfigOption('foreground'), color=bg)
|
||||
fg = fn.mkColor(getConfigOption('foreground'))
|
||||
fg.setAlpha(150)
|
||||
self.filterText = TextItem(border=getConfigOption('foreground'), color=fg)
|
||||
self.filterText.setPos(60,20)
|
||||
self.filterText.setParentItem(self.plot.plotItem)
|
||||
|
||||
self.data = None
|
||||
self.indices = None
|
||||
self.mouseOverField = None
|
||||
self.scatterPlot = None
|
||||
self.selectionScatter = None
|
||||
self.selectedIndices = []
|
||||
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.filter.sigFilterChanged.connect(self.filterChanged)
|
||||
@ -83,16 +90,45 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
||||
item = self.fieldList.addItem(item)
|
||||
self.filter.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):
|
||||
"""
|
||||
Set the data to be processed and displayed.
|
||||
Argument must be a numpy record array.
|
||||
"""
|
||||
self.data = data
|
||||
self.indices = np.arange(len(data))
|
||||
self.filtered = None
|
||||
self.filteredIndices = None
|
||||
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):
|
||||
sel = self.fieldList.selectedItems()
|
||||
if len(sel) > 2:
|
||||
@ -114,15 +150,16 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
||||
else:
|
||||
self.filterText.setText('\n'.join(desc))
|
||||
self.filterText.setVisible(True)
|
||||
|
||||
|
||||
def updatePlot(self):
|
||||
self.plot.clear()
|
||||
if self.data is None:
|
||||
if self.data is None or len(self.data) == 0:
|
||||
return
|
||||
|
||||
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
|
||||
if len(data) == 0:
|
||||
return
|
||||
@ -177,12 +214,14 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
||||
## mask out any nan values
|
||||
mask = np.ones(len(xy[0]), dtype=bool)
|
||||
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':
|
||||
mask &= ~np.isnan(xy[1])
|
||||
mask &= np.isfinite(xy[1])
|
||||
|
||||
xy[0] = xy[0][mask]
|
||||
style['symbolBrush'] = colors[mask]
|
||||
data = data[mask]
|
||||
indices = self.filteredIndices[mask]
|
||||
|
||||
## Scatter y-values for a histogram-like appearance
|
||||
if xy[1] is None:
|
||||
@ -204,16 +243,44 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
||||
if smax != 0:
|
||||
scatter *= 0.2 / smax
|
||||
xy[ax][keymask] += scatter
|
||||
|
||||
|
||||
|
||||
if self.scatterPlot is not None:
|
||||
try:
|
||||
self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked)
|
||||
except:
|
||||
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):
|
||||
# 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)
|
||||
|
||||
|
||||
|
@ -146,7 +146,8 @@ class TableWidget(QtGui.QTableWidget):
|
||||
i += 1
|
||||
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)
|
||||
|
||||
def setEditable(self, editable=True):
|
||||
@ -216,6 +217,8 @@ class TableWidget(QtGui.QTableWidget):
|
||||
return self.iterate, list(map(asUnicode, data.dtype.names))
|
||||
elif data is None:
|
||||
return (None,None)
|
||||
elif np.isscalar(data):
|
||||
return self.iterateScalar, None
|
||||
else:
|
||||
msg = "Don't know how to iterate over data type: {!s}".format(type(data))
|
||||
raise TypeError(msg)
|
||||
@ -230,6 +233,9 @@ class TableWidget(QtGui.QTableWidget):
|
||||
for x in data:
|
||||
yield x
|
||||
|
||||
def iterateScalar(self, data):
|
||||
yield data
|
||||
|
||||
def appendRow(self, data):
|
||||
self.appendData([data])
|
||||
|
||||
|
@ -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)
|
Loading…
Reference in New Issue
Block a user