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.
- 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

View File

@ -6,46 +6,21 @@ 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.
@ -53,33 +28,24 @@ Requirements
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.
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`
The official documentation lives at http://pyqtgraph.org/documentation
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
pg.setConfigOptions(antialias=True)
w = pg.GraphicsWindow()
w = pg.GraphicsLayoutWidget(show=True)
w.setWindowTitle('pyqtgraph example: CustomGraphItem')
v = w.addViewBox()
v.setAspectLocked()

View File

@ -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)

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
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

View File

@ -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()

View File

@ -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

View File

@ -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')

View File

@ -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,14 +56,12 @@ 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__':
import sys

View File

@ -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()

View File

@ -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')

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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]

View File

@ -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)

View File

@ -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))

View File

@ -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()

View File

@ -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')

View File

@ -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)

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)
from functools import reduce
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
@ -112,11 +113,3 @@ if __name__ == '__main__':
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -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()

View File

@ -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])

View File

@ -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)

View File

@ -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")

View File

@ -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()

View File

@ -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')

View File

@ -37,6 +37,18 @@ class GraphicsScene(QtGui.QGraphicsScene):
* 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:
1) Every time the mouse moves, the scene delivers both the standard hoverEnter/Move/LeaveEvents

View File

@ -105,7 +105,13 @@ class Point(QtCore.QPointF):
def length(self):
"""Returns the vector length of this Point."""
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."""

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:
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

View File

@ -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.
"""

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,7 +254,7 @@ 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
@ -277,7 +264,8 @@ def _generateItemSvg(item, nodes=None, root=None):
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')

View File

@ -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)

View File

@ -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]]

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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])]

View File

@ -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:

View File

@ -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):

View File

@ -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']

View File

@ -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:

View File

@ -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):
"""
@ -67,7 +68,7 @@ class GLGridItem(GLGraphicsItem):
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)
glColor4f(*self.color)
for x in xvals:
glVertex3f(x, yvals[0], 0)
glVertex3f(x, yvals[-1], 0)

View File

@ -152,6 +152,8 @@ 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]

View File

@ -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]

View File

@ -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,6 +101,8 @@ 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)
@ -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)

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():
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):
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):
if self._active:
BusyCursor.active.pop(-1)
if len(BusyCursor.active) == 0:
QtGui.QApplication.restoreOverrideCursor()

View File

@ -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)

View File

@ -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
@ -31,9 +37,15 @@ 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,25 +59,46 @@ 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:
return data
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
@ -110,24 +144,16 @@ 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]
@ -148,3 +174,37 @@ 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))
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 -*-
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:
node = QtGui.QTreeWidgetItem([name, "", ""])
parent.addChild(node)
# 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__
node = QtGui.QTreeWidgetItem([name, typeStr, ""])
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()
}
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:
node.setText(2, str(data))
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:
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 .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']:

View File

@ -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)

View File

@ -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)
@ -84,15 +91,44 @@ class ScatterPlotWidget(QtGui.QSplitter):
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:
@ -115,14 +151,15 @@ class ScatterPlotWidget(QtGui.QSplitter):
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:
@ -205,15 +244,43 @@ class ScatterPlotWidget(QtGui.QSplitter):
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._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)

View File

@ -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])

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)