Features:

- Canvas: added per-item context menus
- Isocurve: 
     option to extend curves to array boundaries
     option to generate QPainterPath instead of vertex array
- Isosurface is a bajillion times faster
- ViewBox
     added clear() method
     added locate(item) method (shows where an item is for debugging)

Bugfixes:
- automated example testing working properly
- Exporter gets incorrect source rect when operating on PlotWidget
- Set correct DPI and size for SVG exporter
- GLMeshItem works properly with whole-mesh color specified as sequence
- bugfix in functions.transformCoordinates for rotated matrices
- reload library checks for modules that are imported multiple times
- GraphicsObject, UIGraphicsItem: added workaround for PyQt / itemChange bug
- ScatterPlotItem: disable cached render during export

Other:
- added documentation for several functions
- minor updates to setup.py
This commit is contained in:
Luke Campagnola 2012-12-22 16:51:25 -05:00
commit b25e34f564
20 changed files with 838 additions and 406 deletions

View File

@ -92,6 +92,15 @@ class Canvas(QtGui.QWidget):
if name is not None:
self.registeredName = CanvasManager.instance().registerCanvas(self, name)
self.ui.redirectCombo.setHostName(self.registeredName)
self.menu = QtGui.QMenu()
#self.menu.setTitle("Image")
remAct = QtGui.QAction("Remove item", self.menu)
remAct.triggered.connect(self.removeClicked)
self.menu.addAction(remAct)
self.menu.remAct = remAct
self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent
def storeSvg(self):
self.ui.view.writeSvg()
@ -513,10 +522,20 @@ class Canvas(QtGui.QWidget):
listItem.setCheckState(0, QtCore.Qt.Unchecked)
def removeItem(self, item):
if isinstance(item, QtGui.QTreeWidgetItem):
item = item.canvasItem()
if isinstance(item, CanvasItem):
item.setCanvas(None)
self.itemList.removeTopLevelItem(item.listItem)
listItem = item.listItem
listItem.canvasItem = None
item.listItem = None
self.itemList.removeTopLevelItem(listItem)
self.items.remove(item)
ctrl = item.ctrlWidget()
ctrl.hide()
self.ui.ctrlLayout.removeWidget(ctrl)
else:
if hasattr(item, '_canvasItem'):
self.removeItem(item._canvasItem)
@ -555,7 +574,15 @@ class Canvas(QtGui.QWidget):
#self.emit(QtCore.SIGNAL('itemTransformChangeFinished'), self, item)
self.sigItemTransformChangeFinished.emit(self, item)
def itemListContextMenuEvent(self, ev):
self.menuItem = self.itemList.itemAt(ev.pos())
self.menu.popup(ev.globalPos())
def removeClicked(self):
self.removeItem(self.menuItem)
self.menuItem = None
import gc
gc.collect()
class SelectBox(ROI):
def __init__(self, scalable=False):

View File

@ -208,8 +208,11 @@ class ConsoleWidget(QtGui.QWidget):
#self.stdout.write("</div><br><div style='font-weight: normal; background-color: #FFF;'>")
self.output.insertPlainText(strn)
#self.stdout.write(strn)
def displayException(self):
"""
Display the current exception and stack.
"""
tb = traceback.format_exc()
lines = []
indent = 4

View File

@ -8,7 +8,7 @@ Simple Data Display Functions
.. autofunction:: pyqtgraph.image
.. autofunction:: pyqtgraph.dbg
Color, Pen, and Brush Functions
-------------------------------
@ -34,6 +34,8 @@ Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and
.. autofunction:: pyqtgraph.colorStr
.. autofunction:: pyqtgraph.glColor
Data Slicing
------------
@ -41,6 +43,18 @@ Data Slicing
.. autofunction:: pyqtgraph.affineSlice
Coordinate Transformation
-------------------------
.. autofunction:: pyqtgraph.transformToArray
.. autofunction:: pyqtgraph.transformCoordinates
.. autofunction:: pyqtgraph.solve3DTransform
.. autofunction:: pyqtgraph.solveBilinearTransform
SI Unit Conversion Functions
----------------------------
@ -59,6 +73,12 @@ Image Preparation Functions
.. autofunction:: pyqtgraph.makeQImage
.. autofunction:: pyqtgraph.applyLookupTable
.. autofunction:: pyqtgraph.rescaleData
.. autofunction:: pyqtgraph.imageToArray
Mesh Generation Functions
-------------------------
@ -68,4 +88,13 @@ Mesh Generation Functions
.. autofunction:: pyqtgraph.isosurface
Miscellaneous Functions
-----------------------
.. autofunction:: pyqtgraph.pseudoScatter
.. autofunction:: pyqtgraph.systemInfo

