Skip to content

Commit ccffb72

Browse files
committed
Fix: Improve ellipse drawing for non-uniform aspect ratios
1 parent ead3dcf commit ccffb72

File tree

3 files changed

+89
-6
lines changed

3 files changed

+89
-6
lines changed

doc/release_notes/release_2.08.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Version 2.8 #
22

3+
## PlotPy Version 2.8.3 ##
4+
5+
🛠️ Bug fixes:
6+
7+
* Fixed circle/ellipse shape drawing with non-uniform aspect ratios:
8+
* Axes were not perpendicular and did not connect to the ellipse edge when plot aspect ratio differed from 1.0
9+
* Now uses parametric ellipse drawing that correctly handles non-perpendicular axes in pixel space
10+
* The ellipse properly passes through all four handle points regardless of aspect ratio or rotation
11+
312
## PlotPy Version 2.8.2 (2025-11-10) ##
413

514
🛠️ Bug fixes:

plotpy/items/shape/ellipse.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -258,14 +258,52 @@ def draw(
258258
painter.setRenderHint(QG.QPainter.Antialiasing)
259259
painter.setPen(pen)
260260
painter.setBrush(brush)
261+
262+
# Draw the axes lines connecting handles
261263
painter.drawLine(line0)
262264
painter.drawLine(line1)
263-
painter.save()
264-
painter.translate(rect.center())
265-
painter.rotate(-line0.angle())
266-
painter.translate(-rect.center())
267-
painter.drawEllipse(rect.toRect())
268-
painter.restore()
265+
266+
# For the ellipse, we need to handle non-uniform aspect ratios properly.
267+
# The ellipse should have its semi-axes aligned with line0 and line1 directions.
268+
# We use QPainterPath to draw an ellipse transformed to match the actual
269+
# geometry.
270+
center = line0.pointAt(0.5)
271+
272+
# Create the ellipse in a local coordinate system where line0 is horizontal
273+
# and line1 is vertical, then transform it to match the actual geometry
274+
path = QG.QPainterPath()
275+
276+
# Calculate the angle between line0 and line1 in pixel space
277+
# (they should be 90° in data space but may differ after transformation)
278+
angle0 = math.radians(line0.angle())
279+
angle1 = math.radians(line1.angle())
280+
281+
# Semi-axes lengths
282+
a = line0.length() / 2 # semi-major axis (along line0)
283+
b = line1.length() / 2 # semi-minor axis (along line1)
284+
285+
# Draw ellipse using parametric form, accounting for non-perpendicular axes
286+
# We sample points around the ellipse and create a path
287+
n_points = 72 # Number of points for smooth ellipse
288+
first_point = True
289+
for i in range(n_points + 1):
290+
t = 2 * math.pi * i / n_points
291+
# Parametric ellipse with potentially non-perpendicular axes
292+
# Point = center + cos(t) * a * dir0 + sin(t) * b * dir1
293+
dx = math.cos(t) * a * math.cos(angle0) + math.sin(t) * b * math.cos(angle1)
294+
dy = -math.cos(t) * a * math.sin(angle0) - math.sin(t) * b * math.sin(
295+
angle1
296+
)
297+
px = center.x() + dx
298+
py = center.y() + dy
299+
if first_point:
300+
path.moveTo(px, py)
301+
first_point = False
302+
else:
303+
path.lineTo(px, py)
304+
path.closeSubpath()
305+
painter.drawPath(path)
306+
269307
if symbol != QwtSymbol.NoSymbol:
270308
for i in range(points.size()):
271309
symbol.drawSymbol(painter, points[i].toPoint())
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Licensed under the terms of the BSD 3-Clause
4+
# (see plotpy/LICENSE for details)
5+
6+
"""Image and shapes with unlock aspect ratio test"""
7+
8+
# guitest: show
9+
10+
from guidata.qthelpers import qt_app_context
11+
12+
from plotpy.builder import make
13+
from plotpy.tests import data as ptd
14+
from plotpy.tests import vistools as ptv
15+
16+
17+
def test_image_aspect_ratio():
18+
"""Testing image and shapes with unlock aspect ratio"""
19+
with qt_app_context(exec_loop=True):
20+
data = ptd.gen_image4(500, 700)
21+
image = make.image(data)
22+
image.hide()
23+
circle = make.circle(200, 200, 500, 300)
24+
circle.select()
25+
txt = "This test is considered passed if<br>the ellipse is drawn properly."
26+
win = ptv.show_items(
27+
[image, circle, make.label(txt, "C", (0, 0), "C")],
28+
plot_type="image",
29+
wintitle=test_image_aspect_ratio.__doc__,
30+
lock_aspect_ratio=False,
31+
)
32+
win.plot_widget.plot.set_aspect_ratio(0.25)
33+
34+
35+
if __name__ == "__main__":
36+
test_image_aspect_ratio()

0 commit comments

Comments
 (0)