merge from acq4

This commit is contained in:
Luke Campagnola 2013-01-30 15:59:48 -05:00
commit 5bd3adfa7c
8 changed files with 315 additions and 64 deletions

63
examples/GraphItem.py Normal file
View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""
Simple example of GridItem use.
"""
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
w = pg.GraphicsWindow()
v = w.addViewBox()
v.setAspectLocked()
g = pg.GraphItem()
v.addItem(g)
## Define positions of nodes
pos = np.array([
[0,0],
[10,0],
[0,10],
[10,10],
[5,5],
[15,5]
])
## Define the set of connections in the graph
adj = np.array([
[0,1],
[1,3],
[3,2],
[2,0],
[1,5],
[3,5],
])
## Define the symbol to use for each node (this is optional)
symbols = ['o','o','o','o','t','+']
## Define the line style for each connection (this is optional)
lines = np.array([
(255,0,0,255,1),
(255,0,255,255,2),
(255,0,255,255,3),
(255,255,0,255,2),
(255,0,0,255,1),
(255,255,255,255,4),
], dtype=[('red',np.ubyte),('green',np.ubyte),('blue',np.ubyte),('alpha',np.ubyte),('width',float)])
## Update the graph
g.setData(pos=pos, adj=adj, pen=lines, size=1, symbol=symbols, pxMode=False)
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -27,6 +27,7 @@ from pyqtgraph import getConfigOption
import numpy as np import numpy as np
import decimal, re import decimal, re
import ctypes import ctypes
import sys, struct
try: try:
import scipy.ndimage import scipy.ndimage
@ -1041,6 +1042,97 @@ def colorToAlpha(data, color):
def arrayToQPath(x, y, connect='all'):
"""Convert an array of x,y coordinats to QPath as efficiently as possible.
The *connect* argument may be 'all', indicating that each point should be
connected to the next; 'pairs', indicating that each pair of points
should be connected, or an array of int32 values (0 or 1) indicating
connections.
"""
## Create all vertices in path. The method used below creates a binary format so that all
## vertices can be read in at once. This binary format may change in future versions of Qt,
## so the original (slower) method is left here for emergencies:
#path.moveTo(x[0], y[0])
#for i in range(1, y.shape[0]):
# path.lineTo(x[i], y[i])
## Speed this up using >> operator
## Format is:
## numVerts(i4) 0(i4)
## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect
## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex
## ...
## 0(i4)
##
## All values are big endian--pack using struct.pack('>d') or struct.pack('>i')
path = QtGui.QPainterPath()
#prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True)
if sys.version_info[0] == 2: ## So this is disabled for python 3... why??
n = x.shape[0]
# create empty array, pad with extra space on either end
arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')])
# write first two integers
#prof.mark('allocate empty')
arr.data[12:20] = struct.pack('>ii', n, 0)
#prof.mark('pack header')
# Fill array with vertex values
arr[1:-1]['x'] = x
arr[1:-1]['y'] = y
# decide which points are connected by lines
if connect == 'pairs':
connect = np.empty((n/2,2), dtype=np.int32)
connect[:,0] = 1
connect[:,1] = 0
connect = connect.flatten()
if connect == 'all':
arr[1:-1]['c'] = 1
elif isinstance(connect, np.ndarray):
arr[1:-1]['c'] = connect
else:
raise Exception('connect argument must be "all", "pairs", or array')
#prof.mark('fill array')
# write last 0
lastInd = 20*(n+1)
arr.data[lastInd:lastInd+4] = struct.pack('>i', 0)
#prof.mark('footer')
# create datastream object and stream into path
buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here
#prof.mark('create buffer')
ds = QtCore.QDataStream(buf)
#prof.mark('create datastream')
ds >> path
#prof.mark('load')
#prof.finish()
else:
## This does exactly the same as above, but less efficiently (and more simply).
path.moveTo(x[0], y[0])
if connect == 'all':
for i in range(1, y.shape[0]):
path.lineTo(x[i], y[i])
elif connect == 'pairs':
for i in range(1, y.shape[0]):
if i%2 == 0:
path.lineTo(x[i], y[i])
else:
path.moveTo(x[i], y[i])
elif isinstance(connect, np.ndarray):
for i in range(1, y.shape[0]):
if connect[i] == 1:
path.lineTo(x[i], y[i])
else:
path.moveTo(x[i], y[i])
else:
raise Exception('connect argument must be "all", "pairs", or array')
return path
#def isosurface(data, level): #def isosurface(data, level):
#""" #"""
#Generate isosurface from volumetric data using marching tetrahedra algorithm. #Generate isosurface from volumetric data using marching tetrahedra algorithm.