View File

@ -45,9 +45,9 @@ data = np.abs(np.fromfunction(psi, (50,50,100)))
print("Generating isosurface..")
verts = pg.isosurface(data, data.max()/4.)
verts, faces = pg.isosurface(data, data.max()/4.)
md = gl.MeshData(vertexes=verts)
md = gl.MeshData(vertexes=verts, faces=faces)
colors = np.ones((md.faceCount(), 4), dtype=float)
colors[:,3] = 0.2

View File

@ -175,8 +175,11 @@ def testFile(name, f, exe, lib):
try:
%s
import %s
import sys
print("test complete")
sys.stdout.flush()
import pyqtgraph as pg
import time
while True: ## run a little event loop
pg.QtGui.QApplication.processEvents()
time.sleep(0.01)
@ -186,7 +189,7 @@ except:
""" % ("import %s" % lib if lib != '' else "", os.path.splitext(os.path.split(fn)[1])[0])
#print code
process = subprocess.Popen(['%s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
process.stdin.write(code.encode('UTF-8'))
#process.stdin.close()
output = ''
@ -202,10 +205,11 @@ except:
fail = True
break
time.sleep(1)
process.terminate()
process.kill()
#process.wait()
res = process.communicate()
#if 'exception' in res[1].lower() or 'error' in res[1].lower():
if fail:
if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower():
print('.' * (50-len(name)) + 'FAILED')
print(res[0].decode())
print(res[1].decode())

View File

@ -19,6 +19,7 @@ frames = 200
data = np.random.normal(size=(frames,30,30), loc=0, scale=100)
data = np.concatenate([data, data], axis=0)
data = ndi.gaussian_filter(data, (10, 10, 10))[frames/2:frames + frames/2]
data[:, 15:16, 15:17] += 1
win = pg.GraphicsWindow()
vb = win.addViewBox()

View File

@ -73,7 +73,8 @@ class Exporter(object):
def getSourceRect(self):
if isinstance(self.item, pg.GraphicsScene):
return self.item.getViewWidget().viewRect()
w = self.item.getViewWidget()
return w.viewportTransform().inverted()[0].mapRect(w.rect())
else:
return self.item.sceneBoundingRect()

View File

@ -36,11 +36,14 @@ class SVGExporter(Exporter):
return
self.svg = QtSvg.QSvgGenerator()
self.svg.setFileName(fileName)
self.svg.setSize(QtCore.QSize(100,100))
#self.svg.setResolution(600)
dpi = QtGui.QDesktopWidget().physicalDpiX()
## not really sure why this works, but it seems to be important:
self.svg.setSize(QtCore.QSize(self.params['width']*dpi/90., self.params['height']*dpi/90.))
self.svg.setResolution(dpi)
#self.svg.setViewBox()
targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height'])
sourceRect = self.getSourceRect()
painter = QtGui.QPainter(self.svg)
try:
self.setExportMode(True)

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ from pyqtgraph.GraphicsScene import GraphicsScene
from pyqtgraph.Point import Point
import pyqtgraph.functions as fn
import weakref
import operator
class GraphicsItem(object):
"""
@ -395,8 +396,16 @@ class GraphicsItem(object):
## disconnect from previous view
if oldView is not None:
#print "disconnect:", self, oldView
oldView.sigRangeChanged.disconnect(self.viewRangeChanged)
oldView.sigTransformChanged.disconnect(self.viewTransformChanged)
try:
oldView.sigRangeChanged.disconnect(self.viewRangeChanged)
except TypeError:
pass
try:
oldView.sigTransformChanged.disconnect(self.viewTransformChanged)
except TypeError:
pass
self._connectedView = None
## connect to new view
@ -449,4 +458,22 @@ class GraphicsItem(object):
view = self.getViewBox()
if view is not None and hasattr(view, 'implements') and view.implements('ViewBox'):
view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
def childrenShape(self):
"""Return the union of the shapes of all descendants of this item in local coordinates."""
childs = self.allChildItems()
shapes = [self.mapFromItem(c, c.shape()) for c in self.allChildItems()]
return reduce(operator.add, shapes)
def allChildItems(self, root=None):
"""Return list of the entire item tree descending from this item."""
if root is None:
root = self
tree = []
for ch in root.childItems():
tree.append(ch)
tree.extend(self.allChildItems(ch))
return tree

