Fixed auto ranging for scatter plots
This commit is contained in:
parent
a80f150b86
commit
93a5753f5d
@ -13,6 +13,8 @@ import numpy as np
|
|||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
pg.setConfigOptions(antialias=True)
|
||||||
|
|
||||||
x = np.arange(10)
|
x = np.arange(10)
|
||||||
y = np.arange(10) %3
|
y = np.arange(10) %3
|
||||||
top = np.linspace(1.0, 3.0, 10)
|
top = np.linspace(1.0, 3.0, 10)
|
||||||
@ -21,7 +23,7 @@ bottom = np.linspace(2, 0.5, 10)
|
|||||||
plt = pg.plot()
|
plt = pg.plot()
|
||||||
err = pg.ErrorBarItem(x=x, y=y, top=top, bottom=bottom, beam=0.5)
|
err = pg.ErrorBarItem(x=x, y=y, top=top, bottom=bottom, beam=0.5)
|
||||||
plt.addItem(err)
|
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.
|
## Start Qt event loop unless running in interactive mode or using pyside.
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -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)]
|
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)
|
s2.addPoints(spots)
|
||||||
w2.addItem(s2)
|
w2.addItem(s2)
|
||||||
w2.setRange(s2.boundingRect())
|
|
||||||
s2.sigClicked.connect(clicked)
|
s2.sigClicked.connect(clicked)
|
||||||
|
|
||||||
|
|
||||||
@ -71,7 +70,7 @@ s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to tr
|
|||||||
spots3 = []
|
spots3 = []
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
for j 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)
|
s3.addPoints(spots3)
|
||||||
w3.addItem(s3)
|
w3.addItem(s3)
|
||||||
s3.sigClicked.connect(clicked)
|
s3.sigClicked.connect(clicked)
|
||||||
|
@ -28,6 +28,7 @@ examples = OrderedDict([
|
|||||||
#('PlotItem', 'PlotItem.py'),
|
#('PlotItem', 'PlotItem.py'),
|
||||||
('IsocurveItem', 'isocurve.py'),
|
('IsocurveItem', 'isocurve.py'),
|
||||||
('GraphItem', 'GraphItem.py'),
|
('GraphItem', 'GraphItem.py'),
|
||||||
|
('ErrorBarItem', 'ErrorBarItem.py'),
|
||||||
('ImageItem - video', 'ImageItem.py'),
|
('ImageItem - video', 'ImageItem.py'),
|
||||||
('ImageItem - draw', 'Draw.py'),
|
('ImageItem - draw', 'Draw.py'),
|
||||||
('Region-of-Interest', 'ROIExamples.py'),
|
('Region-of-Interest', 'ROIExamples.py'),
|
||||||
|
@ -66,7 +66,7 @@ class Exporter(object):
|
|||||||
if selectedExt is not None:
|
if selectedExt is not None:
|
||||||
selectedExt = selectedExt.groups()[0].lower()
|
selectedExt = selectedExt.groups()[0].lower()
|
||||||
if ext != selectedExt:
|
if ext != selectedExt:
|
||||||
fileName = fileName + selectedExt
|
fileName = fileName + '.' + selectedExt.lstrip('.')
|
||||||
|
|
||||||
self.export(fileName=fileName, **self.fileDialog.opts)
|
self.export(fileName=fileName, **self.fileDialog.opts)
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ class ErrorBarItem(GraphicsObject):
|
|||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
if self.path is None:
|
if self.path is None:
|
||||||
return QtCore.QRectF()
|
self.drawPath()
|
||||||
return self.path.boundingRect()
|
return self.path.boundingRect()
|
||||||
|
|
||||||
|
|
@ -554,7 +554,6 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
#rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec))
|
#rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec))
|
||||||
if invalidate:
|
if invalidate:
|
||||||
self.invalidate()
|
self.invalidate()
|
||||||
self.informViewBoundsChanged()
|
|
||||||
|
|
||||||
def getSpotOpts(self, recs, scale=1.0):
|
def getSpotOpts(self, recs, scale=1.0):
|
||||||
if recs.ndim == 0:
|
if recs.ndim == 0:
|
||||||
@ -632,23 +631,26 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
if frac >= 1.0:
|
if frac >= 1.0:
|
||||||
## increase size of bounds based on spot size and pen width
|
## 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]
|
#px = self.pixelVectors()[ax]
|
||||||
if px is None:
|
#if px is None:
|
||||||
px = 0
|
#px = 0
|
||||||
else:
|
#else:
|
||||||
px = px.length()
|
#px = px.length()
|
||||||
minIndex = np.argmin(d)
|
#minIndex = np.argmin(d)
|
||||||
maxIndex = np.argmax(d)
|
#maxIndex = np.argmax(d)
|
||||||
minVal = d[minIndex]
|
#minVal = d[minIndex]
|
||||||
maxVal = d[maxIndex]
|
#maxVal = d[maxIndex]
|
||||||
spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth)
|
#spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth)
|
||||||
self.bounds[ax] = (minVal-spotSize, maxVal+spotSize)
|
#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]
|
return self.bounds[ax]
|
||||||
elif frac <= 0.0:
|
elif frac <= 0.0:
|
||||||
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
|
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
|
||||||
else:
|
else:
|
||||||
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
|
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
|
||||||
|
|
||||||
|
def pixelPadding(self):
|
||||||
|
return self._maxSpotPxWidth
|
||||||
|
|
||||||
#def defaultSpotPixmap(self):
|
#def defaultSpotPixmap(self):
|
||||||
### Return the default spot pixmap
|
### Return the default spot pixmap
|
||||||
@ -665,14 +667,26 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
if ymn is None or ymx is None:
|
if ymn is None or ymx is None:
|
||||||
ymn = 0
|
ymn = 0
|
||||||
ymx = 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):
|
def viewTransformChanged(self):
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
GraphicsObject.viewTransformChanged(self)
|
GraphicsObject.viewTransformChanged(self)
|
||||||
self.bounds = [None, None]
|
self.bounds = [None, None]
|
||||||
self.fragments = None
|
self.fragments = None
|
||||||
self.informViewBoundsChanged()
|
|
||||||
|
|
||||||
def generateFragments(self):
|
def generateFragments(self):
|
||||||
tr = self.deviceTransform()
|
tr = self.deviceTransform()
|
||||||
|
@ -297,12 +297,11 @@ class ViewBox(GraphicsWidget):
|
|||||||
|
|
||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
#self.setRange(self.range, padding=0)
|
#self.setRange(self.range, padding=0)
|
||||||
#self.updateAutoRange()
|
self.updateAutoRange()
|
||||||
self._itemBoundsCache.clear()
|
|
||||||
self.updateMatrix()
|
self.updateMatrix()
|
||||||
self.sigStateChanged.emit(self)
|
self.sigStateChanged.emit(self)
|
||||||
self.background.setRect(self.rect())
|
self.background.setRect(self.rect())
|
||||||
|
#self._itemBoundsCache.clear()
|
||||||
#self.linkedXChanged()
|
#self.linkedXChanged()
|
||||||
#self.linkedYChanged()
|
#self.linkedYChanged()
|
||||||
|
|
||||||
@ -730,8 +729,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
|
|
||||||
def itemBoundsChanged(self, item):
|
def itemBoundsChanged(self, item):
|
||||||
self._itemBoundsCache.pop(item, None)
|
self._itemBoundsCache.pop(item, None)
|
||||||
if item in self.addedItems:
|
self.updateAutoRange()
|
||||||
self.updateAutoRange()
|
|
||||||
|
|
||||||
def invertY(self, b=True):
|
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.
|
Values may be None if there are no specific bounds for an axis.
|
||||||
"""
|
"""
|
||||||
prof = debug.Profiler('updateAutoRange', disabled=True)
|
prof = debug.Profiler('updateAutoRange', disabled=True)
|
||||||
|
|
||||||
|
|
||||||
#items = self.allChildren()
|
|
||||||
items = self.addedItems
|
items = self.addedItems
|
||||||
|
|
||||||
#if item is None:
|
## measure pixel dimensions in view box
|
||||||
##print "children bounding rect:"
|
px, py = [v.length() if v is not None else 0 for v in self.childGroup.pixelVectors()]
|
||||||
#item = self.childGroup
|
|
||||||
|
|
||||||
range = [None, None]
|
|
||||||
|
|
||||||
|
## First collect all boundary information
|
||||||
|
itemBounds = []
|
||||||
for item in items:
|
for item in items:
|
||||||
if not item.isVisible():
|
if not item.isVisible():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
useX = True
|
useX = True
|
||||||
useY = 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 = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0])
|
if hasattr(item, 'dataBounds'):
|
||||||
bounds = self.mapFromItemToView(item, bounds).boundingRect()
|
#bounds = self._itemBoundsCache.get(item, None)
|
||||||
self._itemBoundsCache[item] = (bounds, useX, useY)
|
#if bounds is None:
|
||||||
else:
|
if frac is None:
|
||||||
bounds, useX, useY = bounds
|
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()
|
||||||
|
|
||||||
|
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:
|
else:
|
||||||
if int(item.flags() & item.ItemHasNoContents) > 0:
|
if int(item.flags() & item.ItemHasNoContents) > 0:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
bounds = item.boundingRect()
|
bounds = item.boundingRect()
|
||||||
bounds = self.mapFromItemToView(item, bounds).boundingRect()
|
bounds = self.mapFromItemToView(item, bounds).boundingRect()
|
||||||
|
itemBounds.append((bounds, True, True, 0))
|
||||||
|
|
||||||
prof.mark('1')
|
#print itemBounds
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
## determine tentative new range
|
||||||
|
range = [None, None]
|
||||||
|
for bounds, useX, useY, px in itemBounds:
|
||||||
if useY:
|
if useY:
|
||||||
if range[1] is not None:
|
if range[1] is not None:
|
||||||
range[1] = [min(bounds.top(), range[1][0]), max(bounds.bottom(), range[1][1])]
|
range[1] = [min(bounds.top(), range[1][0]), max(bounds.bottom(), range[1][1])]
|
||||||
@ -1072,6 +1078,31 @@ class ViewBox(GraphicsWidget):
|
|||||||
range[0] = [bounds.left(), bounds.right()]
|
range[0] = [bounds.left(), bounds.right()]
|
||||||
prof.mark('2')
|
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()
|
prof.finish()
|
||||||
return range
|
return range
|
||||||
|
|
||||||
@ -1089,6 +1120,8 @@ class ViewBox(GraphicsWidget):
|
|||||||
|
|
||||||
|
|
||||||
def updateMatrix(self, changed=None):
|
def updateMatrix(self, changed=None):
|
||||||
|
## Make the childGroup's transform match the requested range.
|
||||||
|
|
||||||
if changed is None:
|
if changed is None:
|
||||||
changed = [False, False]
|
changed = [False, False]
|
||||||
changed = list(changed)
|
changed = list(changed)
|
||||||
|
Loading…
Reference in New Issue
Block a user