View File

@ -58,6 +58,7 @@ class AxisItem(GraphicsWidget):
self.labelUnitPrefix='' self.labelUnitPrefix=''
self.labelStyle = {} self.labelStyle = {}
self.logMode = False self.logMode = False
self.tickFont = None
self.textHeight = 18 self.textHeight = 18
self.tickLength = maxTickLength self.tickLength = maxTickLength
@ -100,6 +101,14 @@ class AxisItem(GraphicsWidget):
self.picture = None self.picture = None
self.update() self.update()
def setTickFont(self, font):
self.tickFont = font
self.picture = None
self.prepareGeometryChange()
## Need to re-allocate space depending on font size?
self.update()
def resizeEvent(self, ev=None): def resizeEvent(self, ev=None):
#s = self.size() #s = self.size()
@ -311,14 +320,21 @@ class AxisItem(GraphicsWidget):
if linkedView is None or self.grid is False: if linkedView is None or self.grid is False:
rect = self.mapRectFromParent(self.geometry()) rect = self.mapRectFromParent(self.geometry())
## extend rect if ticks go in negative direction ## extend rect if ticks go in negative direction
## also extend to account for text that flows past the edges
if self.orientation == 'left': if self.orientation == 'left':
rect.setRight(rect.right() - min(0,self.tickLength)) #rect.setRight(rect.right() - min(0,self.tickLength))
#rect.setTop(rect.top() - 15)
#rect.setBottom(rect.bottom() + 15)
rect = rect.adjusted(0, -15, -min(0,self.tickLength), 15)
elif self.orientation == 'right': elif self.orientation == 'right':
rect.setLeft(rect.left() + min(0,self.tickLength)) #rect.setLeft(rect.left() + min(0,self.tickLength))
rect = rect.adjusted(min(0,self.tickLength), -15, 0, 15)
elif self.orientation == 'top': elif self.orientation == 'top':
rect.setBottom(rect.bottom() - min(0,self.tickLength)) #rect.setBottom(rect.bottom() - min(0,self.tickLength))
rect = rect.adjusted(-15, 0, 15, -min(0,self.tickLength))
elif self.orientation == 'bottom': elif self.orientation == 'bottom':
rect.setTop(rect.top() + min(0,self.tickLength)) #rect.setTop(rect.top() + min(0,self.tickLength))
rect = rect.adjusted(-15, min(0,self.tickLength), 15, 0)
return rect return rect
else: else:
return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
@ -647,6 +663,9 @@ class AxisItem(GraphicsWidget):
prof.mark('draw ticks') prof.mark('draw ticks')
## Draw text until there is no more room (or no more text) ## Draw text until there is no more room (or no more text)
if self.tickFont is not None:
p.setFont(self.tickFont)
textRects = [] textRects = []
for i in range(len(tickLevels)): for i in range(len(tickLevels)):
## Get the list of strings to display for this level ## Get the list of strings to display for this level

View File