View File

@ -1,4 +1,6 @@
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
if not USE_PYSIDE:
import sip
from .GraphicsItem import GraphicsItem
__all__ = ['GraphicsObject']
@ -20,4 +22,10 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
self._updateView()
if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
self.informViewBoundsChanged()
## workaround for pyqt bug:
## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html
if not USE_PYSIDE and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem):
ret = sip.cast(ret, QtGui.QGraphicsItem)
return ret

View File

@ -45,12 +45,12 @@ class IsocurveItem(GraphicsObject):
"""
Set the data/image to draw isocurves for.
============= ================================================================
============= ========================================================================
**Arguments**
data A 2-dimensional ndarray.
level The cutoff value at which to draw the curve. If level is not specified,
the previous level is used.
============= ================================================================
the previously set level is used.
============= ========================================================================
"""
if level is None:
level = self.level
@ -74,6 +74,12 @@ class IsocurveItem(GraphicsObject):
self.pen = fn.mkPen(*args, **kwargs)
self.update()
def setBrush(self, *args, **kwargs):
"""Set the brush used to draw the isocurve. Arguments can be any that are valid
for :func:`mkBrush <pyqtgraph.mkBrush>`"""
self.brush = fn.mkBrush(*args, **kwargs)
self.update()
def updateLines(self, data, level):
##print "data:", data
@ -88,22 +94,28 @@ class IsocurveItem(GraphicsObject):
self.setData(data, level)
def boundingRect(self):
if self.path is None:
if self.data is None:
return QtCore.QRectF()
if self.path is None:
self.generatePath()
return self.path.boundingRect()
def generatePath(self):
self.path = QtGui.QPainterPath()
if self.data is None:
self.path = None
return
lines = fn.isocurve(self.data, self.level)
lines = fn.isocurve(self.data, self.level, connected=True, extendToEdge=True)
self.path = QtGui.QPainterPath()
for line in lines:
self.path.moveTo(*line[0])
self.path.lineTo(*line[1])
for p in line[1:]:
self.path.lineTo(*p)
def paint(self, p, *args):
if self.data is None:
return
if self.path is None:
self.generatePath()
p.setPen(self.pen)
p.drawPath(self.path)

View File

@ -233,7 +233,7 @@ class ScatterPlotItem(GraphicsObject):
self.bounds = [None, None] ## caches data bounds
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
self.opts = {'pxMode': True, 'useCache': True} ## If useCache is False, symbols are re-drawn on every paint.
self.opts = {'pxMode': True, 'useCache': True, 'exportMode': False} ## If useCache is False, symbols are re-drawn on every paint.
self.setPen(200,200,200, update=False)
self.setBrush(100,100,150, update=False)
@ -664,10 +664,14 @@ class ScatterPlotItem(GraphicsObject):
rect = QtCore.QRectF(y, x, h, w)
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
def setExportMode(self, enabled, opts):
self.opts['exportMode'] = enabled
def paint(self, p, *args):
#p.setPen(fn.mkPen('r'))
#p.drawRect(self.boundingRect())
if self.opts['pxMode']:
if self.opts['pxMode'] is True:
atlas = self.fragmentAtlas.getAtlas()
#arr = fn.imageToArray(atlas.toImage(), copy=True)
#if hasattr(self, 'lastAtlas'):
@ -681,7 +685,7 @@ class ScatterPlotItem(GraphicsObject):
p.resetTransform()
if not USE_PYSIDE and self.opts['useCache']:
if not USE_PYSIDE and self.opts['useCache'] and self.opts['exportMode'] is False:
p.drawPixmapFragments(self.fragments, atlas)
else:
for i in range(len(self.data)):

View File

