Commit b25e34f5 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

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
...@@ -92,6 +92,15 @@ class Canvas(QtGui.QWidget): ...@@ -92,6 +92,15 @@ class Canvas(QtGui.QWidget):
if name is not None: if name is not None:
self.registeredName = CanvasManager.instance().registerCanvas(self, name) self.registeredName = CanvasManager.instance().registerCanvas(self, name)
self.ui.redirectCombo.setHostName(self.registeredName) 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): def storeSvg(self):
self.ui.view.writeSvg() self.ui.view.writeSvg()
...@@ -513,10 +522,20 @@ class Canvas(QtGui.QWidget): ...@@ -513,10 +522,20 @@ class Canvas(QtGui.QWidget):
listItem.setCheckState(0, QtCore.Qt.Unchecked) listItem.setCheckState(0, QtCore.Qt.Unchecked)
def removeItem(self, item): def removeItem(self, item):
if isinstance(item, QtGui.QTreeWidgetItem):
item = item.canvasItem()
if isinstance(item, CanvasItem): if isinstance(item, CanvasItem):
item.setCanvas(None) 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) self.items.remove(item)
ctrl = item.ctrlWidget()
ctrl.hide()
self.ui.ctrlLayout.removeWidget(ctrl)
else: else:
if hasattr(item, '_canvasItem'): if hasattr(item, '_canvasItem'):
self.removeItem(item._canvasItem) self.removeItem(item._canvasItem)
...@@ -555,7 +574,15 @@ class Canvas(QtGui.QWidget): ...@@ -555,7 +574,15 @@ class Canvas(QtGui.QWidget):
#self.emit(QtCore.SIGNAL('itemTransformChangeFinished'), self, item) #self.emit(QtCore.SIGNAL('itemTransformChangeFinished'), self, item)
self.sigItemTransformChangeFinished.emit(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): class SelectBox(ROI):
def __init__(self, scalable=False): def __init__(self, scalable=False):
......
...@@ -208,8 +208,11 @@ class ConsoleWidget(QtGui.QWidget): ...@@ -208,8 +208,11 @@ class ConsoleWidget(QtGui.QWidget):
#self.stdout.write("</div><br><div style='font-weight: normal; background-color: #FFF;'>") #self.stdout.write("</div><br><div style='font-weight: normal; background-color: #FFF;'>")
self.output.insertPlainText(strn) self.output.insertPlainText(strn)
#self.stdout.write(strn) #self.stdout.write(strn)
def displayException(self): def displayException(self):
"""
Display the current exception and stack.
"""
tb = traceback.format_exc() tb = traceback.format_exc()
lines = [] lines = []
indent = 4 indent = 4
......
...@@ -8,7 +8,7 @@ Simple Data Display Functions ...@@ -8,7 +8,7 @@ Simple Data Display Functions
.. autofunction:: pyqtgraph.image .. autofunction:: pyqtgraph.image
.. autofunction:: pyqtgraph.dbg
Color, Pen, and Brush Functions Color, Pen, and Brush Functions
------------------------------- -------------------------------
...@@ -34,6 +34,8 @@ Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and ...@@ -34,6 +34,8 @@ Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and
.. autofunction:: pyqtgraph.colorStr .. autofunction:: pyqtgraph.colorStr
.. autofunction:: pyqtgraph.glColor
Data Slicing Data Slicing
------------ ------------
...@@ -41,6 +43,18 @@ Data Slicing ...@@ -41,6 +43,18 @@ Data Slicing
.. autofunction:: pyqtgraph.affineSlice .. autofunction:: pyqtgraph.affineSlice
Coordinate Transformation
-------------------------
.. autofunction:: pyqtgraph.transformToArray
.. autofunction:: pyqtgraph.transformCoordinates
.. autofunction:: pyqtgraph.solve3DTransform
.. autofunction:: pyqtgraph.solveBilinearTransform
SI Unit Conversion Functions SI Unit Conversion Functions
---------------------------- ----------------------------
...@@ -59,6 +73,12 @@ Image Preparation Functions ...@@ -59,6 +73,12 @@ Image Preparation Functions
.. autofunction:: pyqtgraph.makeQImage .. autofunction:: pyqtgraph.makeQImage
.. autofunction:: pyqtgraph.applyLookupTable
.. autofunction:: pyqtgraph.rescaleData
.. autofunction:: pyqtgraph.imageToArray
Mesh Generation Functions Mesh Generation Functions
------------------------- -------------------------
...@@ -68,4 +88,13 @@ Mesh Generation Functions ...@@ -68,4 +88,13 @@ Mesh Generation Functions
.. autofunction:: pyqtgraph.isosurface .. autofunction:: pyqtgraph.isosurface
Miscellaneous Functions
-----------------------
.. autofunction:: pyqtgraph.pseudoScatter
.. autofunction:: pyqtgraph.systemInfo
...@@ -45,9 +45,9 @@ data = np.abs(np.fromfunction(psi, (50,50,100))) ...@@ -45,9 +45,9 @@ data = np.abs(np.fromfunction(psi, (50,50,100)))
print("Generating isosurface..") 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 = np.ones((md.faceCount(), 4), dtype=float)
colors[:,3] = 0.2 colors[:,3] = 0.2
......
...@@ -175,8 +175,11 @@ def testFile(name, f, exe, lib): ...@@ -175,8 +175,11 @@ def testFile(name, f, exe, lib):
try: try:
%s %s
import %s import %s
import sys
print("test complete") print("test complete")
sys.stdout.flush()
import pyqtgraph as pg import pyqtgraph as pg
import time
while True: ## run a little event loop while True: ## run a little event loop
pg.QtGui.QApplication.processEvents() pg.QtGui.QApplication.processEvents()
time.sleep(0.01) time.sleep(0.01)
...@@ -186,7 +189,7 @@ except: ...@@ -186,7 +189,7 @@ except:
""" % ("import %s" % lib if lib != '' else "", os.path.splitext(os.path.split(fn)[1])[0]) """ % ("import %s" % lib if lib != '' else "", os.path.splitext(os.path.split(fn)[1])[0])
#print code #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.write(code.encode('UTF-8'))
#process.stdin.close() #process.stdin.close()
output = '' output = ''
...@@ -202,10 +205,11 @@ except: ...@@ -202,10 +205,11 @@ except:
fail = True fail = True
break break
time.sleep(1) time.sleep(1)
process.terminate() process.kill()
#process.wait()
res = process.communicate() 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('.' * (50-len(name)) + 'FAILED')
print(res[0].decode()) print(res[0].decode())
print(res[1].decode()) print(res[1].decode())
......
...@@ -19,6 +19,7 @@ frames = 200 ...@@ -19,6 +19,7 @@ frames = 200
data = np.random.normal(size=(frames,30,30), loc=0, scale=100) data = np.random.normal(size=(frames,30,30), loc=0, scale=100)
data = np.concatenate([data, data], axis=0) data = np.concatenate([data, data], axis=0)
data = ndi.gaussian_filter(data, (10, 10, 10))[frames/2:frames + frames/2] data = ndi.gaussian_filter(data, (10, 10, 10))[frames/2:frames + frames/2]
data[:, 15:16, 15:17] += 1
win = pg.GraphicsWindow() win = pg.GraphicsWindow()
vb = win.addViewBox() vb = win.addViewBox()
......
...@@ -73,7 +73,8 @@ class Exporter(object): ...@@ -73,7 +73,8 @@ class Exporter(object):
def getSourceRect(self): def getSourceRect(self):
if isinstance(self.item, pg.GraphicsScene): if isinstance(self.item, pg.GraphicsScene):
return self.item.getViewWidget().viewRect() w = self.item.getViewWidget()
return w.viewportTransform().inverted()[0].mapRect(w.rect())
else: else:
return self.item.sceneBoundingRect() return self.item.sceneBoundingRect()
......
...@@ -36,11 +36,14 @@ class SVGExporter(Exporter): ...@@ -36,11 +36,14 @@ class SVGExporter(Exporter):
return return
self.svg = QtSvg.QSvgGenerator() self.svg = QtSvg.QSvgGenerator()
self.svg.setFileName(fileName) self.svg.setFileName(fileName)
self.svg.setSize(QtCore.QSize(100,100)) dpi = QtGui.QDesktopWidget().physicalDpiX()
#self.svg.setResolution(600) ## 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() #self.svg.setViewBox()
targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height']) targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height'])
sourceRect = self.getSourceRect() sourceRect = self.getSourceRect()
painter = QtGui.QPainter(self.svg) painter = QtGui.QPainter(self.svg)
try: try:
self.setExportMode(True) self.setExportMode(True)
......
This diff is collapsed.
...@@ -3,6 +3,7 @@ from pyqtgraph.GraphicsScene import GraphicsScene ...@@ -3,6 +3,7 @@ from pyqtgraph.GraphicsScene import GraphicsScene
from pyqtgraph.Point import Point from pyqtgraph.Point import Point
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
import weakref import weakref
import operator
class GraphicsItem(object): class GraphicsItem(object):
""" """
...@@ -395,8 +396,16 @@ class GraphicsItem(object): ...@@ -395,8 +396,16 @@ class GraphicsItem(object):
## disconnect from previous view ## disconnect from previous view
if oldView is not None: if oldView is not None:
#print "disconnect:", self, oldView #print "disconnect:", self, oldView
oldView.sigRangeChanged.disconnect(self.viewRangeChanged) try:
oldView.sigTransformChanged.disconnect(self.viewTransformChanged) oldView.sigRangeChanged.disconnect(self.viewRangeChanged)
except TypeError:
pass
try:
oldView.sigTransformChanged.disconnect(self.viewTransformChanged)
except TypeError:
pass
self._connectedView = None self._connectedView = None
## connect to new view ## connect to new view
...@@ -449,4 +458,22 @@ class GraphicsItem(object): ...@@ -449,4 +458,22 @@ class GraphicsItem(object):
view = self.getViewBox() view = self.getViewBox()
if view is not None and hasattr(view, 'implements') and view.implements('ViewBox'): 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 view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
\ No newline at end of file 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
\ No newline at end of file
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
if not USE_PYSIDE:
import sip
from .GraphicsItem import GraphicsItem from .GraphicsItem import GraphicsItem
__all__ = ['GraphicsObject'] __all__ = ['GraphicsObject']
...@@ -20,4 +22,10 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): ...@@ -20,4 +22,10 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
self._updateView() self._updateView()
if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
self.informViewBoundsChanged() 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 return ret
...@@ -45,12 +45,12 @@ class IsocurveItem(GraphicsObject): ...@@ -45,12 +45,12 @@ class IsocurveItem(GraphicsObject):
""" """
Set the data/image to draw isocurves for. Set the data/image to draw isocurves for.
============= ================================================================ ============= ========================================================================
**Arguments** **Arguments**
data A 2-dimensional ndarray. data A 2-dimensional ndarray.
level The cutoff value at which to draw the curve. If level is not specified, 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: if level is None:
level = self.level level = self.level
...@@ -74,6 +74,12 @@ class IsocurveItem(GraphicsObject): ...@@ -74,6 +74,12 @@ class IsocurveItem(GraphicsObject):
self.pen = fn.mkPen(*args, **kwargs) self.pen = fn.mkPen(*args, **kwargs)
self.update() 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): def updateLines(self, data, level):
##print "data:", data ##print "data:", data
...@@ -88,22 +94,28 @@ class IsocurveItem(GraphicsObject): ...@@ -88,22 +94,28 @@ class IsocurveItem(GraphicsObject):
self.setData(data, level) self.setData(data, level)
def boundingRect(self): def boundingRect(self):
if self.path is None: if self.data is None:
return QtCore.QRectF() return QtCore.QRectF()
if self.path is None:
self.generatePath()
return self.path.boundingRect() return self.path.boundingRect()
def generatePath(self): def generatePath(self):
self.path = QtGui.QPainterPath()
if self.data is None: if self.data is None:
self.path = None
return 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: for line in lines:
self.path.moveTo(*line[0]) self.path.moveTo(*line[0])
self.path.lineTo(*line[1]) for p in line[1:]:
self.path.lineTo(*p)
def paint(self, p, *args): def paint(self, p, *args):
if self.data is None:
return
if self.path is None: if self.path is None:
self.generatePath() self.generatePath()
p.setPen(self.pen) p.setPen(self.pen)
p.drawPath(self.path) p.drawPath(self.path)
\ No newline at end of file \ No newline at end of file
...@@ -233,7 +233,7 @@ class ScatterPlotItem(GraphicsObject): ...@@ -233,7 +233,7 @@ class ScatterPlotItem(GraphicsObject):
self.bounds = [None, None] ## caches data bounds self.bounds = [None, None] ## caches data bounds
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots 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._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.setPen(200,200,200, update=False)
self.setBrush(100,100,150, update=False) self.setBrush(100,100,150, update=False)
...@@ -664,10 +664,14 @@ class ScatterPlotItem(GraphicsObject): ...@@ -664,10 +664,14 @@ class ScatterPlotItem(GraphicsObject):
rect = QtCore.QRectF(y, x, h, w) rect = QtCore.QRectF(y, x, h, w)
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect)) self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
def setExportMode(self, enabled, opts):
self.opts['exportMode'] = enabled
def paint(self, p, *args): def paint(self, p, *args):
#p.setPen(fn.mkPen('r')) #p.setPen(fn.mkPen('r'))
#p.drawRect(self.boundingRect()) #p.drawRect(self.boundingRect())
if self.opts['pxMode']: if self.opts['pxMode'] is True:
atlas = self.fragmentAtlas.getAtlas() atlas = self.fragmentAtlas.getAtlas()
#arr = fn.imageToArray(atlas.toImage(), copy=True) #arr = fn.imageToArray(atlas.toImage(), copy=True)
#if hasattr(self, 'lastAtlas'): #if hasattr(self, 'lastAtlas'):
...@@ -681,7 +685,7 @@ class ScatterPlotItem(GraphicsObject): ...@@ -681,7 +685,7 @@ class ScatterPlotItem(GraphicsObject):
p.resetTransform() 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) p.drawPixmapFragments(self.fragments, atlas)
else: else:
for i in range(len(self.data)): for i in range(len(self.data)):
......
...@@ -7,7 +7,7 @@ class TextItem(UIGraphicsItem): ...@@ -7,7 +7,7 @@ class TextItem(UIGraphicsItem):
""" """
GraphicsItem displaying unscaled text (the text will always appear normal even inside a scaled ViewBox). 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: Arguments:
...@@ -22,6 +22,12 @@ class TextItem(UIGraphicsItem): ...@@ -22,6 +22,12 @@ class TextItem(UIGraphicsItem):
*fill* A brush to use when filling within the border *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) UIGraphicsItem.__init__(self)
self.textItem = QtGui.QGraphicsTextItem() self.textItem = QtGui.QGraphicsTextItem()
self.lastTransform = None self.lastTransform = None
...@@ -33,6 +39,7 @@ class TextItem(UIGraphicsItem): ...@@ -33,6 +39,7 @@ class TextItem(UIGraphicsItem):
self.anchor = pg.Point(anchor) self.anchor = pg.Point(anchor)
self.fill = pg.mkBrush(fill) self.fill = pg.mkBrush(fill)
self.border = pg.mkPen(border) self.border = pg.mkPen(border)
self.angle = angle
#self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport #self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport
def setText(self, text, color=(200,200,200)): def setText(self, text, color=(200,200,200)):
...@@ -115,9 +122,11 @@ class TextItem(UIGraphicsItem): ...@@ -115,9 +122,11 @@ class TextItem(UIGraphicsItem):
#p.fillRect(tbr) #p.fillRect(tbr)
p.resetTransform() p.resetTransform()
p.drawRect(tbr) #p.drawRect(tbr)
p.translate(tbr.left(), tbr.top()) 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) self.textItem.paint(p, QtGui.QStyleOptionGraphicsItem(), None)
\ No newline at end of file
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
import weakref import weakref
from .GraphicsObject import GraphicsObject from .GraphicsObject import GraphicsObject
if not USE_PYSIDE:
import sip
__all__ = ['UIGraphicsItem'] __all__ = ['UIGraphicsItem']