|
| 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