So I'm using PyQt5 to do 2D rendering for an LED light show creator app. The light shows are graphs where the $Y$-axis is LED brightness level and the $X$-axis is usually time.

I have a zoom feature in my "view" class that zooms the entire scene in or out depending on user input. When you do this, by default, everything in the scene shrinks or explodes just like you're coming closer to or moving farther away from some structures on land and you're at some vertical distance in the air right above the structures.

The zoom feature is needed in case there are extra long light shows in time and the user would like to view the whole graph at once, for example.

I've already gotten the $X$ and $Y$-axes to scale properly, and this was easier than my current problem, since of course the $X$,$Y$-axes are in parallel with the used 2D coordinate system.

It wasn't a simple matter of calling axesItem.scale(1/sx, 1/sy)("naive") on the axes where $sx, sy$ is the scaling of the view port in the respective directions. I had to individually address each object making up the drawing algorithm of the axes and perform a custom scale to each, so that the end result is correct.

I tried the naive scaling again on the polyline item (what makes up the graph), and that does not work either. So i need to scale the polyline.lineWidth variable that is used in the painting algorithm, instead. However the polyline segments maybe at various angles different from $0$ or $90$ degrees. So given a view scaling of $sx, sy$, what is the formula for scaling the width of a line segment, so that the width alone is perfectly preserved in the eye of the user?

How it should look at 1:1 zoom (default): How it should look at 1:1 zoom (default)

How it looks with naive scaling, zoomed out 9 times: How it looks with naive scaling, zoomed out 9 times.


1 Answer 1


Let one segment of the polyline run from $P$ to $Q$. Then our width vector $\bar{w}$ has vertical component $w \sin \theta$ and horizontal component $w \cos \theta$. We'll want to apply the scaling componentwise to $\bar{w}$ so that $s \cdot \bar{w} = (\dfrac{w}{sx} \cos \theta, \ \dfrac{w}{sy} \sin \theta)$. The length of $\bar{w}$ equals the line width we should draw $PQ$ with $= w' = \sqrt{(\dfrac{w}{sx} \cos\theta)^2 + (\dfrac{w}{sy}\sin\theta)^2}$.

Thus we can either store an array of line segment widths and update the widths with this formula or recalculate this formula each time we draw every line segment. I'll code the second one, since Qt only calls repaint when it has to such as line segment move, in which case we'd have to recalculate any way.

Picture of the situation:

enter image description here

$\theta = \text{atan2}(PQ_y/PQ_x)$

$PQ \equiv Q - P$


Got the scale components reversed in the formula, $sx$ should be applied to the $Y$-component of $\bar{w}$, and vice versa.

The relevant code is now:

def paint(self, painter, item, widget):
    GraphicsItem.paint(self, painter, item, widget)
    painter.setPen(QPen(QBrush(self.lineColor), self.lineWidth))
    sx = self.scalex
    sy = self.scaley
    for k in range(0, len(self.offspring)-1):
        P = self.offspring[k].centerPos
        Q = self.offspring[k+1].centerPos
        PQ = Q - P
        t = atan2(PQ.y() / PQ.x())
        w = self.lineWidth
        w = sqrt((w*cos(t)/sy)**2 + (w*sin(t)/sx)**2)
        pen = painter.pen()
        painter.drawLine(P, Q)

def viewScaleXY(self, sx, sy):
    self.scalex *= sx
    self.scaley *= sy

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.