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 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__':
|
||||
|
@ -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)
|
||||
|
@ -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'),
|
||||
|
@ -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)
|
||||
|
||||
|
@ -127,7 +127,7 @@ class ErrorBarItem(GraphicsObject):
|
||||
|
||||
def boundingRect(self):
|
||||
if self.path is None:
|
||||
return QtCore.QRectF()
|
||||
self.drawPath()
|
||||
return self.path.boundingRect()
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user