From 9f55a27fdd6db90e13202b210814497e12cf0170 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 12 Feb 2013 21:44:42 -0500 Subject: [PATCH] More boundingRect / dataBounds bugfixes --- examples/ScatterPlot.py | 2 +- pyqtgraph/graphicsItems/PlotCurveItem.py | 124 +++++++++++++++------ pyqtgraph/graphicsItems/PlotDataItem.py | 70 ++++++++---- pyqtgraph/graphicsItems/ScatterPlotItem.py | 46 +++----- 4 files changed, 149 insertions(+), 93 deletions(-) diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 2a15164f..e72e2631 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -70,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, '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) w3.addItem(s3) s3.sigClicked.connect(clicked) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index b321714a..35a38ae7 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -93,7 +93,7 @@ class PlotCurveItem(GraphicsObject): (x, y) = self.getData() if x is None or len(x) == 0: - return (0, 0) + return (None, None) if ax == 0: d = x @@ -102,20 +102,106 @@ class PlotCurveItem(GraphicsObject): d = y d2 = x + ## If an orthogonal range is specified, mask the data now if orthoRange is not None: mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) d = d[mask] d2 = d2[mask] - + ## Get min/max (or percentiles) of the requested data range if frac >= 1.0: b = (d.min(), d.max()) elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: 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] 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): self._boundingRect = None @@ -280,40 +366,6 @@ class PlotCurveItem(GraphicsObject): return QtGui.QPainterPath() 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): prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) if self.xData is None: diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 83afbbfe..c0d5f2f3 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -471,33 +471,57 @@ class PlotDataItem(GraphicsObject): and max) =============== ============================================================= """ - 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 + range = [None, None] + if self.curve.isVisible(): + 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: - d = x - d2 = y - elif ax == 1: - d = y - d2 = x + #if ax == 0: + #d = x + #d2 = y + #elif ax == 1: + #d = y + #d2 = x - if orthoRange is not None: - mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) - d = d[mask] - #d2 = d2[mask] + #if orthoRange is not None: + #mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) + #d = d[mask] + ##d2 = d2[mask] - if len(d) > 0: - if frac >= 1.0: - return (np.min(d), np.max(d)) - else: - return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) - else: - return None - + #if len(d) > 0: + #if frac >= 1.0: + #return (np.min(d), np.max(d)) + #else: + #return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + #else: + #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): #for i in self.curves+self.scatters: diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 93653869..18d9ebf3 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -60,7 +60,7 @@ def renderSymbol(symbol, size, pen, brush, device=None): #return SymbolPixmapCache[key] ## 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.fill(0) p = QtGui.QPainter(image) @@ -115,7 +115,7 @@ class SymbolAtlas(object): symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush'] pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen 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: newCoords = SymbolAtlas.SymbolCoords() self.symbolMap[key] = newCoords @@ -589,13 +589,13 @@ class ScatterPlotItem(GraphicsObject): width = 0 pxWidth = 0 if self.opts['pxMode']: - pxWidth = size + pen.width() + pxWidth = size + pen.widthF() else: width = size if pen.isCosmetic(): - pxWidth += pen.width() + pxWidth += pen.widthF() else: - width += pen.width() + width += pen.widthF() self._maxSpotWidth = max(self._maxSpotWidth, width) self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth) self.bounds = [None, None] @@ -629,20 +629,7 @@ class ScatterPlotItem(GraphicsObject): d2 = d2[mask] 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) - self.bounds[ax] = (d.min() - 0.5*self._maxSpotWidth, d.max() + 0.5*self._maxSpotWidth) + self.bounds[ax] = (d.min() - self._maxSpotWidth*0.7072, d.max() + self._maxSpotWidth*0.7072) return self.bounds[ax] elif frac <= 0.0: 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))) def pixelPadding(self): - return self._maxSpotPxWidth - - #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 + return self._maxSpotPxWidth*0.7072 def boundingRect(self): (xmn, xmx) = self.dataBounds(ax=0) @@ -669,17 +650,16 @@ class ScatterPlotItem(GraphicsObject): ymx = 0 px = py = 0.0 - if self._maxSpotPxWidth > 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() * 0.5 - py = 0 if py is None else py.length() * 0.5 + 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 *= self._maxSpotPxWidth - py *= self._maxSpotPxWidth - px += self._maxSpotWidth * 0.5 - py += self._maxSpotWidth * 0.5 + px *= pxPad + py *= pxPad return QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn) def viewTransformChanged(self):