python-3.xpyqt5pyqtgraphbounds

How to scale bounds to the Qpicture in pyqtgraph?


I'm trying to set the bounds of this PyQtGraph candlestick chart to properly zoom in so it views the candlesticks as something other than a tiny spec on the chart that I need to zoom in to. This works fine if the value of t (time) is a full integer, but if I use a value such as 1.00001, 1.00002, ect the candle sticks become a tiny dot and it does not automatically scale. I struggle to understand how bounding works due to lack of quality documentation however I think I get the jist of it enough to know that

QtCore.QRectF(self.picture.boundingRect())

should automatically scale the bounds to the picture that is drawn.

import pyqtgraph as pg
from PyQt5 import QtWidgets, QtCore, QtGui
from pyqtgraph import PlotWidget, plot, QtCore, QtGui

#------------------------------------------------------------------------------    

'''chart items'''
class CandlestickItem(pg.GraphicsObject):
    def __init__(self):
        pg.GraphicsObject.__init__(self)
        self.flagHasData = False

    def set_data(self, data):
        self.data = data 
        self.flagHasData = True
        self.generatePicture()
        self.informViewBoundsChanged()
        


    def generatePicture(self):
        self.picture = QtGui.QPicture()
        
        p = QtGui.QPainter(self.picture)
        p.setPen(pg.mkPen('w'))
        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(pg.mkBrush('r'))
            else:
                p.setBrush(pg.mkBrush('g'))
            p.drawRect(QtCore.QRectF(t-w, open, w*2, close-open))
        p.end()

    def paint(self, p, *args):
        if self.flagHasData:
            p.drawPicture(0, 0, self.picture)

    def boundingRect(self):
        return QtCore.QRectF(self.picture.boundingRect())
        
        
#------------------------------------------------------------------------------    

data = [  
    [1.00001, 1.00569 , 1.00577, 1.00560, 1.00578],
    [1.00002, 1.00577, 1.00571 , 1.00579, 1.00540],
]

        
app = QtWidgets.QApplication([])
item = CandlestickItem()
item.set_data(data)
plt = pg.plot()
plt.addItem(item)
plt.setWindowTitle('pyqtgraph example: customGraphicsItem')

#------------------------------------------------------------------------------    

if __name__ == '__main__':
    # window = Window()    
    import sys
    if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
        QtWidgets.QApplication.instance().exec_()

Can someone explain to me why it is not working? Would much appreciate it.


Solution

  • The boundingRect() of QPicture is a QRect, which is integer based. If you print out the result, even after converting it to a QRectF, you will see that it will be:

    QRect(1, 1, 1, 1)
    

    Which corresponds to the visible range in the view.

    This clearly won't work out, because all those rectangles are actually very small.

    The solution is to provide a rectangle that precisely maps the contents. To achieve so, you can create a temporary QPainterPath, add the lines and rectangles to it, and store the bounding rect of that path so that you can return it when the new picture is generated:

    class CandlestickItem(pg.GraphicsObject):
        _boundingRect = QtCore.QRectF()
        # ...
        def generatePicture(self):
            self.picture = QtGui.QPicture()
            
            path = QtGui.QPainterPath()
            p = QtGui.QPainter(self.picture)
            p.setPen(pg.mkPen('w'))
            w = (self.data[1][0] - self.data[0][0]) / 3.
            for (t, open, close, min, max) in self.data:
                line = QtCore.QLineF(t, min, t, max)
                path.moveTo(line.p1())
                path.lineTo(line.p2())
                p.drawLine(line)
    
                rect = QtCore.QRectF(t-w, open, w*2, close-open)
                path.addRect(rect)
    
                if open > close:
                    p.setBrush(pg.mkBrush('r'))
                else:
                    p.setBrush(pg.mkBrush('g'))
                p.drawRect(rect)
    
            p.end()
            self._boundingRect = path.boundingRect()
    
        def paint(self, p, *args):
            if self.flagHasData:
                p.drawPicture(0, 0, self.picture)
    
        def boundingRect(self):
            return self._boundingRect