Fixed auto ranging for scatter plots
This commit is contained in:
parent
c570280d77
commit
7ac74e52e8
@ -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
|
|
||||||
|
## First collect all boundary information
|
||||||
range = [None, None]
|
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'):
|
if hasattr(item, 'dataBounds'):
|
||||||
bounds = self._itemBoundsCache.get(item, None)
|
#bounds = self._itemBoundsCache.get(item, None)
|
||||||
if bounds is None:
|
#if bounds is None:
|
||||||
if frac is None:
|
if frac is None:
|
||||||
frac = (1.0, 1.0)
|
frac = (1.0, 1.0)
|
||||||
xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
|
xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
|
||||||
yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1])
|
yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1])
|
||||||
if xr is None or xr == (None, None):
|
pxPad = 0 if not hasattr(item, 'pixelPadding') else item.pixelPadding()
|
||||||
useX = False
|
if xr is None or xr == (None, None):
|
||||||
xr = (0,0)
|
useX = False
|
||||||
if yr is None or yr == (None, None):
|
xr = (0,0)
|
||||||
useY = False
|
if yr is None or yr == (None, None):
|
||||||
yr = (0,0)
|
useY = False
|
||||||
|
yr = (0,0)
|
||||||
|
|
||||||
bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0])
|
bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0])
|
||||||
bounds = self.mapFromItemToView(item, bounds).boundingRect()
|
bounds = self.mapFromItemToView(item, bounds).boundingRect()
|
||||||
self._itemBoundsCache[item] = (bounds, useX, useY)
|
|
||||||
else:
|
if not any([useX, useY]):
|
||||||
bounds, useX, useY = bounds
|
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
|
## determine tentative new range
|
||||||
|
range = [None, None]
|
||||||
if useX != useY: ## != means xor
|
for bounds, useX, useY, px in itemBounds:
|
||||||
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.
|
|
||||||
|
|
||||||
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])]
|
||||||
@ -1071,7 +1077,32 @@ class ViewBox(GraphicsWidget):
|
|||||||
else:
|
else:
|
||||||
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