Skip to content

Commit 36fffb6

Browse files
Merge pull request #114 from PlotPyStack/fix/113-scale-draw-geometry
fix(scale_widget): keep scale geometry when replacing the scale draw
2 parents c03f993 + 66d7362 commit 36fffb6

3 files changed

Lines changed: 123 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
- Python 3.10 or later is now required. Python 3.9 users must keep an earlier compatible release.
88

9+
### Bug fixes
10+
11+
- Fixed [Issue #113](https://github.com/PlotPyStack/PythonQwt/issues/113): after replacing the scale draw of an axis (`QwtPlot.setAxisScaleDraw`), the axis ticks could be misaligned with the grid lines and plot items, until the next scale division change (e.g. after panning or zooming): a replot did not fix it. The new scale draw was laid out before getting the position and length of the previous one, so that the axis reserved space for its end labels that the canvas did not take into account. The new scale draw now takes the geometry of the previous one, and the axis stays aligned with the canvas
12+
913
## Version 0.16.3
1014

1115
### New features

‎qwt/scale_widget.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import math
1717

18-
from qtpy.QtCore import QObject, QRectF, QSize, Qt, Signal
18+
from qtpy.QtCore import QObject, QPointF, QRectF, QSize, Qt, Signal
1919
from qtpy.QtGui import QPainter, QPalette
2020
from qtpy.QtWidgets import QSizePolicy, QStyle, QStyleOption, QWidget
2121

@@ -339,6 +339,11 @@ class destructor or the next call of `setScaleDraw()`.
339339
if sd.scaleMap().transformation():
340340
transform = sd.scaleMap().transformation().copy()
341341
scaleDraw.setTransformation(transform)
342+
# The border distance hint computed by `layoutScale` depends on the
343+
# scale geometry: without it, a zero-length scale gives the axis
344+
# border distances differing from those used by the canvas maps.
345+
scaleDraw.move(QPointF(sd.pos()))
346+
scaleDraw.setLength(sd.length())
342347
self.__data.scaleDraw = scaleDraw
343348
self.layoutScale()
344349

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Licensed under the terms of the Qwt License
4+
# (see LICENSE file for more details)
5+
6+
"""
7+
Test for issue 113: axis ticks misaligned with the canvas after replacing the
8+
scale draw.
9+
10+
``QwtScaleWidget.setScaleDraw`` passed the alignment, the scale division and
11+
the transformation of the previous scale draw to the new one, but not its
12+
position and length. The border distance hint was then computed from a scale
13+
of zero length, so the axis was laid out with border distances differing from
14+
the stored ones used by ``QwtPlot.canvasMap``: grid lines and plot items no
15+
longer matched the ticks, even after a replot, until the next scale division
16+
change.
17+
"""
18+
19+
import numpy as np
20+
import pytest
21+
from qtpy import QtWidgets as QW
22+
23+
from qwt import QwtPlot, QwtPlotCurve, QwtPlotGrid, QwtScaleDraw
24+
25+
# Scale bounds that are not major ticks: end labels do not overflow the scale
26+
# ends, so that the stored border distances are zero
27+
X_BOUNDS = (3.06e-7, 4.1e-7)
28+
Y_BOUNDS = (-1.05, 1.05)
29+
AXES = (QwtPlot.xBottom, QwtPlot.yLeft)
30+
AXIS_IDS = ("xBottom", "yLeft")
31+
32+
33+
def _ensure_app():
34+
# A live QApplication must exist before constructing any QWidget, otherwise
35+
# Qt aborts the process. Tests run in a shared interpreter, but no test
36+
# keeps a persistent Python reference to the application, so the singleton
37+
# may be garbage-collected between tests (observed on Linux/PyQt5 in CI).
38+
return QW.QApplication.instance() or QW.QApplication([])
39+
40+
41+
def _make_plot(app):
42+
plot = QwtPlot()
43+
x = np.linspace(*X_BOUNDS, 1000)
44+
QwtPlotCurve.make(x, np.tanh((x - 3.1e-7) * 1e8), plot=plot)
45+
QwtPlotGrid().attach(plot)
46+
plot.setAxisScale(QwtPlot.xBottom, *X_BOUNDS)
47+
plot.setAxisScale(QwtPlot.yLeft, *Y_BOUNDS)
48+
plot.resize(600, 400)
49+
plot.show()
50+
plot.replot()
51+
app.processEvents()
52+
return plot
53+
54+
55+
def _scale_ends(plot, axis_id):
56+
"""Return the ends of the axis scale and of the canvas map, in canvas
57+
coordinates"""
58+
widget = plot.axisWidget(axis_id)
59+
if axis_id in (QwtPlot.yLeft, QwtPlot.yRight):
60+
offset = widget.y() - plot.canvas().y()
61+
else:
62+
offset = widget.x() - plot.canvas().x()
63+
smap = widget.scaleDraw().scaleMap()
64+
cmap = plot.canvasMap(axis_id)
65+
scale_ends = sorted((smap.p1() + offset, smap.p2() + offset))
66+
canvas_ends = sorted((cmap.p1(), cmap.p2()))
67+
return scale_ends, canvas_ends
68+
69+
70+
@pytest.mark.parametrize("axis_id", AXES, ids=AXIS_IDS)
71+
def test_replaced_scale_draw_keeps_geometry(axis_id):
72+
"""The new scale draw takes the position and length of the previous one."""
73+
app = _ensure_app() # keep a reference alive for the duration of the test
74+
plot = _make_plot(app)
75+
76+
old_draw = plot.axisScaleDraw(axis_id)
77+
pos, length = old_draw.pos(), old_draw.length()
78+
new_draw = QwtScaleDraw()
79+
plot.setAxisScaleDraw(axis_id, new_draw)
80+
81+
assert plot.axisScaleDraw(axis_id) is new_draw
82+
assert new_draw.pos() == pos
83+
assert new_draw.length() == length
84+
85+
plot.close()
86+
del app
87+
88+
89+
@pytest.mark.parametrize("axis_id", AXES, ids=AXIS_IDS)
90+
def test_axis_aligned_with_canvas_after_scale_draw_change(axis_id):
91+
"""Axis ticks stay aligned with the canvas after replacing the scale draw."""
92+
app = _ensure_app() # keep a reference alive for the duration of the test
93+
plot = _make_plot(app)
94+
95+
scale_ends, canvas_ends = _scale_ends(plot, axis_id)
96+
assert scale_ends == pytest.approx(canvas_ends, abs=0.5)
97+
98+
plot.setAxisScaleDraw(axis_id, QwtScaleDraw())
99+
plot.replot()
100+
app.processEvents()
101+
102+
scale_ends, canvas_ends = _scale_ends(plot, axis_id)
103+
assert scale_ends == pytest.approx(canvas_ends, abs=0.5)
104+
105+
plot.close()
106+
del app
107+
108+
109+
if __name__ == "__main__":
110+
for axis in AXES:
111+
test_replaced_scale_draw_keeps_geometry(axis)
112+
test_axis_aligned_with_canvas_after_scale_draw_change(axis)
113+
print("OK")

0 commit comments

Comments
 (0)