fixed scatterplotitem segfault

added graphitem
This commit is contained in:
Luke Campagnola 2013-01-30 15:56:08 -05:00
parent 899663c6ca
commit ee21e2d054
6 changed files with 284 additions and 55 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

@ -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

@ -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

@ -671,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])
@ -683,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
@ -732,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: