From 93a5753f5d3e06c5dfb1e6c29ffa0d9b8484e319 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 12 Feb 2013 19:15:45 -0500 Subject: [PATCH] Fixed auto ranging for scatter plots --- examples/ErrorBarItem.py | 4 +- examples/ScatterPlot.py | 3 +- examples/__main__.py | 1 + pyqtgraph/exporters/Exporter.py | 2 +- pyqtgraph/graphicsItems/ErrorBarItem.py | 2 +- pyqtgraph/graphicsItems/ScatterPlotItem.py | 42 ++++--- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 127 +++++++++++++-------- 7 files changed, 115 insertions(+), 66 deletions(-) diff --git a/examples/ErrorBarItem.py b/examples/ErrorBarItem.py index 9c1bbf1e..816e8474 100644 --- a/examples/ErrorBarItem.py +++ b/examples/ErrorBarItem.py @@ -13,6 +13,8 @@ import numpy as np import pyqtgraph as pg import numpy as np +pg.setConfigOptions(antialias=True) + x = np.arange(10) y = np.arange(10) %3 top = np.linspace(1.0, 3.0, 10) @@ -21,7 +23,7 @@ bottom = np.linspace(2, 0.5, 10) plt = pg.plot() err = pg.ErrorBarItem(x=x, y=y, top=top, bottom=bottom, beam=0.5) plt.addItem(err) -plt.plot(x, y, symbol='o') +plt.plot(x, y, symbol='o', pen={'color': 0.8, 'width': 2}) ## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 03e849ad..2a15164f 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -59,7 +59,6 @@ pos = np.random.normal(size=(2,n), scale=1e-5) spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)] s2.addPoints(spots) w2.addItem(s2) -w2.setRange(s2.boundingRect()) s2.sigClicked.connect(clicked) @@ -71,7 +70,7 @@ s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to tr spots3 = [] for i in range(10): for j in range(10): - spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'brush':pg.intColor(i*10+j, 100)}) + spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'pen': {'color': 'w', 'width': 8}, 'brush':pg.intColor(i*10+j, 100)}) s3.addPoints(spots3) w3.addItem(s3) s3.sigClicked.connect(clicked) diff --git a/examples/__main__.py b/examples/__main__.py index 80f23f7d..096edba0 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -28,6 +28,7 @@ examples = OrderedDict([ #('PlotItem', 'PlotItem.py'), ('IsocurveItem', 'isocurve.py'), ('GraphItem', 'GraphItem.py'), + ('ErrorBarItem', 'ErrorBarItem.py'), ('ImageItem - video', 'ImageItem.py'), ('ImageItem - draw', 'Draw.py'), ('Region-of-Interest', 'ROIExamples.py'), diff --git a/pyqtgraph/exporters/Exporter.py b/pyqtgraph/exporters/Exporter.py index 81930670..f5a93088 100644 --- a/pyqtgraph/exporters/Exporter.py +++ b/pyqtgraph/exporters/Exporter.py @@ -66,7 +66,7 @@ class Exporter(object): if selectedExt is not None: selectedExt = selectedExt.groups()[0].lower() if ext != selectedExt: - fileName = fileName + selectedExt + fileName = fileName + '.' + selectedExt.lstrip('.') self.export(fileName=fileName, **self.fileDialog.opts) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index ccb38774..656b9e2e 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -127,7 +127,7 @@ class ErrorBarItem(GraphicsObject): def boundingRect(self): if self.path is None: - return QtCore.QRectF() + self.drawPath() return self.path.boundingRect() \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 7c204479..93653869 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -554,7 +554,6 @@ class ScatterPlotItem(GraphicsObject): #rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec)) if invalidate: self.invalidate() - self.informViewBoundsChanged() def getSpotOpts(self, recs, scale=1.0): if recs.ndim == 0: @@ -632,23 +631,26 @@ 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.pixelVectors()[ax] - if px is None: - px = 0 - else: - px = px.length() - minIndex = np.argmin(d) - maxIndex = np.argmax(d) - minVal = d[minIndex] - maxVal = d[maxIndex] - spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth) - self.bounds[ax] = (minVal-spotSize, maxVal+spotSize) + #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] + #maxVal = d[maxIndex] + #spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth) + #self.bounds[ax] = (minVal-spotSize, maxVal+spotSize) + self.bounds[ax] = (d.min() - 0.5*self._maxSpotWidth, d.max() + 0.5*self._maxSpotWidth) return self.bounds[ax] elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + def pixelPadding(self): + return self._maxSpotPxWidth #def defaultSpotPixmap(self): ### Return the default spot pixmap @@ -665,14 +667,26 @@ class ScatterPlotItem(GraphicsObject): if ymn is None or ymx is None: ymn = 0 ymx = 0 - return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) + + px = py = 0.0 + if self._maxSpotPxWidth > 0: + # determine length of pixel in local x, y directions + px, py = self.pixelVectors() + px = 0 if px is None else px.length() * 0.5 + py = 0 if py is None else py.length() * 0.5 + + # return bounds expanded by pixel size + px *= self._maxSpotPxWidth + py *= self._maxSpotPxWidth + px += self._maxSpotWidth * 0.5 + py += self._maxSpotWidth * 0.5 + return QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn) def viewTransformChanged(self): self.prepareGeometryChange() GraphicsObject.viewTransformChanged(self) self.bounds = [None, None] self.fragments = None - self.informViewBoundsChanged() def generateFragments(self): tr = self.deviceTransform() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 44f98e77..ce1d61f9 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -297,12 +297,11 @@ class ViewBox(GraphicsWidget): def resizeEvent(self, ev): #self.setRange(self.range, padding=0) - #self.updateAutoRange() - self._itemBoundsCache.clear() + self.updateAutoRange() self.updateMatrix() self.sigStateChanged.emit(self) self.background.setRect(self.rect()) - + #self._itemBoundsCache.clear() #self.linkedXChanged() #self.linkedYChanged() @@ -730,8 +729,7 @@ class ViewBox(GraphicsWidget): def itemBoundsChanged(self, item): self._itemBoundsCache.pop(item, None) - if item in self.addedItems: - self.updateAutoRange() + self.updateAutoRange() def invertY(self, b=True): """ @@ -1003,63 +1001,71 @@ class ViewBox(GraphicsWidget): Values may be None if there are no specific bounds for an axis. """ prof = debug.Profiler('updateAutoRange', disabled=True) - - - #items = self.allChildren() items = self.addedItems - #if item is None: - ##print "children bounding rect:" - #item = self.childGroup - - range = [None, None] - + ## measure pixel dimensions in view box + px, py = [v.length() if v is not None else 0 for v in self.childGroup.pixelVectors()] + + ## First collect all boundary information + itemBounds = [] for item in items: if not item.isVisible(): continue useX = True useY = True + if hasattr(item, 'dataBounds'): - bounds = self._itemBoundsCache.get(item, None) - if bounds is None: - if frac is None: - frac = (1.0, 1.0) - xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) - yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) - if xr is None or xr == (None, None): - useX = False - xr = (0,0) - if yr is None or yr == (None, None): - useY = False - yr = (0,0) + #bounds = self._itemBoundsCache.get(item, None) + #if bounds is None: + if frac is None: + frac = (1.0, 1.0) + xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) + yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) + pxPad = 0 if not hasattr(item, 'pixelPadding') else item.pixelPadding() + if xr is None or xr == (None, None): + useX = False + xr = (0,0) + if yr is None or yr == (None, None): + useY = False + yr = (0,0) - bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0]) - bounds = self.mapFromItemToView(item, bounds).boundingRect() - self._itemBoundsCache[item] = (bounds, useX, useY) - else: - bounds, useX, useY = bounds + bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0]) + bounds = self.mapFromItemToView(item, bounds).boundingRect() + + if not any([useX, useY]): + continue + + ## If we are ignoring only one axis, we need to check for rotations + if useX != useY: ## != means xor + ang = round(item.transformAngle()) + if ang == 0 or ang == 180: + pass + elif ang == 90 or ang == 270: + useX, useY = useY, useX + else: + ## Item is rotated at non-orthogonal angle, ignore bounds entirely. + ## Not really sure what is the expected behavior in this case. + continue ## need to check for item rotations and decide how best to apply this boundary. + + + itemBounds.append((bounds, useX, useY, pxPad)) + #self._itemBoundsCache[item] = (bounds, useX, useY) + #else: + #bounds, useX, useY = bounds else: if int(item.flags() & item.ItemHasNoContents) > 0: continue else: bounds = item.boundingRect() bounds = self.mapFromItemToView(item, bounds).boundingRect() - - prof.mark('1') - - if not any([useX, useY]): - continue - - if useX != useY: ## != means xor - ang = item.transformAngle() - if ang == 0 or ang == 180: - pass - elif ang == 90 or ang == 270: - useX, useY = useY, useX - else: - continue ## need to check for item rotations and decide how best to apply this boundary. - + itemBounds.append((bounds, True, True, 0)) + + #print itemBounds + + ## determine tentative new range + range = [None, None] + for bounds, useX, useY, px in itemBounds: if useY: if range[1] is not None: range[1] = [min(bounds.top(), range[1][0]), max(bounds.bottom(), range[1][1])] @@ -1071,7 +1077,32 @@ class ViewBox(GraphicsWidget): else: range[0] = [bounds.left(), bounds.right()] prof.mark('2') - + + #print "range", range + + ## Now expand any bounds that have a pixel margin + ## This must be done _after_ we have a good estimate of the new range + ## to ensure that the pixel size is roughly accurate. + w = self.width() + h = self.height() + #print "w:", w, "h:", h + if w > 0 and range[0] is not None: + pxSize = (range[0][1] - range[0][0]) / w + for bounds, useX, useY, px in itemBounds: + if px == 0 or not useX: + continue + range[0][0] = min(range[0][0], bounds.left() - px*pxSize) + range[0][1] = max(range[0][1], bounds.right() + px*pxSize) + if h > 0 and range[1] is not None: + pxSize = (range[1][1] - range[1][0]) / h + for bounds, useX, useY, px in itemBounds: + if px == 0 or not useY: + continue + range[1][0] = min(range[1][0], bounds.top() - px*pxSize) + range[1][1] = max(range[1][1], bounds.bottom() + px*pxSize) + + #print "final range", range + prof.finish() return range @@ -1089,6 +1120,8 @@ class ViewBox(GraphicsWidget): def updateMatrix(self, changed=None): + ## Make the childGroup's transform match the requested range. + if changed is None: changed = [False, False] changed = list(changed)