From 2f510de2caafdd536c8c8b6329d022202a7374b0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Mar 2013 17:17:39 -0400 Subject: [PATCH 1/4] Added PolyLineROI.getArrayRegion --- examples/ROIExamples.py | 2 +- pyqtgraph/graphicsItems/ImageItem.py | 4 ++- pyqtgraph/graphicsItems/ROI.py | 42 ++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py index 56d6b13c..a67e279d 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -56,7 +56,7 @@ rois.append(pg.MultiRectROI([[20, 90], [50, 60], [60, 90]], width=5, pen=(2,9))) rois.append(pg.EllipseROI([60, 10], [30, 20], pen=(3,9))) rois.append(pg.CircleROI([80, 50], [20, 20], pen=(4,9))) #rois.append(pg.LineSegmentROI([[110, 50], [20, 20]], pen=(5,9))) -#rois.append(pg.PolyLineROI([[110, 60], [20, 30], [50, 10]], pen=(6,9))) +rois.append(pg.PolyLineROI([[80, 60], [90, 30], [60, 40]], pen=(6,9), closed=True)) def update(roi): img1b.setImage(roi.getArrayRegion(arr, img1a), levels=(0, arr.max())) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 123612b8..fad88bee 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -249,7 +249,7 @@ class ImageItem(GraphicsObject): def render(self): prof = debug.Profiler('ImageItem.render', disabled=True) - if self.image is None: + if self.image is None or self.image.size == 0: return if isinstance(self.lut, collections.Callable): lut = self.lut(self.image) @@ -269,6 +269,8 @@ class ImageItem(GraphicsObject): return if self.qimage is None: self.render() + if self.qimage is None: + return prof.mark('render QImage') if self.paintMode is not None: p.setCompositionMode(self.paintMode) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 4da8fa4a..9cdc8c29 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -802,7 +802,11 @@ class ROI(GraphicsObject): Also returns the transform which maps the ROI into data coordinates. If returnSlice is set to False, the function returns a pair of tuples with the values that would have - been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))""" + been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop)) + + If the slice can not be computed (usually because the scene/transforms are not properly + constructed yet), then the method returns None. + """ #print "getArraySlice" ## Determine shape of array along ROI axes @@ -810,8 +814,11 @@ class ROI(GraphicsObject): #print " dshape", dShape ## Determine transform that maps ROI bounding box to image coordinates - tr = self.sceneTransform() * fn.invertQTransform(img.sceneTransform()) - + try: + tr = self.sceneTransform() * fn.invertQTransform(img.sceneTransform()) + except np.linalg.linalg.LinAlgError: + return None + ## Modify transform to scale from image coords to data coords #m = QtGui.QTransform() tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) @@ -1737,11 +1744,34 @@ class PolyLineROI(ROI): def shape(self): p = QtGui.QPainterPath() + if len(self.handles) == 0: + return p p.moveTo(self.handles[0]['item'].pos()) for i in range(len(self.handles)): p.lineTo(self.handles[i]['item'].pos()) p.lineTo(self.handles[0]['item'].pos()) - return p + return p + + def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): + sl = self.getArraySlice(data, img, axes=(0,1)) + if sl is None: + return None + sliced = data[sl[0]] + im = QtGui.QImage(sliced.shape[axes[0]], sliced.shape[axes[1]], QtGui.QImage.Format_ARGB32) + im.fill(0x0) + p = QtGui.QPainter(im) + p.setPen(fn.mkPen(None)) + p.setBrush(fn.mkBrush('w')) + p.setTransform(self.itemTransform(img)[0]) + bounds = self.mapRectToItem(img, self.boundingRect()) + p.translate(-bounds.left(), -bounds.top()) + p.drawPath(self.shape()) + p.end() + mask = fn.imageToArray(im)[:,:,0].astype(float) / 255. + shape = [1] * data.ndim + shape[axes[0]] = sliced.shape[axes[0]] + shape[axes[1]] = sliced.shape[axes[1]] + return sliced * mask class LineSegmentROI(ROI): @@ -1845,8 +1875,8 @@ class SpiralROI(ROI): #for h in self.handles: #h['pos'] = h['item'].pos()/self.state['size'][0] - def stateChanged(self): - ROI.stateChanged(self) + def stateChanged(self, finish=True): + ROI.stateChanged(self, finish=finish) if len(self.handles) > 1: self.path = QtGui.QPainterPath() h0 = Point(self.handles[0]['item'].pos()).length() From ad20103ccca980dbef23987bfed5406dc66d91a0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 17 Mar 2013 14:26:23 -0400 Subject: [PATCH 2/4] Check for length=0 arrays when using autoVisible --- pyqtgraph/graphicsItems/PlotCurveItem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index c5a8ec3f..d707a347 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -112,7 +112,10 @@ class PlotCurveItem(GraphicsObject): if orthoRange is not None: mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) d = d[mask] - d2 = d2[mask] + #d2 = d2[mask] + + if len(d) == 0: + return (None, None) ## Get min/max (or percentiles) of the requested data range if frac >= 1.0: From 87f45186d885aa6ece60debfb38996082af40c2c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 17 Mar 2013 15:16:27 -0400 Subject: [PATCH 3/4] bugfix: prevent auto-range disabling when dragging with one mouse axis diabled --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 68 +++++++++++++++++----- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 654a12c9..b7785a9d 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -467,12 +467,32 @@ class ViewBox(GraphicsWidget): padding = 0.02 return padding - def scaleBy(self, s, center=None): + def scaleBy(self, s=None, center=None, x=None, y=None): """ Scale by *s* around given center point (or center of view). - *s* may be a Point or tuple (x, y) + *s* may be a Point or tuple (x, y). + + Optionally, x or y may be specified individually. This allows the other + axis to be left unaffected (note that using a scale factor of 1.0 may + cause slight changes due to floating-point error). """ - scale = Point(s) + if s is not None: + scale = Point(s) + else: + scale = [x, y] + + affect = [True, True] + if scale[0] is None and scale[1] is None: + return + elif scale[0] is None: + affect[0] = False + scale[0] = 1.0 + elif scale[1] is None: + affect[1] = False + scale[1] = 1.0 + + scale = Point(scale) + if self.state['aspectLocked'] is not False: scale[0] = self.state['aspectLocked'] * scale[1] @@ -481,21 +501,37 @@ class ViewBox(GraphicsWidget): center = Point(vr.center()) else: center = Point(center) + tl = center + (vr.topLeft()-center) * scale br = center + (vr.bottomRight()-center) * scale - self.setRange(QtCore.QRectF(tl, br), padding=0) - def translateBy(self, t): + if not affect[0]: + self.setYRange(tl.y(), br.y(), padding=0) + elif not affect[1]: + self.setXRange(tl.x(), br.x(), padding=0) + else: + self.setRange(QtCore.QRectF(tl, br), padding=0) + + def translateBy(self, t=None, x=None, y=None): """ Translate the view by *t*, which may be a Point or tuple (x, y). - """ - t = Point(t) - #if viewCoords: ## scale from pixels - #o = self.mapToView(Point(0,0)) - #t = self.mapToView(t) - o + Alternately, x or y may be specified independently, leaving the other + axis unchanged (note that using a translation of 0 may still cause + small changes due to floating-point error). + """ vr = self.targetRect() - self.setRange(vr.translated(t), padding=0) + if t is not None: + t = Point(t) + self.setRange(vr.translated(t), padding=0) + elif x is not None: + x1, x2 = vr.left()+x, vr.right()+x + self.setXRange(x1, x2, padding=0) + elif y is not None: + y1, y2 = vr.top()+y, vr.bottom()+y + self.setYRange(y1, y2, padding=0) + + def enableAutoRange(self, axis=None, enable=True): """ @@ -935,7 +971,10 @@ class ViewBox(GraphicsWidget): else: tr = dif*mask tr = self.mapToView(tr) - self.mapToView(Point(0,0)) - self.translateBy(tr) + x = tr.x() if mask[0] == 1 else None + y = tr.y() if mask[1] == 1 else None + + self.translateBy(x=x, y=y) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) elif ev.button() & QtCore.Qt.RightButton: #print "vb.rightDrag" @@ -950,8 +989,11 @@ class ViewBox(GraphicsWidget): tr = self.childGroup.transform() tr = fn.invertQTransform(tr) + x = s[0] if mask[0] == 1 else None + y = s[1] if mask[1] == 1 else None + center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton))) - self.scaleBy(s, center) + self.scaleBy(x=x, y=y, center=center) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) def keyPressEvent(self, ev): From 4716a841175cdf0ee22c346cfd8ca660f26add7d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 19 Mar 2013 11:49:10 -0400 Subject: [PATCH 4/4] AxisItem bugfix: corrected x-linked view update behavior Added MultiplePlotAxes example --- examples/MultiplePlotAxes.py | 67 +++++++++++++++++++++++++++++ pyqtgraph/graphicsItems/AxisItem.py | 13 +++--- 2 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 examples/MultiplePlotAxes.py diff --git a/examples/MultiplePlotAxes.py b/examples/MultiplePlotAxes.py new file mode 100644 index 00000000..75e0c680 --- /dev/null +++ b/examples/MultiplePlotAxes.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +""" +Demonstrates a way to put multiple axes around a single plot. + +(This will eventually become a built-in feature of PlotItem) + +""" +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 + +pg.mkQApp() + +pw = pg.PlotWidget() +pw.show() +pw.setWindowTitle('pyqtgraph example: MultiplePlotAxes') +p1 = pw.plotItem +p1.setLabels(left='axis 1') + +## create a new ViewBox, link the right axis to its coordinate system +p2 = pg.ViewBox() +p1.showAxis('right') +p1.scene().addItem(p2) +p1.getAxis('right').linkToView(p2) +p2.setXLink(p1) +p1.getAxis('right').setLabel('axis2', color='#0000ff') + +## create third ViewBox. +## this time we need to create a new axis as well. +p3 = pg.ViewBox() +ax3 = pg.AxisItem('right') +p1.layout.addItem(ax3, 2, 3) +p1.scene().addItem(p3) +ax3.linkToView(p3) +p3.setXLink(p1) +ax3.setZValue(-10000) +ax3.setLabel('axis 3', color='#ff0000') + + +## Handle view resizing +def updateViews(): + ## view has resized; update auxiliary views to match + global p1, p2, p3 + p2.setGeometry(p1.vb.sceneBoundingRect()) + p3.setGeometry(p1.vb.sceneBoundingRect()) + + ## need to re-update linked axes since this was called + ## incorrectly while views had different shapes. + ## (probably this should be handled in ViewBox.resizeEvent) + p2.linkedViewChanged(p1.vb, p2.XAxis) + p3.linkedViewChanged(p1.vb, p3.XAxis) + +updateViews() +p1.vb.sigResized.connect(updateViews) + + +p1.plot([1,2,4,8,16,32]) +p2.addItem(pg.PlotCurveItem([10,20,40,80,40,20], pen='b')) +p3.addItem(pg.PlotCurveItem([3200,1600,800,400,200,100], pen='r')) + +## 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_() diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 9d1684bd..7081f0ba 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -314,10 +314,13 @@ class AxisItem(GraphicsWidget): view.sigResized.connect(self.linkedViewChanged) def linkedViewChanged(self, view, newRange=None): - if self.orientation in ['right', 'left'] and view.yInverted(): + if self.orientation in ['right', 'left']: if newRange is None: newRange = view.viewRange()[1] - self.setRange(*newRange[::-1]) + if view.yInverted(): + self.setRange(*newRange[::-1]) + else: + self.setRange(*newRange) else: if newRange is None: newRange = view.viewRange()[0] @@ -330,18 +333,12 @@ class AxisItem(GraphicsWidget): ## 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.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 = rect.adjusted(min(0,self.tickLength), -15, 0, 15) elif self.orientation == 'top': - #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 = rect.adjusted(-15, min(0,self.tickLength), 15, 0) return rect else: