0

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()
Giorgio
  • 61
  • 1
  • 4
  • 1
    As the documentation [explains](https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/textitem.html), TextItem displays *unscaled text*. What you're asking is a bit complex and ambiguous, because pyqtgraph allows independent scaling (horizontal and vertical) that almost never keep the aspect ratio, so how should the "scaled" text base its size? And what size ratio reference would it use? – musicamante Jun 26 '22 at 16:08
  • I'm trying to figure out a way to have text inside the rectangles displayed in the code so that when you zoom in/zoom out the text's font changes Its y-lenght and Its x-lenght (in this code the x-axis Is fixed) in proportion (same amount) to the change of the y-lenght and x-lenght of the rectangles, so that the text stays centered in them and adapt Its size to the scale. – Giorgio Jun 26 '22 at 16:20
  • Well, the problem is quite complex, and I'm afraid you're underestimating it. Text laying out is difficult, and setting a scaled geometry make things much more complex. You have to consider: 1. the aspect ratio (width/height) of the rectangle; 2. the aspect ratio of each text fragment; 3. find the optimal size ratio between them in order to show the scaled text. Now, here's the thing: each letter has different sizes, so "l" is a "tall" character with a very small width, while "m" is the opposite. If you only consider the above, the result is that when rectangles are taller than wide the "l" -> – musicamante Jun 26 '22 at 20:46
  • -> will be pretty "big", while the "m" will be extremely small, since the rectangle width is narrow. To add even more complexity, text is always aligned to a "base line", so you would get inconsistent results with "a", "b" and "g": while "a" will be perfectly centered, "b" will be a bit lower than that base line, and "g" a bit higher. A quote character will be way below its intended position (on "top" of other characters). I'm not saying it's impossible, but it *is* very, **very** difficult, and you need to consider *a lot* of aspects that are not easy to figure out. Is it really worth it? – musicamante Jun 26 '22 at 20:46
  • To clarify what written above, I've created a "basic" implementation, and [this is the result](https://i.stack.imgur.com/bv4F0.png). As you can see, this is clearly not acceptable: the narrower letters (i, l) are quite big, while wider ones (m) are extremely small in comparison. The "c" also displays another issue: is it lower or uppercase? Suppose you have *both* "c" and "C": depending on the font and aspect ratio you could even get the lower "c" actually *bigger* than the upper "C". – musicamante Jun 26 '22 at 20:59
  • there won't be letters inside the final version, there will be only numbers; that said I'm open to suggestions, do you think I should continue this path and try to program It since number are easier than letters (all the same height and lenght) or do you have a better solution like a different object type (like a label)? @musicamante – Giorgio Jun 27 '22 at 13:21
  • 1
    *IF* you're sure that you'll always use numbers (or, at least) just *one* character, there could be a simpler workaround, which is to compute the maximum bounding rect of the *possible* characters (basically, numbers and ascii letters) when the *first* instance is created, in that way all characters will be properly placed. – musicamante Jun 27 '22 at 16:49

1 Answers1

1

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:

  • construct a QPainterPath;
  • add the letter to the path;
  • get the max() of that path width and the others;
  • get the minimum Y and maximum bottom of the bounding rectangle;
  • translate the path based on all other values computed above (in a separate loop);

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.

musicamante
  • 41,230
  • 6
  • 33
  • 58