@ -0,0 +1,109 @@
from .. import functions as fn
from GraphicsObject import GraphicsObject
from ScatterPlotItem import ScatterPlotItem
import pyqtgraph as pg
import numpy as np
__all__ = ['GraphItem']
class GraphItem(GraphicsObject):
"""A GraphItem displays graph information (as in 'graph theory', not 'graphics') as
a set of nodes connected by lines.
"""
def __init__(self, **kwds):
GraphicsObject.__init__(self)
self.scatter = ScatterPlotItem()
self.scatter.setParentItem(self)
self.adjacency = None
self.pos = None
self.picture = None
self.pen = 'default'
self.setData(**kwds)
def setData(self, **kwds):
"""
Change the data displayed by the graph.
============ =========================================================
Arguments
pos (N,2) array of the positions of each node in the graph
adj (M,2) array of connection data. Each row contains indexes
of two nodes that are connected.
pen The pen to use when drawing lines between connected
nodes. May be one of:
* QPen
* a single argument to pass to pg.mkPen
* a record array of length M
with fields (red, green, blue, alpha, width).
* None (to disable connection drawing)
* 'default' to use the default foreground color.
symbolPen The pen used for drawing nodes.
**opts All other keyword arguments are given to ScatterPlotItem
to affect the appearance of nodes (symbol, size, brush,
etc.)
============ =========================================================
"""
if 'adj' in kwds:
self.adjacency = kwds.pop('adj')
assert self.adjacency.dtype.kind in 'iu'
self.picture = None
if 'pos' in kwds:
self.pos = kwds['pos']
self.picture = None
if 'pen' in kwds:
self.setPen(kwds.pop('pen'))
self.picture = None
if 'symbolPen' in kwds:
kwds['pen'] = kwds.pop('symbolPen')
self.scatter.setData(**kwds)
self.informViewBoundsChanged()
def setPen(self, pen):
self.pen = pen
self.picture = None
def generatePicture(self):
self.picture = pg.QtGui.QPicture()
if self.pen is None or self.pos is None or self.adjacency is None:
return
p = pg.QtGui.QPainter(self.picture)
try:
pts = self.pos[self.adjacency]
pen = self.pen
if isinstance(pen, np.ndarray):
lastPen = None
for i in range(pts.shape[0]):
pen = self.pen[i]
if np.any(pen != lastPen):
lastPen = pen
if pen.dtype.fields is None:
p.setPen(pg.mkPen(color=(pen[0], pen[1], pen[2], pen[3]), width=1))
else:
p.setPen(pg.mkPen(color=(pen['red'], pen['green'], pen['blue'], pen['alpha']), width=pen['width']))
p.drawLine(pg.QtCore.QPointF(*pts[i][0]), pg.QtCore.QPointF(*pts[i][1]))
else:
if pen == 'default':
pen = pg.getConfigOption('foreground')
p.setPen(pg.mkPen(pen))
pts = pts.reshape((pts.shape[0]*pts.shape[1], pts.shape[2]))
path = fn.arrayToQPath(x=pts[:,0], y=pts[:,1], connect='pairs')
p.drawPath(path)
finally:
p.end()
def paint(self, p, *args):
if self.picture == None:
self.generatePicture()
self.picture.play(p)
def boundingRect(self):
return self.scatter.boundingRect()

View File

@ -202,7 +202,7 @@ class GraphicsItem(object):
## check global cache ## check global cache
key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32()) key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
pv = self._pixelVectorGlobalCache.get(key, None) pv = self._pixelVectorGlobalCache.get(key, None)
if pv is not None: if direction is None and pv is not None:
self._pixelVectorCache = [dt, pv] self._pixelVectorCache = [dt, pv]
return tuple(map(Point,pv)) ## return a *copy* return tuple(map(Point,pv)) ## return a *copy*

View File

