From e05447de519241ecadea3e6904e321f2b05c6512 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 24 Jan 2013 13:47:05 -0500 Subject: [PATCH 1/2] bugfixes for scatterplot boundary miss added method for setting axis tick font --- graphicsItems/AxisItem.py | 33 +++++++++++++++++++++++++------- graphicsItems/GraphicsItem.py | 2 +- graphicsItems/ScatterPlotItem.py | 5 ++++- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index aba5fa8c..8d8ed11b 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -58,13 +58,14 @@ class AxisItem(GraphicsWidget): self.labelUnitPrefix='' self.labelStyle = {} self.logMode = False + self.tickFont = None self.textHeight = 18 self.tickLength = maxTickLength self._tickLevels = None ## used to override the automatic ticking system with explicit ticks self.scale = 1.0 self.autoScale = True - + self.setRange(0, 1) self.setPen(pen) @@ -72,12 +73,12 @@ class AxisItem(GraphicsWidget): self._linkedView = None if linkView is not None: self.linkToView(linkView) - + self.showLabel(False) self.grid = False #self.setCacheMode(self.DeviceCoordinateCache) - + def close(self): self.scene().removeItem(self.label) self.label = None @@ -100,6 +101,14 @@ class AxisItem(GraphicsWidget): self.picture = None 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): #s = self.size() @@ -287,14 +296,21 @@ class AxisItem(GraphicsWidget): if linkedView is None or self.grid is False: rect = self.mapRectFromParent(self.geometry()) ## extend rect if ticks go in negative direction + ## also extend to account for text that flows past the edges 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': - 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': - 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': - 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 else: return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) @@ -623,6 +639,9 @@ class AxisItem(GraphicsWidget): prof.mark('draw ticks') ## Draw text until there is no more room (or no more text) + if self.tickFont is not None: + p.setFont(self.tickFont) + textRects = [] for i in range(len(tickLevels)): ## Get the list of strings to display for this level diff --git a/graphicsItems/GraphicsItem.py b/graphicsItems/GraphicsItem.py index 94615fe3..75e72177 100644 --- a/graphicsItems/GraphicsItem.py +++ b/graphicsItems/GraphicsItem.py @@ -202,7 +202,7 @@ class GraphicsItem(object): ## check global cache key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32()) pv = self._pixelVectorGlobalCache.get(key, None) - if pv is not None: + if direction is None and pv is not None: self._pixelVectorCache = [dt, pv] return tuple(map(Point,pv)) ## return a *copy* diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index 0b422596..d606cfdf 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -620,9 +620,12 @@ class ScatterPlotItem(GraphicsObject): if frac >= 1.0: ## 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: px = 0 + else: + px = px.length() minIndex = np.argmin(d) maxIndex = np.argmax(d) minVal = d[minIndex] From 1b7cd44e27c8d4109a5739eb1879fd9e4599f9ab Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 30 Jan 2013 15:56:08 -0500 Subject: [PATCH 2/2] 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: