pythonscalingpyqtgraphpyqt6qgraphicstextitem

TextItem scaling in pyqtgraph


I'm having a problem with the font scaling of TextItems in pyqtgraph, like you can see from the following code when I zoom in/zoom out in the main graph the font of the TextItems stays the same while I'm trying to make It scale in the same exact way (rate) of the QGraphicsRectItem. I've tried to look on all the forums I know but I haven't find an answer so I really hope someone has a solution for this.

import sys
import pyqtgraph as pg
from PyQt6.QtWidgets import QApplication, QGraphicsRectItem
from pyqtgraph.Qt import QtCore

app = QApplication(sys.argv)
view = pg.GraphicsView()
l = pg.GraphicsLayout()
view.setCentralItem(l)
view.show()
view.resize(800, 600)

p0 = l.addPlot(0, 0)
p0.showGrid(x=True, y=True, alpha=1.0)

# have no x-axis tickmark below the upper plot (coordinate 0,0)
# without these lines, there will be separate coordinate systems with a gap inbetween
ay0 = p0.getAxis('left')      # get handle to y-axis 0
ay0.setStyle(showValues=False)  # this will remove the tick labels and reduces gap b/w plots almost to zero
                                # there will be a double line separating the plot rows
# ay02 = p0.getAxis('right')
# ay02.setStyle(showValues=False)
p0.hideAxis('right')
ax02 = p0.getAxis('top')
ax02.setStyle(showValues=False)

p1 = l.addPlot(0, 1)


# p1.showGrid(x=True, y=True, alpha=1.0)

p1.setYLink(p0)

l.layout.setSpacing(0.5)
l.setContentsMargins(0., 0., 0., 0.)

p1.setFixedWidth(300)
# p1.setFixedHeight(h-451)

p1.setMouseEnabled(x=False)

# ay1 = p1.getAxis('left')
# ay1.setStyle(showValues=False)
ax12 = p1.getAxis('top')
ax12.setStyle(showValues=False)
# ax1 = p1.getAxis('bottom')
# ax1.setStyle(showValues=False)
p1.showAxis('right')
p1.hideAxis('left')
p1.setXRange(0, 6, padding=0)   # Then add others like 1 pip

# p1.getAxis('bottom').setTextPen('black')

board = ['123456',
         'abcdef',
         'ghilmn']


def draw_board(board2):
    for j, row in enumerate(board2):
        for i, cell in enumerate(row):
            rect_w = 1
            rect_h = 1
            r = QGraphicsRectItem(i, -j+2, rect_w, rect_h)
            r.setPen(pg.mkPen((0, 0, 0, 100)))
            r.setBrush(pg.mkBrush((50, 50, 200)))
            p1.addItem(r)

            t_up = pg.TextItem(cell, (255, 255, 255), anchor=(0, 0))
            t_up.setPos(i, -j+1+2)
            p1.addItem(t_up)


draw_board(board)

if __name__ == '__main__':
    if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
        QApplication.instance().exec()

Solution

  • Scaling of a text item is quite difficult, as you need to consider a constant aspect ratio of the base scale, and the problems related to the way fonts are positioned and drawn relative to the origin point.

    Assuming that the displayed text will always be a single character and that the characters used are standard ascii letters and numbers, the only possibility is to cycle through all possible characters, and create properly aligned paths for each of them.

    So, for every character:

    Then, you have to set a reference size for the letter (using the maximum width above and the font metrics' height) and get the aspect ratio for that size.

    The last part is implemented in the paint() function of the QGraphicsRectItem subclass, which is required to get the proper geometry of the item (if any transformation is applied to a parent item, the item will not know it), and get the maximum rectangle for the reference size based on the current rectangle size.

    class NumberRectItem(QGraphicsRectItem):
        textSize = None
        textPaths = {}
        textPath = None
        def __init__(self, x, y, width, height, letter=''):
            super().__init__(x, y, width, height)
            if letter:
                if not self.textPaths:
                    self._buildTextPaths()
                self.textPath = self.textPaths[letter]
    
        def _buildTextPaths(self):
            from string import ascii_letters, digits
            font = QApplication.font()
            fm = QFontMetricsF(font)
            maxWidth = 0
            minY = 1000
            maxY = 0
            for l in ascii_letters + digits:
                path = QPainterPath()
                path.addText(0, 0, font, l)
                br = path.boundingRect()
                maxWidth = max(maxWidth, br.width())
                minY = min(minY, br.y())
                maxY = max(maxY, br.bottom())
                self.textPaths[l] = path
            self.__class__.textSize = QSizeF(maxWidth, fm.height())
            self.__class__.textRatio = self.textSize.height() / self.textSize.width()
    
            middle = minY + (maxY - minY) / 2
            for path in self.textPaths.values():
                path.translate(
                    -path.boundingRect().center().x(), 
                    -middle)
    
        def paint(self, qp, opt, widget=None):
            super().paint(qp, opt, widget)
            if not self.textPath:
                return
            qp.save()
            qp.resetTransform()
            view = widget.parent()
            sceneRect = self.mapToScene(self.rect())
            viewRect = view.mapFromScene(sceneRect).boundingRect()
            rectSize = QSizeF(viewRect.size())
            newSize = self.textSize.scaled(rectSize, Qt.KeepAspectRatio)
            if newSize.width() == rectSize.width():
                # width is the maximum
                ratio = newSize.width() / self.textSize.width()
            else:
                ratio = newSize.height() / self.textSize.height()
            transform = QTransform().scale(ratio, ratio)
            path = transform.map(self.textPath)
    
            qp.setRenderHint(qp.Antialiasing)
            qp.setPen(Qt.NoPen)
            qp.setBrush(Qt.white)
            qp.drawPath(path.translated(viewRect.center()))
    
            qp.restore()
    
    
    def draw_board(board2):
        for j, row in enumerate(board2):
            for i, cell in enumerate(row):
                rect_w = 1
                rect_h = 1
                r = NumberRectItem(i, -j+2, rect_w, rect_h, letter=cell)
                r.setPen(pg.mkPen((150, 0, 0, 255)))
                r.setBrush(pg.mkBrush((50, 50, 200, 128)))
                p1.addItem(r)
    

    Note: for PyQt6 you need to use the full enum names: Qt.GlobalColor.white, etc.