@ -249,26 +249,6 @@ class PlotCurveItem(GraphicsObject):
prof.finish() prof.finish()
def generatePath(self, x, y): def generatePath(self, x, y):
prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True)
path = QtGui.QPainterPath()
## Create all vertices in path. The method used below creates a binary format so that all
## vertices can be read in at once. This binary format may change in future versions of Qt,
## so the original (slower) method is left here for emergencies:
#path.moveTo(x[0], y[0])
#for i in range(1, y.shape[0]):
# path.lineTo(x[i], y[i])
## Speed this up using >> operator
## Format is:
## numVerts(i4) 0(i4)
## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect
## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex
## ...
## 0(i4)
##
## All values are big endian--pack using struct.pack('>d') or struct.pack('>i')
if self.opts['stepMode']: if self.opts['stepMode']:
## each value in the x/y arrays generates 2 points. ## each value in the x/y arrays generates 2 points.
x2 = np.empty((len(x),2), dtype=x.dtype) x2 = np.empty((len(x),2), dtype=x.dtype)
@ -287,40 +267,7 @@ class PlotCurveItem(GraphicsObject):
y[0] = self.opts['fillLevel'] y[0] = self.opts['fillLevel']
y[-1] = self.opts['fillLevel'] y[-1] = self.opts['fillLevel']
path = fn.arrayToQPath(x, y, connect='all')
if sys.version_info[0] == 2: ## So this is disabled for python 3... why??
n = x.shape[0]
# create empty array, pad with extra space on either end
arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')])
# write first two integers
prof.mark('allocate empty')
arr.data[12:20] = struct.pack('>ii', n, 0)
prof.mark('pack header')
# Fill array with vertex values
arr[1:-1]['x'] = x
arr[1:-1]['y'] = y
arr[1:-1]['c'] = 1
prof.mark('fill array')
# write last 0
lastInd = 20*(n+1)
arr.data[lastInd:lastInd+4] = struct.pack('>i', 0)
prof.mark('footer')
# create datastream object and stream into path
buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here
prof.mark('create buffer')
ds = QtCore.QDataStream(buf)
prof.mark('create datastream')
ds >> path
prof.mark('load')
prof.finish()
else:
path.moveTo(x[0], y[0])
for i in range(1, y.shape[0]):
path.lineTo(x[i], y[i])
return path return path

View File

@ -1054,6 +1054,21 @@ class PlotItem(GraphicsWidget):
""" """
self.getAxis(axis).setLabel(text=text, units=units, **args) self.getAxis(axis).setLabel(text=text, units=units, **args)
def setLabels(self, **kwds):
"""
Convenience function allowing multiple labels and/or title to be set in one call.
Keyword arguments can be 'title', 'left', 'bottom', 'right', or 'top'.
Values may be strings or a tuple of arguments to pass to setLabel.
"""
for k,v in kwds.items():
if k == 'title':
self.setTitle(v)
else:
if isinstance(v, basestring):
v = (v,)
self.setLabel(k, *v)
def showLabel(self, axis, show=True): def showLabel(self, axis, show=True):
""" """
Show or hide one of the plot's axis labels (the axis itself will be unaffected). Show or hide one of the plot's axis labels (the axis itself will be unaffected).

View File

@ -620,9 +620,12 @@ class ScatterPlotItem(GraphicsObject):
if frac >= 1.0: if frac >= 1.0:
## increase size of bounds based on spot size and pen width ## increase size of bounds based on spot size and pen width
px = self.pixelLength(Point(1, 0) if ax == 0 else Point(0, 1)) ## determine length of pixel along this axis #px = self.pixelLength(Point(1, 0) if ax == 0 else Point(0, 1)) ## determine length of pixel along this axis
px = self.pixelVectors()[ax]
if px is None: if px is None:
px = 0 px = 0
else:
px = px.length()
minIndex = np.argmin(d) minIndex = np.argmin(d)
maxIndex = np.argmax(d) maxIndex = np.argmax(d)
minVal = d[minIndex] minVal = d[minIndex]
@ -668,6 +671,8 @@ class ScatterPlotItem(GraphicsObject):
pts[1] = self.data['y'] pts[1] = self.data['y']
pts = fn.transformCoordinates(tr, pts) pts = fn.transformCoordinates(tr, pts)
self.fragments = [] self.fragments = []
pts = np.clip(pts, -2**31, 2**31) ## prevent Qt segmentation fault.
## Still won't be able to render correctly, though.
for i in xrange(len(self.data)): for i in xrange(len(self.data)):
rec = self.data[i] rec = self.data[i]
pos = QtCore.QPointF(pts[0,i], pts[1,i]) pos = QtCore.QPointF(pts[0,i], pts[1,i])
@ -680,8 +685,10 @@ class ScatterPlotItem(GraphicsObject):
self.invalidate() self.invalidate()
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._exportOpts is not False: if self._exportOpts is not False:
aa = self._exportOpts.get('antialias', True) aa = self._exportOpts.get('antialias', True)
scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed
@ -729,7 +736,6 @@ class ScatterPlotItem(GraphicsObject):
self.picture.play(p) self.picture.play(p)
def points(self): def points(self):
for rec in self.data: for rec in self.data:
if rec['item'] is None: if rec['item'] is None: