More boundingRect / dataBounds bugfixes

This commit is contained in:
Luke Campagnola 2013-02-12 21:44:42 -05:00
parent 93a5753f5d
commit 9f55a27fdd
4 changed files with 149 additions and 93 deletions

View File

@ -70,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, 'pen': {'color': 'w', 'width': 8}, 'brush':pg.intColor(i*10+j, 100)}) spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'pen': {'color': 'w', 'width': 2}, '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)

View File

@ -93,7 +93,7 @@ class PlotCurveItem(GraphicsObject):
(x, y) = self.getData() (x, y) = self.getData()
if x is None or len(x) == 0: if x is None or len(x) == 0:
return (0, 0) return (None, None)
if ax == 0: if ax == 0:
d = x d = x
@ -102,20 +102,106 @@ class PlotCurveItem(GraphicsObject):
d = y d = y
d2 = x d2 = x
## If an orthogonal range is specified, mask the data now
if orthoRange is not None: if orthoRange is not None:
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask] d = d[mask]
d2 = d2[mask] d2 = d2[mask]
## Get min/max (or percentiles) of the requested data range
if frac >= 1.0: if frac >= 1.0:
b = (d.min(), d.max()) b = (d.min(), d.max())
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:
b = (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) b = (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
## adjust for fill level
if ax == 1 and self.opts['fillLevel'] is not None:
b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel']))
## Add pen width only if it is non-cosmetic.
pen = self.opts['pen']
spen = self.opts['shadowPen']
if not pen.isCosmetic():
b = (b[0] - pen.widthF()*0.7072, b[1] + pen.widthF()*0.7072)
if spen is not None and not spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen:
b = (b[0] - spen.widthF()*0.7072, b[1] + spen.widthF()*0.7072)
self._boundsCache[ax] = [(frac, orthoRange), b] self._boundsCache[ax] = [(frac, orthoRange), b]
return b return b
def pixelPadding(self):
pen = self.opts['pen']
spen = self.opts['shadowPen']
w = 0
if pen.isCosmetic():
w += pen.widthF()*0.7072
if spen is not None and spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen:
w = max(w, spen.widthF()*0.7072)
return w
def boundingRect(self):
if self._boundingRect is None:
(xmn, xmx) = self.dataBounds(ax=0)
(ymn, ymx) = self.dataBounds(ax=1)
if xmn is None:
return QtCore.QRectF()
px = py = 0.0
pxPad = self.pixelPadding()
if pxPad > 0:
# determine length of pixel in local x, y directions
px, py = self.pixelVectors()
px = 0 if px is None else px.length()
py = 0 if py is None else py.length()
# return bounds expanded by pixel size
px *= pxPad
py *= pxPad
#px += self._maxSpotWidth * 0.5
#py += self._maxSpotWidth * 0.5
self._boundingRect = QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)
return self._boundingRect
def viewTransformChanged(self):
self.invalidateBounds()
self.prepareGeometryChange()
#def boundingRect(self):
#if self._boundingRect is None:
#(x, y) = self.getData()
#if x is None or y is None or len(x) == 0 or len(y) == 0:
#return QtCore.QRectF()
#if self.opts['shadowPen'] is not None:
#lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
#else:
#lineWidth = (self.opts['pen'].width()+1)
#pixels = self.pixelVectors()
#if pixels == (None, None):
#pixels = [Point(0,0), Point(0,0)]
#xmin = x.min()
#xmax = x.max()
#ymin = y.min()
#ymax = y.max()
#if self.opts['fillLevel'] is not None:
#ymin = min(ymin, self.opts['fillLevel'])
#ymax = max(ymax, self.opts['fillLevel'])
#xmin -= pixels[0].x() * lineWidth
#xmax += pixels[0].x() * lineWidth
#ymin -= abs(pixels[1].y()) * lineWidth
#ymax += abs(pixels[1].y()) * lineWidth
#self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
#return self._boundingRect
def invalidateBounds(self): def invalidateBounds(self):
self._boundingRect = None self._boundingRect = None
@ -280,40 +366,6 @@ class PlotCurveItem(GraphicsObject):
return QtGui.QPainterPath() return QtGui.QPainterPath()
return self.path return self.path
def boundingRect(self):
if self._boundingRect is None:
(x, y) = self.getData()
if x is None or y is None or len(x) == 0 or len(y) == 0:
return QtCore.QRectF()
if self.opts['shadowPen'] is not None:
lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
else:
lineWidth = (self.opts['pen'].width()+1)
pixels = self.pixelVectors()
if pixels == (None, None):
pixels = [Point(0,0), Point(0,0)]
xmin = x.min()
xmax = x.max()
ymin = y.min()
ymax = y.max()
if self.opts['fillLevel'] is not None:
ymin = min(ymin, self.opts['fillLevel'])
ymax = max(ymax, self.opts['fillLevel'])
xmin -= pixels[0].x() * lineWidth
xmax += pixels[0].x() * lineWidth
ymin -= abs(pixels[1].y()) * lineWidth
ymax += abs(pixels[1].y()) * lineWidth
self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
return self._boundingRect
def paint(self, p, opt, widget): def paint(self, p, opt, widget):
prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True)
if self.xData is None: if self.xData is None:

View File

