From 1b7cd44e27c8d4109a5739eb1879fd9e4599f9ab Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 30 Jan 2013 15:56:08 -0500 Subject: [PATCH] fixed scatterplotitem segfault added graphitem --- functions.py | 92 ++++++++++++++++++++++++ graphicsItems/GraphItem.py | 109 +++++++++++++++++++++++++++++ graphicsItems/PlotCurveItem.py | 55 +-------------- graphicsItems/PlotItem/PlotItem.py | 15 ++++ graphicsItems/ScatterPlotItem.py | 5 +- 5 files changed, 221 insertions(+), 55 deletions(-) create mode 100644 graphicsItems/GraphItem.py diff --git a/functions.py b/functions.py index d5899c8c..86298ea2 100644 --- a/functions.py +++ b/functions.py @@ -27,6 +27,7 @@ from pyqtgraph import getConfigOption import numpy as np import decimal, re import ctypes +import sys, struct try: 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): #""" #Generate isosurface from volumetric data using marching tetrahedra algorithm. diff --git a/graphicsItems/GraphItem.py b/graphicsItems/GraphItem.py new file mode 100644 index 00000000..9a0f1244 --- /dev/null +++ b/graphicsItems/GraphItem.py @@ -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() + + + + + + diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index c54671bb..b321714a 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -249,26 +249,6 @@ class PlotCurveItem(GraphicsObject): prof.finish() 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']: ## each value in the x/y arrays generates 2 points. x2 = np.empty((len(x),2), dtype=x.dtype) @@ -286,41 +266,8 @@ class PlotCurveItem(GraphicsObject): y = y2.reshape(y2.size)[1:-1] y[0] = self.opts['fillLevel'] y[-1] = self.opts['fillLevel'] - - - - - 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]) + path = fn.arrayToQPath(x, y, connect='all') return path diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index c362ffb5..63b4bf03 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -1054,6 +1054,21 @@ class PlotItem(GraphicsWidget): """ 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): """ Show or hide one of the plot's axis labels (the axis itself will be unaffected). diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index d606cfdf..32fac052 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -671,6 +671,8 @@ class ScatterPlotItem(GraphicsObject): pts[1] = self.data['y'] pts = fn.transformCoordinates(tr, pts) 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)): rec = self.data[i] pos = QtCore.QPointF(pts[0,i], pts[1,i]) @@ -683,8 +685,10 @@ class ScatterPlotItem(GraphicsObject): self.invalidate() def paint(self, p, *args): + #p.setPen(fn.mkPen('r')) #p.drawRect(self.boundingRect()) + if self._exportOpts is not False: aa = self._exportOpts.get('antialias', True) scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed @@ -731,7 +735,6 @@ class ScatterPlotItem(GraphicsObject): p2.end() self.picture.play(p) - def points(self): for rec in self.data: