diff --git a/datalab/gui/plothandler.py b/datalab/gui/plothandler.py index 0ee5f204..40abc318 100644 --- a/datalab/gui/plothandler.py +++ b/datalab/gui/plothandler.py @@ -37,7 +37,7 @@ from plotpy.items import CurveItem, GridItem, LegendBoxItem, MaskedXYImageItem from plotpy.plot import PlotOptions from qtpy import QtWidgets as QW -from qwt import QwtScaleDraw +from qwt import QwtDateTimeScaleDraw, QwtScaleDraw from sigima.objects import ImageObj, SignalObj, TypeObj from datalab.adapters_metadata import GeometryAdapter, TableAdapter @@ -725,8 +725,12 @@ def refresh_plot( if has_datetime and datetime_format is not None: self.plot.set_axis_datetime("bottom", format=datetime_format) else: - # Restore default scale draw (remove datetime formatting) - self.plot.setAxisScaleDraw(self.plot.get_axis_id("bottom"), QwtScaleDraw()) + # Restore default scale draw only when leaving the datetime mode: + # replacing it on every refresh could misalign the axis ticks with + # the grid and the curves (see Issue #457) + axis_id = self.plot.get_axis_id("bottom") + if isinstance(self.plot.axisScaleDraw(axis_id), QwtDateTimeScaleDraw): + self.plot.setAxisScaleDraw(axis_id, QwtScaleDraw()) class ImagePlotHandler(BasePlotHandler[ImageObj, MaskedXYImageItem]): diff --git a/datalab/tests/features/signal/xaxis_scale_draw_unit_test.py b/datalab/tests/features/signal/xaxis_scale_draw_unit_test.py new file mode 100644 index 00000000..d4e781b8 --- /dev/null +++ b/datalab/tests/features/signal/xaxis_scale_draw_unit_test.py @@ -0,0 +1,102 @@ +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. + +""" +X axis scale draw unit tests +---------------------------- + +The X axis of the signal view keeps its scale draw when the view is refreshed, so +that its ticks stay aligned with the grid and the curves (the scale draw is only +replaced when leaving the date/time mode). +""" + +# guitest: show + +from __future__ import annotations + +from datetime import datetime, timedelta + +import numpy as np +import pytest +from plotpy.plot import BasePlot +from qwt import QwtDateTimeScaleDraw +from sigima.objects import SignalObj, create_signal + +from datalab.tests import datalab_test_app_context + +# X bounds which are not graduations of the axis once autoscaled (as for the +# 'step.npy' test file): end labels do not overflow the axis ends +X_MIN, X_MAX = 3.1e-7, 4.08e-7 + + +def _create_step_signal() -> SignalObj: + """Create a signal whose X axis bounds are not graduations""" + x = np.linspace(X_MIN, X_MAX, 500) + return create_signal("Step", x, np.tanh((x - 3.15e-7) * 1e8)) + + +def _create_datetime_signal() -> SignalObj: + """Create a signal with a date/time X axis""" + start = datetime(2025, 1, 1) + timestamps = [start + timedelta(seconds=i) for i in range(100)] + obj = create_signal("Date/time", np.arange(100.0), np.sin(np.arange(100.0) / 10)) + obj.set_x_from_datetime(timestamps, unit="s") + return obj + + +def _x_axis_ends(plot: BasePlot) -> tuple[list[float], list[float]]: + """Return the ends of the X axis scale and of the X canvas map, in canvas + coordinates""" + widget = plot.axisWidget(plot.xBottom) + offset = widget.x() - plot.canvas().x() + smap = widget.scaleDraw().scaleMap() + cmap = plot.canvasMap(plot.xBottom) + return ( + sorted((smap.p1() + offset, smap.p2() + offset)), + sorted((cmap.p1(), cmap.p2())), + ) + + +def test_refresh_keeps_x_scale_draw() -> None: + """Refreshing the signal view keeps the scale draw of the X axis""" + with datalab_test_app_context(console=False, exec_loop=False) as win: + panel = win.signalpanel + plot = panel.plothandler.plot + panel.add_object(_create_step_signal()) + scale_draw = plot.axisScaleDraw(plot.xBottom) + panel.manual_refresh() + assert plot.axisScaleDraw(plot.xBottom) is scale_draw + panel.add_object(_create_step_signal()) + assert plot.axisScaleDraw(plot.xBottom) is scale_draw + + +def test_x_axis_aligned_with_canvas() -> None: + """The X axis ticks are aligned with the grid and the curves after loading""" + with datalab_test_app_context(console=False, exec_loop=False) as win: + panel = win.signalpanel + plot = panel.plothandler.plot + panel.add_object(_create_step_signal()) + scale_ends, canvas_ends = _x_axis_ends(plot) + assert scale_ends == pytest.approx(canvas_ends, abs=0.5) + panel.manual_refresh() + scale_ends, canvas_ends = _x_axis_ends(plot) + assert scale_ends == pytest.approx(canvas_ends, abs=0.5) + + +def test_datetime_x_axis_restored() -> None: + """The date/time X axis formatting is removed when showing regular signals""" + with datalab_test_app_context(console=False, exec_loop=False) as win: + panel = win.signalpanel + plot = panel.plothandler.plot + panel.add_object(_create_datetime_signal()) + assert isinstance(plot.axisScaleDraw(plot.xBottom), QwtDateTimeScaleDraw) + panel.add_object(_create_step_signal()) + scale_draw = plot.axisScaleDraw(plot.xBottom) + assert not isinstance(scale_draw, QwtDateTimeScaleDraw) + panel.manual_refresh() + assert plot.axisScaleDraw(plot.xBottom) is scale_draw + + +if __name__ == "__main__": + test_refresh_keeps_x_scale_draw() + test_x_axis_aligned_with_canvas() + test_datetime_x_axis_restored() diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po index 83fc9ad1..8cdc95b8 100644 --- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po +++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po @@ -495,6 +495,9 @@ msgstr "Correction du sous-échantillonnage automatique des signaux (**Préfére msgid "Fixed changes of the auto downsampling and line width settings only applying to the curves of the selected signals: the other curves kept the former settings, even once selected again. These settings now apply to all curves as soon as the Settings dialog is closed" msgstr "Correction des modifications des préférences de sous-échantillonnage automatique et d'épaisseur de trait qui ne s'appliquaient qu'aux courbes des signaux sélectionnés : les autres courbes conservaient les anciennes préférences, même une fois sélectionnées à nouveau. Ces préférences s'appliquent désormais à toutes les courbes dès la fermeture de la boîte de dialogue des préférences" +msgid "Fixed the X axis ticks being shifted relative to the vertical grid lines and the curves after opening a signal or changing the selection, for signals whose X axis does not start and end on a graduation (e.g., `step.npy` from the Sigima test data): the shift remained until the view was panned or zoomed. The X axis now stays aligned with the grid and the curves (fixes [Issue #457](https://github.com/DataLab-Platform/DataLab/issues/457))" +msgstr "Correction des graduations de l'axe X qui étaient décalées par rapport aux lignes verticales de la grille et aux courbes après l'ouverture d'un signal ou un changement de sélection, pour les signaux dont l'axe X ne commence et ne se termine pas sur une graduation (par exemple, `step.npy` des données de test de Sigima) : le décalage persistait jusqu'à un déplacement ou un zoom de la vue. L'axe X reste désormais aligné avec la grille et les courbes (corrige [Issue #457](https://github.com/DataLab-Platform/DataLab/issues/457))" + msgid "**HDF5 import:**" msgstr "**Import HDF5 :**" diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index 6c42d920..0f2f26a7 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -216,6 +216,7 @@ See the [HDF5 browser guide](../features/common/h5browser.rst) for scientific ob * Fixed auto downsampling of signals (**Settings > Visualization > Signals**) only starting above 5 times the configured maximum number of points: with the default value of 100,000 points, signals of a few hundred thousand points were drawn in full, which slowed down the display. Signals with more points than the maximum are now downsampled so that at most this number of points is drawn, and the Settings dialog and the documentation now give the actual default value (100,000 instead of 10,000) (fixes [Issue #455](https://github.com/DataLab-Platform/DataLab/issues/455)) * Fixed changes of the auto downsampling and line width settings only applying to the curves of the selected signals: the other curves kept the former settings, even once selected again. These settings now apply to all curves as soon as the Settings dialog is closed +* Fixed the X axis ticks being shifted relative to the vertical grid lines and the curves after opening a signal or changing the selection, for signals whose X axis does not start and end on a graduation (e.g., `step.npy` from the Sigima test data): the shift remained until the view was panned or zoomed. The X axis now stays aligned with the grid and the curves (fixes [Issue #457](https://github.com/DataLab-Platform/DataLab/issues/457)) **HDF5 import:**