Fixed auto ranging for scatter plots

This commit is contained in:
Luke Campagnola 2013-02-12 19:15:45 -05:00
parent a80f150b86
commit 93a5753f5d
7 changed files with 115 additions and 66 deletions

View File

@ -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__':

View File

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

View File

@ -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'),

View File

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

View File

@ -127,7 +127,7 @@ class ErrorBarItem(GraphicsObject):
def boundingRect(self):
if self.path is None:
return QtCore.QRectF()
self.drawPath()
return self.path.boundingRect()

View File

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

View File

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