@ -7,7 +7,7 @@ class TextItem(UIGraphicsItem):
"""
GraphicsItem displaying unscaled text (the text will always appear normal even inside a scaled ViewBox).
"""
def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None):
def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0):
"""
=========== =================================================================================
Arguments:
@ -22,6 +22,12 @@ class TextItem(UIGraphicsItem):
*fill* A brush to use when filling within the border
=========== =================================================================================
"""
## not working yet
#*angle* Angle in degrees to rotate text (note that the rotation assigned in this item's
#transformation will be ignored)
UIGraphicsItem.__init__(self)
self.textItem = QtGui.QGraphicsTextItem()
self.lastTransform = None
@ -33,6 +39,7 @@ class TextItem(UIGraphicsItem):
self.anchor = pg.Point(anchor)
self.fill = pg.mkBrush(fill)
self.border = pg.mkPen(border)
self.angle = angle
#self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport
def setText(self, text, color=(200,200,200)):
@ -115,9 +122,11 @@ class TextItem(UIGraphicsItem):
#p.fillRect(tbr)
p.resetTransform()
p.drawRect(tbr)
#p.drawRect(tbr)
p.translate(tbr.left(), tbr.top())
p.rotate(self.angle)
p.drawRect(QtCore.QRectF(0, 0, tbr.width(), tbr.height()))
self.textItem.paint(p, QtGui.QStyleOptionGraphicsItem(), None)

View File

@ -1,6 +1,8 @@
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
import weakref
from .GraphicsObject import GraphicsObject
if not USE_PYSIDE:
import sip
__all__ = ['UIGraphicsItem']
class UIGraphicsItem(GraphicsObject):
@ -44,9 +46,12 @@ class UIGraphicsItem(GraphicsObject):
def itemChange(self, change, value):
ret = GraphicsObject.itemChange(self, change, value)
#if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged: ## handled by GraphicsItem now.
##print "caught parent/scene change:", self.parentItem(), self.scene()
#self.updateView()
## workaround for pyqt bug:
## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html
if not USE_PYSIDE and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem):
ret = sip.cast(ret, QtGui.QGraphicsItem)
if change == self.ItemScenePositionHasChanged:
self.setNewBounds()
return ret

View File

@ -111,6 +111,7 @@ class ViewBox(GraphicsWidget):
}
self._updatingRange = False ## Used to break recursive loops. See updateAutoRange.
self.locateGroup = None ## items displayed when using ViewBox.locate(item)
self.setFlag(self.ItemClipsChildrenToShape)
self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses
@ -286,6 +287,12 @@ class ViewBox(GraphicsWidget):
self.scene().removeItem(item)
self.updateAutoRange()
def clear(self):
for i in self.addedItems[:]:
self.removeItem(i)
for ch in self.childGroup.childItems():
ch.setParent(None)
def resizeEvent(self, ev):
#self.setRange(self.range, padding=0)
#self.updateAutoRange()
@ -1231,6 +1238,46 @@ class ViewBox(GraphicsWidget):
k.destroyed.disconnect()
except RuntimeError: ## signal is already disconnected.
pass
def locate(self, item, timeout=3.0, children=False):
"""
Temporarily display the bounding rect of an item and lines connecting to the center of the view.
This is useful for determining the location of items that may be out of the range of the ViewBox.
if allChildren is True, then the bounding rect of all item's children will be shown instead.
"""
self.clearLocate()
if item.scene() is not self.scene():
raise Exception("Item does not share a scene with this ViewBox.")
c = self.viewRect().center()
if children:
br = self.mapFromItemToView(item, item.childrenBoundingRect()).boundingRect()
else:
br = self.mapFromItemToView(item, item.boundingRect()).boundingRect()
g = ItemGroup()
g.setParentItem(self.childGroup)
self.locateGroup = g
g.box = QtGui.QGraphicsRectItem(br)
g.box.setParentItem(g)
g.lines = []
for p in (br.topLeft(), br.bottomLeft(), br.bottomRight(), br.topRight()):
line = QtGui.QGraphicsLineItem(c.x(), c.y(), p.x(), p.y())
line.setParentItem(g)
g.lines.append(line)
for item in g.childItems():
item.setPen(fn.mkPen(color='y', width=3))
item.setZValue(1000000)
QtCore.QTimer.singleShot(timeout*1000, self.clearLocate)
def clearLocate(self):
if self.locateGroup is None:
return
self.scene().removeItem(self.locateGroup)
self.locateGroup = None
from .ViewBoxMenu import ViewBoxMenu

