BarGraphItem: implement dataBounds and pixelPadding#2565
BarGraphItem: implement dataBounds and pixelPadding#2565j9ac9k merged 1 commit intopyqtgraph:masterfrom
Conversation
|
For a easier comparison of the various ways of computing the bounding rectangle, a standalone script that plots them all together. import pyqtgraph as pg
from pyqtgraph import QtCore, QtGui, QtWidgets
class CandlestickItem(pg.GraphicsObject):
def __init__(self, data):
pg.GraphicsObject.__init__(self)
self.data = data ## data must have fields: time, open, close, min, max
self.generatePicture()
def generatePicture(self):
## pre-computing a QPicture object allows paint() to run much more quickly,
## rather than re-drawing the shapes every time.
dataBounds = QtCore.QRectF()
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
pen = pg.mkPen('w', width=6)
p.setPen(pen)
brush_pos = pg.mkBrush('r')
brush_neg = pg.mkBrush('g')
w = (self.data[1][0] - self.data[0][0]) / 3.
for (t, open, close, min, max) in self.data:
p.drawLine(QtCore.QPointF(t, min), QtCore.QPointF(t, max))
if open > close:
p.setBrush(brush_pos)
else:
p.setBrush(brush_neg)
p.drawRect(QtCore.QRectF(t-w, open, w*2, close-open))
dataBounds |= QtCore.QRectF(t-w, min, w*2, max-min)
p.end()
self._dataBounds = dataBounds
self._pixelPadding = pen.widthF()*0.7072
def paint(self, p, *args):
p.drawPicture(0, 0, self.picture)
def boundingRect(self):
if self.picture is None:
self.generatePicture()
return QtCore.QRectF(self.picture.boundingRect())
class ItemX(CandlestickItem):
# provide boundingRect that takes into account pen width
def boundingRect(self):
if self.picture is None:
self.generatePicture()
px = py = 0.0
pxPad = self._pixelPadding
if pxPad > 0:
# determine length of pixel in local x, y directions
px, py = self.pixelVectors()
try:
px = 0 if px is None else px.length()
except OverflowError:
px = 0
try:
py = 0 if py is None else py.length()
except OverflowError:
py = 0
# return bounds expanded by pixel size
px *= pxPad
py *= pxPad
boundingRect = self._dataBounds.adjusted(-px, -py, px, py)
return boundingRect
class ItemY(ItemX):
# also provide dataBounds and pixelPadding
def dataBounds(self, ax, frac=1.0, orthoRange=None):
if self.picture is None:
self.generatePicture()
br = self._dataBounds
if ax == 0:
return [br.left(), br.right()]
else:
return [br.top(), br.bottom()]
def pixelPadding(self):
if self.picture is None:
self.generatePicture()
return self._pixelPadding
class ObjectBounds(QtWidgets.QGraphicsItem):
def paint(self, painter, *args):
pen = QtGui.QPen(QtCore.Qt.GlobalColor.red, 0, QtCore.Qt.PenStyle.DashLine)
rect = self.boundingRect()
painter.setPen(pen)
painter.drawRect(rect)
# print(rect)
def boundingRect(self):
return self.parentItem().boundingRect()
data = [ ## fields are (time, open, close, min, max).
(1., 10, 13, 5, 15),
(2., 13, 17, 9, 20),
(3., 17, 14, 11, 23),
(4., 14, 15, 5, 19),
(5., 15, 9, 8, 22),
(6., 9, 15, 8, 16),
]
pg.mkQApp()
glw = pg.GraphicsLayoutWidget()
glw.show()
items = [CandlestickItem(data), ItemX(data), ItemY(data)]
rects = []
for idx, item in enumerate(items):
plt = glw.addPlot(row=idx, col=0)
plt.addItem(item)
rects.append(ObjectBounds(item))
pg.exec() |
|
I have a test case here where (on Windows) the implementation of #2561 and this PR have different behavior. With #2561 and on Qt6, the viewbox is not tightly sized on the initial plot. Pressing the "A" button will make it tightly sized. Even minimizing and restoring the window will make it tightly sized. With this PR, the viewbox is tightly sized on the initial plot. I am not sure if this is a bug of pyqtgraph. This test-case also shows another deficiency of using import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
class ObjectBounds(QtWidgets.QGraphicsItem):
def paint(self, painter, *args):
pen = QtGui.QPen(QtCore.Qt.GlobalColor.red, 0, QtCore.Qt.PenStyle.DashLine)
rect = self.boundingRect()
painter.setPen(pen)
painter.drawRect(rect)
print(rect)
def boundingRect(self):
return self.parentItem().boundingRect()
pg.mkQApp()
pw = pg.PlotWidget()
pw.show()
x = np.linspace(0, 1, 100)
y = np.sin(2*np.pi*1.0*x)
pen = pg.mkPen('w', width=1)
bgi = pg.BarGraphItem(x=x, width=x[1]-x[0], height=y, pen=pen, brush=(0,0,255,150))
pw.addItem(bgi)
rect = ObjectBounds(bgi)
pg.exec()As far as I can tell, this is what happened: |
385e7e9 to
02014b4
Compare
and implement boundingRect in terms of dataBounds and pixelPadding
02014b4 to
b2de5fc
Compare
|
If you have a hidpi screen, you will be able to see that the bounding rectangle has a small gap, compared to a device pixel ratio 1 screen. (This effect is more obvious with thicker line widths.) |
|
I randomly encountered a need for this exact functionality in """
Returns the range occupied by the data (along a specific axis) in this item.
This method is called by :class:`ViewBox` when auto-scaling.
=============== ====================================================================
**Arguments:**
ax (0 or 1) the axis for which to return this item's data range
frac (float 0.0-1.0) Specifies what fraction of the total data
range to return. By default, the entire range is returned.
This allows the :class:`ViewBox` to ignore large spikes in the data
when auto-scaling.
orthoRange ([min,max] or None) Specifies that only the data within the
given range (orthogonal to *ax*) should me measured when
returning the data range. (For example, a ViewBox might ask
what is the y-range of all data with x-values between min
and max)
=============== ====================================================================
"""I had some trouble trying to figure out exactly what they were supposed to do from reading methods that didn't implement their respective functionality. Another potentially useful resource is the Would it be possible to write some of this functionality in a generic way that can be reused across different classes? I think it would be useful for any item that uses |
I don't understand how The scope that I hope to achieve here in this PR is to implement a simple enough replacement for current uses of "Simple" here means that we don't handle things like non-finites or overflow. |
|
There is one example https://github.com/pyqtgraph/pyqtgraph/blob/master/pyqtgraph/examples/PlotAutoRange.py that exercises the There are only two |
👀 that's ...absolutely fantastic. The script you added, here is the output on macOS (this is with PyQt 6.4) This PR looks great @pijyoi I'm 👍🏻 on merging ... curious if there are any other potential issues we would like to have a closer look, but I don't want to hold off on this PR to handle things like non-finite values and such... |
|
hi @pijyoi thanks again for this PR, this looks good to me. Thanks for diving into this tricky issue, I'm sure it wasn't easy to sort out! |


Alternate implementation of #2561.
Implements methods
dataBoundsandpixelPaddingwhich are used byViewBox.One difference that I noticed between this implementation and #2561 is that this implementation takes less iterations to stabilize on Windows. (7 vs 3).