@ -471,33 +471,57 @@ class PlotDataItem(GraphicsObject):
and max) and max)
=============== ============================================================= =============== =============================================================
""" """
if frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
(x, y) = self.getData() range = [None, None]
if x is None or len(x) == 0: if self.curve.isVisible():
return None range = self.curve.dataBounds(ax, frac, orthoRange)
elif self.scatter.isVisible():
r2 = self.scatter.dataBounds(ax, frac, orthoRange)
range = [
r2[0] if range[0] is None else (range[0] if r2[0] is None else min(r2[0], range[0])),
r2[1] if range[1] is None else (range[1] if r2[1] is None else min(r2[1], range[1]))
]
return range
#if frac <= 0.0:
#raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
#(x, y) = self.getData()
#if x is None or len(x) == 0:
#return None
if ax == 0: #if ax == 0:
d = x #d = x
d2 = y #d2 = y
elif ax == 1: #elif ax == 1:
d = y #d = y
d2 = x #d2 = x
if orthoRange is not None: #if orthoRange is not None:
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) #mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask] #d = d[mask]
#d2 = d2[mask] ##d2 = d2[mask]
if len(d) > 0: #if len(d) > 0:
if frac >= 1.0: #if frac >= 1.0:
return (np.min(d), np.max(d)) #return (np.min(d), np.max(d))
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)))
else: #else:
return None #return None
def pixelPadding(self):
"""
Return the size in pixels that this item may draw beyond the values returned by dataBounds().
This method is called by ViewBox when auto-scaling.
"""
pad = 0
if self.curve.isVisible():
pad = max(pad, self.curve.pixelPadding())
elif self.scatter.isVisible():
pad = max(pad, self.scatter.pixelPadding())
return pad
def clear(self): def clear(self):
#for i in self.curves+self.scatters: #for i in self.curves+self.scatters:

View File

@ -60,7 +60,7 @@ def renderSymbol(symbol, size, pen, brush, device=None):
#return SymbolPixmapCache[key] #return SymbolPixmapCache[key]
## Render a spot with the given parameters to a pixmap ## Render a spot with the given parameters to a pixmap
penPxWidth = max(np.ceil(pen.width()), 1) penPxWidth = max(np.ceil(pen.widthF()), 1)
image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32) image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32)
image.fill(0) image.fill(0)
p = QtGui.QPainter(image) p = QtGui.QPainter(image)
@ -115,7 +115,7 @@ class SymbolAtlas(object):
symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush'] symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush']
pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen
brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush
key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color())) key = (symbol, size, fn.colorTuple(pen.color()), pen.widthF(), pen.style(), fn.colorTuple(brush.color()))
if key not in self.symbolMap: if key not in self.symbolMap:
newCoords = SymbolAtlas.SymbolCoords() newCoords = SymbolAtlas.SymbolCoords()
self.symbolMap[key] = newCoords self.symbolMap[key] = newCoords
@ -589,13 +589,13 @@ class ScatterPlotItem(GraphicsObject):
width = 0 width = 0
pxWidth = 0 pxWidth = 0
if self.opts['pxMode']: if self.opts['pxMode']:
pxWidth = size + pen.width() pxWidth = size + pen.widthF()
else: else:
width = size width = size
if pen.isCosmetic(): if pen.isCosmetic():
pxWidth += pen.width() pxWidth += pen.widthF()
else: else:
width += pen.width() width += pen.widthF()
self._maxSpotWidth = max(self._maxSpotWidth, width) self._maxSpotWidth = max(self._maxSpotWidth, width)
self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth) self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth)
self.bounds = [None, None] self.bounds = [None, None]
@ -629,20 +629,7 @@ class ScatterPlotItem(GraphicsObject):
d2 = d2[mask] d2 = d2[mask]
if frac >= 1.0: if frac >= 1.0:
## increase size of bounds based on spot size and pen width self.bounds[ax] = (d.min() - self._maxSpotWidth*0.7072, d.max() + self._maxSpotWidth*0.7072)
#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)
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))
@ -650,13 +637,7 @@ class ScatterPlotItem(GraphicsObject):
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): def pixelPadding(self):
return self._maxSpotPxWidth return self._maxSpotPxWidth*0.7072
#def defaultSpotPixmap(self):
### Return the default spot pixmap
#if self._spotPixmap is None:
#self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
#return self._spotPixmap
def boundingRect(self): def boundingRect(self):
(xmn, xmx) = self.dataBounds(ax=0) (xmn, xmx) = self.dataBounds(ax=0)
@ -669,17 +650,16 @@ class ScatterPlotItem(GraphicsObject):
ymx = 0 ymx = 0
px = py = 0.0 px = py = 0.0
if self._maxSpotPxWidth > 0: pxPad = self.pixelPadding()
if pxPad > 0:
# determine length of pixel in local x, y directions # determine length of pixel in local x, y directions
px, py = self.pixelVectors() px, py = self.pixelVectors()
px = 0 if px is None else px.length() * 0.5 px = 0 if px is None else px.length()
py = 0 if py is None else py.length() * 0.5 py = 0 if py is None else py.length()
# return bounds expanded by pixel size # return bounds expanded by pixel size
px *= self._maxSpotPxWidth px *= pxPad
py *= self._maxSpotPxWidth py *= pxPad
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) return QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)
def viewTransformChanged(self): def viewTransformChanged(self):