diff --git a/datalab/gui/main.py b/datalab/gui/main.py index 565ca05a..25cbcf01 100644 --- a/datalab/gui/main.py +++ b/datalab/gui/main.py @@ -2797,6 +2797,7 @@ def __edit_settings(self) -> None: # pylint: disable=too-many-branches,too-many sigima_options.fft_shift_enabled.set(Conf.fft_shift_enabled.get()) sigima_options.auto_normalize_kernel.set(Conf.auto_normalize_kernel.get()) refresh_signal_panel = refresh_image_panel = False + update_signal_items = False # Handling changes to shape/marker parameters s_view_result_param = ( @@ -2858,7 +2859,9 @@ def __edit_settings(self) -> None: # pylint: disable=too-many-branches,too-many if isinstance(widget, DockablePlotWidget): widget.update_toolbar_position() if option.startswith(("sig_autodownsampling", "sig_linewidth")): - refresh_signal_panel = True + # Signal display settings apply to the curves of all signals, not + # only to those of the selected signals + update_signal_items = True if option == "sig_autoscale_margin_percent": # Update signal plot widget autoscale margin sig_margin = Conf.sig_autoscale_margin_percent.get() @@ -2898,6 +2901,8 @@ def __edit_settings(self) -> None: # pylint: disable=too-many-branches,too-many self.imagepanel.update_metadata_view_settings() if option == "ima_aspect_ratio_1_1": refresh_image_panel = True + if update_signal_items: + self.signalpanel.plothandler.update_existing_items() if refresh_signal_panel: self.signalpanel.manual_refresh() if refresh_image_panel: diff --git a/datalab/gui/plothandler.py b/datalab/gui/plothandler.py index 75e97a19..0ee5f204 100644 --- a/datalab/gui/plothandler.py +++ b/datalab/gui/plothandler.py @@ -333,6 +333,24 @@ def update_item_on_plot(self, oid: str, just_show: bool = False) -> None: item.param.label = self.panel.mainwindow.render_object_title(obj.title) item.param.update_item(item) + def update_existing_items(self) -> None: + """Update all existing plot items from their objects and current settings + + Unlike :py:meth:`refresh_plot`, the visibility of the plot items and the + shape items are left unchanged. This applies a change of display settings + (e.g. signal auto downsampling or line width) to the plot items of all + objects, and not only to those of the selected objects. + """ + auto_replot = self.plot.autoReplot() + self.plot.setAutoReplot(False) + try: + with block_signals(self.plot): + for oid in self.get_existing_oids(): + self.update_item_on_plot(oid) + finally: + self.plot.setAutoReplot(auto_replot) + self.plot.replot() + def set_auto_refresh(self, auto_refresh: bool) -> None: """Set auto refresh mode. diff --git a/datalab/gui/settings.py b/datalab/gui/settings.py index 21f132dc..d1e5f76a 100644 --- a/datalab/gui/settings.py +++ b/datalab/gui/settings.py @@ -419,9 +419,12 @@ class ViewSettings(gds.DataSet): ).set_prop("display", store=_prop_ads) sig_autodownsampling_maxpoints = gds.IntItem( _("Downsampling max points"), - default=10000, + default=100000, min=1000, - help=_("Maximum number of points for downsampling"), + help=_( + "Maximum number of points displayed for a signal: signals with more " + "points are downsampled for display only (data are not modified)" + ), ).set_prop("display", active=_prop_ads) sig_autoscale_margin_percent = gds.FloatItem( _("Autoscale margin"), diff --git a/datalab/locale/fr/LC_MESSAGES/datalab.po b/datalab/locale/fr/LC_MESSAGES/datalab.po index 6a2c3024..a7b9130b 100644 --- a/datalab/locale/fr/LC_MESSAGES/datalab.po +++ b/datalab/locale/fr/LC_MESSAGES/datalab.po @@ -3944,8 +3944,8 @@ msgstr "Utiliser l'auto-sous-échantillonnage pour les signaux volumineux" msgid "Downsampling max points" msgstr "Nombre max. de points" -msgid "Maximum number of points for downsampling" -msgstr "Nombre maximum de points pour le sous-échantillonnage" +msgid "Maximum number of points displayed for a signal: signals with more points are downsampled for display only (data are not modified)" +msgstr "Nombre maximal de points affichés pour un signal : les signaux comportant plus de points sont sous-échantillonnés pour l'affichage uniquement (les données ne sont pas modifiées)" msgid "Autoscale margin" msgstr "Marge de zoom automatique" diff --git a/datalab/tests/features/signal/autodownsampling_unit_test.py b/datalab/tests/features/signal/autodownsampling_unit_test.py new file mode 100644 index 00000000..525700ee --- /dev/null +++ b/datalab/tests/features/signal/autodownsampling_unit_test.py @@ -0,0 +1,179 @@ +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. + +""" +Signal auto downsampling unit tests +----------------------------------- + +Signal display settings (auto downsampling, line width) are applied to the curves of +all signals when they are changed in the Settings dialog, and not only to the curves +of the selected signals. +""" + +# guitest: show + +from __future__ import annotations + +import numpy as np +import pytest +from plotpy.items import CurveItem +from sigima.objects import SignalObj, create_signal + +from datalab.adapters_plotpy import create_adapter_from_object +from datalab.config import Conf +from datalab.gui import main as main_module +from datalab.gui.main import DLMainWindow +from datalab.gui.panel.signal import SignalPanel +from datalab.gui.settings import ViewSettings +from datalab.objectmodel import get_uuid +from datalab.tests import datalab_test_app_context + +#: Number of points of the test signals +SIZES = (1_000, 30_000, 300_000) + + +def _create_signals(sizes: tuple[int, ...] = SIZES) -> list[SignalObj]: + """Create test signals with given numbers of points""" + signals = [] + for size in sizes: + x = np.linspace(0.0, 1.0, size) + signals.append(create_signal(f"{size} points", x, np.sin(2 * np.pi * 50 * x))) + return signals + + +def _add_signals(panel: SignalPanel, signals: list[SignalObj]) -> None: + """Add signals to the panel, create their curves, then select the first one, + so that the curves of the other signals exist but are hidden""" + panel.add_objects(signals, set_current=False) + panel.objview.select_objects(signals) + panel.objview.select_objects(signals[:1]) + + +def _item(panel: SignalPanel, obj: SignalObj) -> CurveItem: + """Return the curve item of a signal""" + return panel.plothandler[get_uuid(obj)] + + +def _edit_settings( + win: DLMainWindow, monkeypatch: pytest.MonkeyPatch, **options +) -> None: + """Change settings as the user does with the Settings dialog + + Args: + win: DataLab main window + monkeypatch: pytest monkeypatch fixture + options: option names and their new values + """ + + def fake_edit_settings(_parent) -> list[str]: + for name, value in options.items(): + getattr(Conf, name).set(value) + return list(options) + + monkeypatch.setattr(main_module, "edit_settings", fake_edit_settings) + win.settings_action.trigger() + + +def _check_downsampling( + panel: SignalPanel, signals: list[SignalObj], maxpoints: int | None +) -> None: + """Check the number of points drawn for each signal + + Args: + panel: signal panel + signals: signals to check + maxpoints: maximum number of points to draw (None if auto downsampling is + disabled) + """ + for obj in signals: + item = _item(panel, obj) + size = obj.x.size + if maxpoints is not None and size > maxpoints: + assert item.param.use_dsamp, obj.title + # At most 2 more points with min/max downsampling (PlotPy >= 2.12) + assert item.dataSize() <= maxpoints + 2, obj.title + else: + assert item.dataSize() == size, obj.title + + +def _visibility(panel: SignalPanel, signals: list[SignalObj]) -> list[bool]: + """Return the visibility of the curves of signals""" + return [_item(panel, obj).isVisible() for obj in signals] + + +def test_settings_default_maxpoints() -> None: + """The Settings dialog shows the actual default maximum number of points""" + assert ViewSettings().sig_autodownsampling_maxpoints == ( + Conf.sig_autodownsampling_maxpoints.get() + ) + + +def test_downsampling_settings_apply_to_all_curves( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Auto downsampling settings are applied to the curves of all signals""" + with datalab_test_app_context(console=False, exec_loop=False) as win: + panel = win.signalpanel + signals = _create_signals() + _add_signals(panel, signals) + maxpoints = Conf.sig_autodownsampling_maxpoints.get() + _check_downsampling(panel, signals, maxpoints) + visibility = _visibility(panel, signals) + assert visibility == [True, False, False] + + _edit_settings(win, monkeypatch, sig_autodownsampling_maxpoints=10_000) + _check_downsampling(panel, signals, 10_000) + assert _visibility(panel, signals) == visibility + + _edit_settings(win, monkeypatch, sig_autodownsampling=False) + _check_downsampling(panel, signals, None) + + _edit_settings(win, monkeypatch, sig_autodownsampling=True) + _check_downsampling(panel, signals, 10_000) + assert _visibility(panel, signals) == visibility + + +def test_line_width_applies_to_all_curves(monkeypatch: pytest.MonkeyPatch) -> None: + """Line width settings are applied to the curves of all signals""" + with datalab_test_app_context(console=False, exec_loop=False) as win: + panel = win.signalpanel + signals = _create_signals((100, 200, 300)) + _add_signals(panel, signals) + _edit_settings(win, monkeypatch, sig_linewidth=2.0) + for obj in signals: + item = _item(panel, obj) + assert item.param.line.width == 2.0, obj.title + assert item.pen().widthF() == 2.0, obj.title + + +def test_settings_update_keeps_shapes(monkeypatch: pytest.MonkeyPatch) -> None: + """Applying signal display settings neither duplicates nor shows shape items""" + with datalab_test_app_context(console=False, exec_loop=False) as win: + panel = win.signalpanel + signals = _create_signals() + _add_signals(panel, signals) + with Conf.show_result_dialog.context(False): + panel.processor.run_feature("stats") + plot = panel.plothandler.plot + nb_items = len(plot.get_items()) + visibility = _visibility(panel, signals) + _edit_settings(win, monkeypatch, sig_autodownsampling_maxpoints=10_000) + assert len(plot.get_items()) == nb_items + assert _visibility(panel, signals) == visibility + + +def test_dialog_items_use_current_settings(monkeypatch: pytest.MonkeyPatch) -> None: + """Plot items made from existing curves (e.g. for dialogs) use the current + auto downsampling settings""" + with datalab_test_app_context(console=False, exec_loop=False) as win: + panel = win.signalpanel + signals = _create_signals() + _add_signals(panel, signals) + _edit_settings(win, monkeypatch, sig_autodownsampling_maxpoints=10_000) + obj = signals[-1] + item = create_adapter_from_object(obj).make_item(update_from=_item(panel, obj)) + assert item.param.use_dsamp + assert item.dataSize() <= 10_000 + 2 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/doc/features/common/settings.rst b/doc/features/common/settings.rst index 2796661e..531ad45f 100644 --- a/doc/features/common/settings.rst +++ b/doc/features/common/settings.rst @@ -264,10 +264,13 @@ The Signals sub-tab contains settings specific to signal visualizations: **Use auto downsampling** Enable automatic downsampling for large signals to improve performance and - visualization clarity. + visualization clarity. Downsampling only affects the display: signal data and + processing are not modified. **Downsampling max points** - Maximum number of points to display when downsampling is enabled (default: 10,000). + Maximum number of points displayed for a signal when auto downsampling is enabled + (default: 100,000). Signals with more points are downsampled so that at most this + number of points is displayed. **Autoscale margin** Percentage of margin to add around data when auto-scaling signal plots. A value of diff --git a/doc/locale/fr/LC_MESSAGES/features/common/settings.po b/doc/locale/fr/LC_MESSAGES/features/common/settings.po index da1c34e0..78fc6449 100644 --- a/doc/locale/fr/LC_MESSAGES/features/common/settings.po +++ b/doc/locale/fr/LC_MESSAGES/features/common/settings.po @@ -332,14 +332,14 @@ msgstr "Pour les signaux comportant plus de ce nombre de points (par défaut : 1 msgid "**Use auto downsampling**" msgstr "**Utiliser le sous-échantillonnage automatique**" -msgid "Enable automatic downsampling for large signals to improve performance and visualization clarity." -msgstr "Activer le sous-échantillonnage automatique pour les grands signaux afin d'améliorer les performances et la clarté de la visualisation." +msgid "Enable automatic downsampling for large signals to improve performance and visualization clarity. Downsampling only affects the display: signal data and processing are not modified." +msgstr "Activer le sous-échantillonnage automatique pour les grands signaux afin d'améliorer les performances et la clarté de la visualisation. Le sous-échantillonnage ne concerne que l'affichage : les données des signaux et les traitements ne sont pas modifiés." msgid "**Downsampling max points**" msgstr "**Nombre maximum de points de sous-échantillonnage**" -msgid "Maximum number of points to display when downsampling is enabled (default: 10,000)." -msgstr "Nombre maximum de points à afficher lorsque le sous-échantillonnage est activé (par défaut : 10 000)." +msgid "Maximum number of points displayed for a signal when auto downsampling is enabled (default: 100,000). Signals with more points are downsampled so that at most this number of points is displayed." +msgstr "Nombre maximal de points affichés pour un signal lorsque le sous-échantillonnage automatique est activé (par défaut : 100 000). Les signaux comportant plus de points sont sous-échantillonnés de sorte qu'au plus ce nombre de points soit affiché." msgid "**Autoscale margin**" msgstr "**Marge d'échelle automatique**" 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 e88cb092..83fc9ad1 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 @@ -486,6 +486,15 @@ msgstr "**Disposition de la fenêtre :**" msgid "Fixed the width of the signal/image panels not being restored at startup: after narrowing the panels and restarting DataLab, they came back wider (up to their maximum width) instead of keeping the width chosen by the user" msgstr "Correction de la largeur des panneaux Signal/Image non restaurée au démarrage : après avoir réduit les panneaux puis redémarré DataLab, ils réapparaissaient plus larges (jusqu'à leur largeur maximale) au lieu de conserver la largeur choisie par l'utilisateur" +msgid "**Signal display:**" +msgstr "**Affichage des signaux :**" + +msgid "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))" +msgstr "Correction du sous-échantillonnage automatique des signaux (**Préférences > Visualisation > Signaux**), qui ne commençait qu'au-delà de 5 fois le nombre maximal de points configuré : avec la valeur par défaut de 100 000 points, les signaux de quelques centaines de milliers de points étaient tracés en entier, ce qui ralentissait l'affichage. Les signaux comportant plus de points que ce maximum sont désormais sous-échantillonnés de sorte qu'au plus ce nombre de points soit tracé, et la boîte de dialogue des préférences ainsi que la documentation indiquent désormais la valeur par défaut réelle (100 000 au lieu de 10 000) (corrige [Issue #455](https://github.com/DataLab-Platform/DataLab/issues/455))" + +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 "**HDF5 import:**" msgstr "**Import HDF5 :**" diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index f3fd3601..6c42d920 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -212,6 +212,11 @@ See the [HDF5 browser guide](../features/common/h5browser.rst) for scientific ob * Fixed the width of the signal/image panels not being restored at startup: after narrowing the panels and restarting DataLab, they came back wider (up to their maximum width) instead of keeping the width chosen by the user +**Signal display:** + +* 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 + **HDF5 import:** * Fixed images with unsigned 32-bit values above 2 147 483 647 being clipped when imported from an HDF5 file ("Clipping uint32 data to int32" warning): such images are now imported as 64-bit floating-point images keeping every value exactly. Other integer images that no supported integer type can hold also use a sufficiently precise floating-point type when possible, instead of rounding to 32-bit floating-point values beyond 16 777 216; a warning is emitted when no supported type guarantees exact conversion (in particular beyond ±2⁵³ for float64)