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

@ -93,6 +93,15 @@ class Canvas(QtGui.QWidget):
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

@ -210,6 +210,9 @@ class ConsoleWidget(QtGui.QWidget):
#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)

View File

@ -491,9 +491,6 @@ def transformToArray(tr):
## map coordinates through transform
mapped = np.dot(m, coords)
"""
if isinstance(tr, np.ndarray):
return tr
#return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]])
## The order of elements given by the method names m11..m33 is misleading--
## It is most common for x,y translation to occupy the positions 1,3 and 2,3 in
@ -506,18 +503,28 @@ def transformToArray(tr):
else:
raise Exception("Transform argument must be either QTransform or QMatrix4x4.")
def transformCoordinates(tr, coords):
def transformCoordinates(tr, coords, transpose=False):
"""
Map a set of 2D or 3D coordinates through a QTransform or QMatrix4x4.
The shape of coords must be (2,...) or (3,...)
The mapping will _ignore_ any perspective transformations.
For coordinate arrays with ndim=2, this is basically equivalent to matrix multiplication.
Most arrays, however, prefer to put the coordinate axis at the end (eg. shape=(...,3)). To
allow this, use transpose=True.
"""
if transpose:
## move last axis to beginning. This transposition will be reversed before returning the mapped coordinates.
coords = coords.transpose((coords.ndim-1,) + tuple(range(0,coords.ndim-1)))
nd = coords.shape[0]
if not isinstance(tr, np.ndarray):
if isinstance(tr, np.ndarray):
m = tr
else:
m = transformToArray(tr)
m = m[:m.shape[0]-1] # remove perspective
else:
m = tr
## If coords are 3D and tr is 2D, assume no change for Z axis
if m.shape == (2,3) and nd == 3:
@ -545,9 +552,15 @@ def transformCoordinates(tr, coords):
## map coordinates and return
mapped = (m*coords).sum(axis=1) ## apply scale/rotate
mapped += translate
if transpose:
## move first axis to end.
mapped = mapped.transpose(tuple(range(1,mapped.ndim)) + (0,))
return mapped
def solve3DTransform(points1, points2):
"""
Find a 3D transformation matrix that maps points1 onto points2
@ -782,7 +795,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
if levels.shape != (data.shape[-1], 2):
raise Exception('levels must have shape (data.shape[-1], 2)')
else:
print(levels)
print levels
raise Exception("levels argument must be 1D or 2D.")
#levels = np.array(levels)
#if levels.shape == (2,):
@ -1066,16 +1079,43 @@ def imageToArray(img, copy=False, transpose=True):
#return facets
def isocurve(data, level):
def isocurve(data, level, connected=False, extendToEdge=False, path=False):
"""
Generate isocurve from 2D data using marching squares algorithm.
*data* 2D numpy array of scalar values
*level* The level at which to generate an isosurface
============= =========================================================
Arguments
data 2D numpy array of scalar values
level The level at which to generate an isosurface
connected If False, return a single long list of point pairs
If True, return multiple long lists of connected point
locations. (This is slower but better for drawing
continuous lines)
extendToEdge If True, extend the curves to reach the exact edges of
the data.
path if True, return a QPainterPath rather than a list of
vertex coordinates. This forces connected=True.
============= =========================================================
This function is SLOW; plenty of room for optimization here.
"""
if path is True:
connected = True
if extendToEdge:
d2 = np.empty((data.shape[0]+2, data.shape[1]+2), dtype=data.dtype)
d2[1:-1, 1:-1] = data
d2[0, 1:-1] = data[0]
d2[-1, 1:-1] = data[-1]
d2[1:-1, 0] = data[:, 0]
d2[1:-1, -1] = data[:, -1]
d2[0,0] = d2[0,1]
d2[0,-1] = d2[1,-1]
d2[-1,0] = d2[-1,1]
d2[-1,-1] = d2[-1,-2]
data = d2
sideTable = [
[],
[0,1],
@ -1096,7 +1136,7 @@ def isocurve(data, level):
]
edgeKey=[
[(0,1),(0,0)],
[(0,1), (0,0)],
[(0,0), (1,0)],
[(1,0), (1,1)],
[(1,1), (0,1)]
@ -1140,31 +1180,150 @@ def isocurve(data, level):
p1[0]*fi + p2[0]*f + i + 0.5,
p1[1]*fi + p2[1]*f + j + 0.5
)
if extendToEdge:
## check bounds
p = (
min(data.shape[0]-2, max(0, p[0]-1)),
min(data.shape[1]-2, max(0, p[1]-1)),
)
if connected:
gridKey = i + (1 if edges[m]==2 else 0), j + (1 if edges[m]==3 else 0), edges[m]%2
pts.append((p, gridKey)) ## give the actual position and a key identifying the grid location (for connecting segments)
else:
pts.append(p)
lines.append(pts)
if not connected:
return lines
## turn disjoint list of segments into continuous lines
#lines = [[2,5], [5,4], [3,4], [1,3], [6,7], [7,8], [8,6], [11,12], [12,15], [11,13], [13,14]]
#lines = [[(float(a), a), (float(b), b)] for a,b in lines]
points = {} ## maps each point to its connections
for a,b in lines:
if a[1] not in points:
points[a[1]] = []
points[a[1]].append([a,b])
if b[1] not in points:
points[b[1]] = []
points[b[1]].append([b,a])
## rearrange into chains
for k in points.keys():
try:
chains = points[k]
except KeyError: ## already used this point elsewhere
continue
#print "===========", k
for chain in chains:
#print " chain:", chain
x = None
while True:
if x == chain[-1][1]:
break ## nothing left to do on this chain
x = chain[-1][1]
if x == k:
break ## chain has looped; we're done and can ignore the opposite chain
y = chain[-2][1]
connects = points[x]
for conn in connects[:]:
if conn[1][1] != y:
#print " ext:", conn
chain.extend(conn[1:])
#print " del:", x
del points[x]
if chain[0][1] == chain[-1][1]: # looped chain; no need to continue the other direction
chains.pop()
break
## extract point locations
lines = []
for chain in points.values():
if len(chain) == 2:
chain = chain[1][1:][::-1] + chain[0] # join together ends of chain
else:
chain = chain[0]
lines.append([p[0] for p in chain])
if not path:
return lines ## a list of pairs of points
path = QtGui.QPainterPath()
for line in lines:
path.moveTo(*line[0])
for p in line[1:]:
path.lineTo(*p)
return path
def traceImage(image, values, smooth=0.5):
"""
Convert an image to a set of QPainterPath curves.
One curve will be generated for each item in *values*; each curve outlines the area
of the image that is closer to its value than to any others.
If image is RGB or RGBA, then the shape of values should be (nvals, 3/4)
The parameter *smooth* is expressed in pixels.
"""
import scipy.ndimage as ndi
if values.ndim == 2:
values = values.T
values = values[np.newaxis, np.newaxis, ...].astype(float)
image = image[..., np.newaxis].astype(float)
diff = np.abs(image-values)
if values.ndim == 4:
diff = diff.sum(axis=2)
labels = np.argmin(diff, axis=2)
paths = []
for i in range(diff.shape[-1]):
d = (labels==i).astype(float)
d = ndi.gaussian_filter(d, (smooth, smooth))
lines = isocurve(d, 0.5, connected=True, extendToEdge=True)
path = QtGui.QPainterPath()
for line in lines:
path.moveTo(*line[0])
for p in line[1:]:
path.lineTo(*p)
paths.append(path)
return paths
IsosurfaceDataCache = None
def isosurface(data, level):
"""
Generate isosurface from volumetric data using marching cubes algorithm.
See Paul Bourke, "Polygonising a Scalar Field"
(http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/)
(http://paulbourke.net/geometry/polygonise/)
*data* 3D numpy array of scalar values
*level* The level at which to generate an isosurface
Returns an array of vertex coordinates (N, 3, 3);
This function is SLOW; plenty of room for optimization here.
Returns an array of vertex coordinates (Nv, 3) and an array of
per-face vertex indexes (Nf, 3)
"""
## For improvement, see:
##
## Efficient implementation of Marching Cubes' cases with topological guarantees.
## Thomas Lewiner, Helio Lopes, Antonio Wilson Vieira and Geovan Tavares.
## Journal of Graphics Tools 8(2): pp. 1-15 (december 2003)
## Precompute lookup tables on the first run
global IsosurfaceDataCache
if IsosurfaceDataCache is None:
## map from grid cell index to edge index.
## grid cell index tells us which corners are below the isosurface,
## edge index tells us which edges are cut by the isosurface.
## (Data stolen from Bourk; see above.)
edgeTable = [
edgeTable = np.array([
0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
@ -1196,7 +1355,7 @@ def isosurface(data, level):
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190,
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ]
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ], dtype=np.uint16)
## Table of triangles to use for filling each grid cell.
## Each set of three integers tells us which three edges to
@ -1460,27 +1619,43 @@ def isosurface(data, level):
[0, 3, 8],
[]
]
edgeShifts = np.array([ ## maps edge ID (0-11) to (x,y,z) cell offset and edge ID (0-2)
[0, 0, 0, 0],
[1, 0, 0, 1],
[0, 1, 0, 0],
[0, 0, 0, 1],
[0, 0, 1, 0],
[1, 0, 1, 1],
[0, 1, 1, 0],
[0, 0, 1, 1],
[0, 0, 0, 2],
[1, 0, 0, 2],
[1, 1, 0, 2],
[0, 1, 0, 2],
#[9, 9, 9, 9] ## fake
], dtype=np.ubyte)
nTableFaces = np.array([len(f)/3 for f in triTable], dtype=np.ubyte)
faceShiftTables = [None]
for i in range(1,6):
## 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 = faceTableI.reshape((len(triTable), i, 3))
faceShiftTables.append(edgeShifts[faceTableI])
## translation between edge index and
## the vertex indexes that bound the edge
edgeKey = [
[(0,0,0), (1,0,0)],
[(1,0,0), (1,1,0)],
[(1,1,0), (0,1,0)],
[(0,1,0), (0,0,0)],
[(0,0,1), (1,0,1)],
[(1,0,1), (1,1,1)],
[(1,1,1), (0,1,1)],
[(0,1,1), (0,0,1)],
[(0,0,0), (0,0,1)],
[(1,0,0), (1,0,1)],
[(1,1,0), (1,1,1)],
[(0,1,0), (0,1,1)],
]
## Let's try something different:
#faceTable = np.empty((256, 5, 3, 4), dtype=np.ubyte) # (grid cell index, faces, vertexes, edge lookup)
#for i,f in enumerate(triTable):
#f = np.array(f + [12] * (15-len(f))).reshape(5,3)
#faceTable[i] = edgeShifts[f]
IsosurfaceDataCache = (faceShiftTables, edgeShifts, edgeTable, nTableFaces)
else:
faceShiftTables, edgeShifts, edgeTable, nTableFaces = IsosurfaceDataCache
facets = []
## mark everything below the isosurface level
mask = data < level
@ -1494,35 +1669,93 @@ def isosurface(data, level):
for k in [0,1]:
fields[i,j,k] = mask[slices[i], slices[j], slices[k]]
vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme
#print i,j,k," : ", fields[i,j,k], 2**vertIndex
index += fields[i,j,k] * 2**vertIndex
#print index
#print index
## add facets
for i in range(index.shape[0]): # data x-axis
for j in range(index.shape[1]): # data y-axis
for k in range(index.shape[2]): # data z-axis
tris = triTable[index[i,j,k]]
for l in range(0, len(tris), 3): ## faces for this grid cell
edges = tris[l:l+3]
pts = []
for m in [0,1,2]: # points in this face
p1 = edgeKey[edges[m]][0]
p2 = edgeKey[edges[m]][1]
v1 = data[i+p1[0], j+p1[1], k+p1[2]]
v2 = data[i+p2[0], j+p2[1], k+p2[2]]
f = (level-v1) / (v2-v1)
fi = 1.0 - f
p = ( ## interpolate between corners
p1[0]*fi + p2[0]*f + i + 0.5,
p1[1]*fi + p2[1]*f + j + 0.5,
p1[2]*fi + p2[2]*f + k + 0.5
)
pts.append(p)
facets.append(pts)
### Generate table of edges that have been cut
cutEdges = np.zeros([x+1 for x in index.shape]+[3], dtype=np.uint32)
edges = edgeTable[index]
for i, shift in enumerate(edgeShifts[:12]):
slices = [slice(shift[j],cutEdges.shape[j]+(shift[j]-1)) for j in range(3)]
cutEdges[slices[0], slices[1], slices[2], shift[3]] += edges & 2**i
return np.array(facets)
## for each cut edge, interpolate to see where exactly the edge is cut and generate vertex positions
m = cutEdges > 0
vertexInds = np.argwhere(m) ## argwhere is slow!
vertexes = vertexInds[:,:3].astype(np.float32)
dataFlat = data.reshape(data.shape[0]*data.shape[1]*data.shape[2])
## re-use the cutEdges array as a lookup table for vertex IDs
cutEdges[vertexInds[:,0], vertexInds[:,1], vertexInds[:,2], vertexInds[:,3]] = np.arange(vertexInds.shape[0])
for i in [0,1,2]:
vim = vertexInds[:,3] == i
vi = vertexInds[vim, :3]
viFlat = (vi * (np.array(data.strides[:3]) / data.itemsize)[np.newaxis,:]).sum(axis=1)
v1 = dataFlat[viFlat]
v2 = dataFlat[viFlat + data.strides[i]/data.itemsize]
vertexes[vim,i] += (level-v1) / (v2-v1)
### compute the set of vertex indexes for each face.
## This works, but runs a bit slower.
#cells = np.argwhere((index != 0) & (index != 255)) ## all cells with at least one face
#cellInds = index[cells[:,0], cells[:,1], cells[:,2]]
#verts = faceTable[cellInds]
#mask = verts[...,0,0] != 9
#verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges
#verts = verts[mask]
#faces = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want.
## To allow this to be vectorized efficiently, we count the number of faces in each
## grid cell and handle each group of cells with the same number together.
## determine how many faces to assign to each grid cell
nFaces = nTableFaces[index]
totFaces = nFaces.sum()
faces = np.empty((totFaces, 3), dtype=np.uint32)
ptr = 0
#import debug
#p = debug.Profiler('isosurface', disabled=False)
## this helps speed up an indexing operation later on
cs = np.array(cutEdges.strides)/cutEdges.itemsize
cutEdges = cutEdges.flatten()
## this, strangely, does not seem to help.
#ins = np.array(index.strides)/index.itemsize
#index = index.flatten()
for i in range(1,6):
### expensive:
#p.mark('1')
cells = np.argwhere(nFaces == i) ## all cells which require i faces (argwhere is expensive)
#p.mark('2')
if cells.shape[0] == 0:
continue
#cellInds = index[(cells*ins[np.newaxis,:]).sum(axis=1)]
cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round
#p.mark('3')
### expensive:
verts = faceShiftTables[i][cellInds]
#p.mark('4')
verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges
verts = verts.reshape((verts.shape[0]*i,)+verts.shape[2:])
#p.mark('5')
### expensive:
#print verts.shape
verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2)
#vertInds = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want.
vertInds = cutEdges[verts]
#p.mark('6')
nv = vertInds.shape[0]
#p.mark('7')
faces[ptr:ptr+nv] = vertInds #.reshape((nv, 3))
#p.mark('8')
ptr += nv
return vertexes, faces

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
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
@ -450,3 +459,21 @@ class GraphicsItem(object):
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,20 +94,26 @@ 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)

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()
@ -1232,5 +1239,45 @@ class ViewBox(GraphicsWidget):
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",
],
)