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:
commit
b25e34f564
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
355
functions.py
355
functions.py
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
12
reload.py
12
reload.py
@ -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..
|
||||
|
18
setup.py
18
setup.py
@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user