View File

@ -158,7 +158,7 @@ class GLMeshItem(GLGraphicsItem):
if self.colors is None:
color = self.opts['color']
if isinstance(color, QtGui.QColor):
glColor4f(*fn.glColor(color))
glColor4f(*pg.glColor(color))
else:
glColor4f(*color)
else:

View File

@ -53,6 +53,7 @@ if sys.version_info[0] == 3:
else:
return 0
builtins.cmp = cmp
builtins.xrange = range
#else: ## don't use __builtin__ -- this confuses things like pyshell and ActiveState's lazy import recipe
#import __builtin__
#__builtin__.asUnicode = asUnicode

View File

@ -35,6 +35,7 @@ def reloadAll(prefix=None, debug=False):
- if prefix is None, checks all loaded modules
"""
failed = []
changed = []
for modName, mod in list(sys.modules.items()): ## don't use iteritems; size may change during reload
if not inspect.ismodule(mod):
continue
@ -50,10 +51,11 @@ def reloadAll(prefix=None, debug=False):
## ignore if the .pyc is newer than the .py (or if there is no pyc or py)
py = os.path.splitext(mod.__file__)[0] + '.py'
pyc = py + 'c'
if os.path.isfile(pyc) and os.path.isfile(py) and os.stat(pyc).st_mtime >= os.stat(py).st_mtime:
if py not in changed and os.path.isfile(pyc) and os.path.isfile(py) and os.stat(pyc).st_mtime >= os.stat(py).st_mtime:
#if debug:
#print "Ignoring module %s; unchanged" % str(mod)
continue
changed.append(py) ## keep track of which modules have changed to insure that duplicate-import modules get reloaded.
try:
reload(mod, debug=debug)
@ -73,7 +75,7 @@ def reload(module, debug=False, lists=False, dicts=False):
- Requires that class and function names have not changed
"""
if debug:
print("Reloading", module)
print("Reloading %s" % str(module))
## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison
oldDict = module.__dict__.copy()
@ -158,7 +160,7 @@ def updateClass(old, new, debug):
if isinstance(ref, old) and ref.__class__ is old:
ref.__class__ = new
if debug:
print(" Changed class for", safeStr(ref))
print(" Changed class for %s" % safeStr(ref))
elif inspect.isclass(ref) and issubclass(ref, old) and old in ref.__bases__:
ind = ref.__bases__.index(old)
@ -174,7 +176,7 @@ def updateClass(old, new, debug):
## (and I presume this may slow things down?)
ref.__bases__ = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:]
if debug:
print(" Changed superclass for", safeStr(ref))
print(" Changed superclass for %s" % safeStr(ref))
#else:
#if debug:
#print " Ignoring reference", type(ref)
@ -208,7 +210,7 @@ def updateClass(old, new, debug):
for attr in dir(new):
if not hasattr(old, attr):
if debug:
print(" Adding missing attribute", attr)
print(" Adding missing attribute %s" % attr)
setattr(old, attr, getattr(new, attr))
## finally, update any previous versions still hanging around..

View File

@ -9,7 +9,11 @@ all_packages = ['.'.join(p) for p in subdirs]
setup(name='pyqtgraph',
version='',
description='Scientific Graphics and GUI Library for Python',
long_description="PyQtGraph is a pure-python graphics and GUI library built on PyQt4 and numpy. It is intended for use in mathematics / scientific / engineering applications. Despite being written entirely in python, the library is very fast due to its heavy leverage of numpy for number crunching and Qt's GraphicsView framework for fast display.",
long_description="""\
PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and numpy.
It is intended for use in mathematics / scientific / engineering applications. Despite being written entirely in python, the library is very fast due to its heavy leverage of numpy for number crunching and Qt's GraphicsView framework for fast display.
""",
license='MIT',
url='http://www.pyqtgraph.org',
author='Luke Campagnola',
@ -17,5 +21,17 @@ setup(name='pyqtgraph',
packages=all_packages,
package_dir = {'pyqtgraph': '.'},
package_data={'pyqtgraph': ['graphicsItems/PlotItem/*.png']},
classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Development Status :: 4 - Beta",
"Environment :: Other Environment",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Scientific/Engineering :: Visualization",
"Topic :: Software Development :: User Interfaces",
],
)