From 79b663894c9aa406e4abc5d3a3d2375e1c3335cb Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 30 Oct 2026 07:13:12 +0100 Subject: [PATCH] fix(curves): always restore stored curve styles Colors and line styles stored in signal metadata were ignored when they matched the automatic style cycle of earlier versions (tab10 colors with one of 4 line styles), so that a style chosen by the user among these 40 combinations (e.g. #1f77b4 with a solid line) was lost when reloading a workspace, and removed from metadata when saving it again. Stored styles are now always restored. `is_legacy_automatic` remains available to applications, to convert the data saved by earlier versions when loading them (DataLab does it for workspaces saved by DataLab 1.3 or earlier). Assisted-by: Claude Opus 5.5 --- doc/release_notes/release_1.01.md | 2 +- sigimax/adapters_plotpy/objects/signal.py | 22 +++---- .../adapters_plotpy/test_curve_styles.py | 62 ++++++++++++++----- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/doc/release_notes/release_1.01.md b/doc/release_notes/release_1.01.md index 269b098..40c37c7 100644 --- a/doc/release_notes/release_1.01.md +++ b/doc/release_notes/release_1.01.md @@ -27,7 +27,7 @@ * Automatic curve colors now have a contrast ratio of at least 3:1 against both the light and the dark plot backgrounds: several colors of the former palette (orange, pink, yellow-green, cyan) were pale on a white background, and the gray curve color could not be told apart from the grid. * Automatic line styles now alternate only between solid and dashed lines: dash-dot and dash-dot-dot lines, which drew only half of the curve with thin lines, are no longer used. * The plot grid now uses discreet solid lines, adapted to the color theme, instead of gray dotted lines competing with dashed curves. The grid is now also updated after a color theme change. -* Automatic curve styles are no longer stored in signal metadata: only a color or a line style chosen by the user is stored, and then restored. Colors and line styles stored by earlier versions from their automatic style cycle are recognized and handled as automatic styles. +* Automatic curve styles are no longer stored in signal metadata: only a color or a line style chosen by the user is stored, and then always restored, whatever its value. `CurveStyles.is_legacy_automatic` recognizes the colors and line styles that earlier versions stored from their automatic style cycle, so that applications may convert the data saved by these versions when loading them. * Making a plot item from an existing one (`make_item(update_from=...)`) keeps its style instead of using the next style of the cycle, so that dialogs showing a signal no longer shift the styles of the next curves. * New `CurveStyles` methods: `style_at` returns the automatic style at a given position, `set_automatic_style`, `get_automatic_style`, `has_automatic_style` and `restore_automatic_style` manage the automatic style of a curve, and `is_legacy_automatic` recognizes the styles stored by earlier versions. diff --git a/sigimax/adapters_plotpy/objects/signal.py b/sigimax/adapters_plotpy/objects/signal.py index 63e98a1..ef81df7 100644 --- a/sigimax/adapters_plotpy/objects/signal.py +++ b/sigimax/adapters_plotpy/objects/signal.py @@ -110,9 +110,15 @@ def style_generator() -> Generator[tuple[str, str], None, None]: @staticmethod def is_legacy_automatic(color: str | None, linestyle: str | None) -> bool: - """Return True if a stored style comes from the automatic style cycle of + """Return True if a stored style may come from the automatic style cycle of earlier versions, which stored automatic styles in signal metadata + Signal metadata are not converted when curves are displayed: a stored color + and line style are always restored, whatever their values. Applications + may use this method to convert, when loading them, data saved by earlier + versions (e.g. by removing such styles from the signal metadata, so that + curves are given the current automatic styles). + Args: color: stored color (None if not stored) linestyle: stored line style (None if not stored) @@ -299,22 +305,16 @@ def update_plot_item_parameters(self, item: CurveItem) -> None: have been overriden by object metadata entries or other object data. The goal is to update the plot item accordingly. - A color and a line style stored by earlier versions from their automatic - style cycle are ignored, so that the curve keeps its automatic style. + A color or a line style stored in the signal metadata is always restored: + it has been chosen by the user (automatic styles are not stored). This is *almost* the inverse operation of `update_metadata_from_plot_item`. Args: item: plot item """ - metadata = self.obj.metadata - line_source = metadata - if CURVESTYLES.is_legacy_automatic(*(metadata.get(k) for k in LINE_STYLE_KEYS)): - line_source = { - k: v for k, v in metadata.items() if k not in LINE_STYLE_KEYS - } - update_dataset(item.param.line, line_source) - update_dataset(item.param.symbol, metadata) + update_dataset(item.param.line, self.obj.metadata) + update_dataset(item.param.symbol, self.obj.metadata) super().update_plot_item_parameters(item) def update_metadata_from_plot_item(self, item: CurveItem) -> None: diff --git a/sigimax/tests/adapters_plotpy/test_curve_styles.py b/sigimax/tests/adapters_plotpy/test_curve_styles.py index 152dcef..d8888b6 100644 --- a/sigimax/tests/adapters_plotpy/test_curve_styles.py +++ b/sigimax/tests/adapters_plotpy/test_curve_styles.py @@ -138,29 +138,63 @@ def test_explicit_style_roundtrip() -> None: assert not CURVESTYLES.has_automatic_style(item2) +def _save_and_reload(adapter: SignalObjPlotPyAdapter, item, cycles: int = 2): + """Store the curve style in metadata and make a new item from them, as done + when saving and reloading a workspace, several times""" + for _cycle in range(cycles): + adapter.update_metadata_from_plot_item(item) + item = adapter.make_item() + return item + + @pytest.mark.parametrize("linestyle", CurveStyles.LEGACY_LINESTYLES) -def test_legacy_style_is_automatic(linestyle: str) -> None: - """Styles stored by earlier versions from their style cycle are automatic""" +def test_stored_style_is_kept(linestyle: str) -> None: + """A stored style is always restored, even if it belongs to the automatic style + cycle of earlier versions (legacy data is converted by applications on load)""" with qt_app_context(exec_loop=False): adapter = _make_signal_adapter() - adapter.obj.metadata["color"] = CurveStyles.LEGACY_COLORS[1] + color = CurveStyles.LEGACY_COLORS[1] + adapter.obj.metadata["color"] = color adapter.obj.metadata["style"] = linestyle - item = adapter.make_item() - assert CURVESTYLES.has_automatic_style(item) - assert _line_style(item)[0] in CurveStyles.COLORS - adapter.update_metadata_from_plot_item(item) - for key in LINE_STYLE_KEYS: - assert key not in adapter.obj.metadata + item = _save_and_reload(adapter, adapter.make_item()) + assert _line_style(item) == (color, linestyle) + assert not CURVESTYLES.has_automatic_style(item) + assert adapter.obj.metadata["color"] == color + assert adapter.obj.metadata["style"] == linestyle -def test_partial_legacy_style_is_explicit() -> None: - """A stored color without line style (e.g. set by a script) is explicit""" +def test_user_style_from_legacy_cycle_roundtrip() -> None: + """A style chosen by the user is kept, even if it matches a style of the + automatic style cycle of earlier versions""" with qt_app_context(exec_loop=False): adapter = _make_signal_adapter() - adapter.obj.metadata["color"] = CurveStyles.LEGACY_COLORS[1] item = adapter.make_item() - assert _line_style(item)[0] == CurveStyles.LEGACY_COLORS[1] - assert not CURVESTYLES.has_automatic_style(item) + item.param.line.color = "#1f77b4" + item.param.line.style = "SolidLine" + item.param.update_item(item) + assert CurveStyles.is_legacy_automatic("#1f77b4", "SolidLine") + item = _save_and_reload(adapter, item) + assert _line_style(item) == ("#1f77b4", "SolidLine") + assert adapter.obj.metadata["color"] == "#1f77b4" + assert adapter.obj.metadata["style"] == "SolidLine" + + +@pytest.mark.parametrize( + "color, linestyle, expected", + [ + ("#1f77b4", "SolidLine", True), + ("#17BECF", "DashDotDotLine", True), # Case insensitive colors + ("#1f77b4", "DotLine", False), # Not a style of the legacy cycle + ("#2a77ae", "SolidLine", False), # Color of the current palette + ("#1f77b4", None, False), # Line style not stored + (None, "SolidLine", False), # Color not stored + (1, "SolidLine", False), # Invalid values + ], +) +def test_is_legacy_automatic(color, linestyle, expected: bool) -> None: + """Styles of the automatic style cycle of earlier versions are recognized""" + with qt_app_context(exec_loop=False): + assert CurveStyles.is_legacy_automatic(color, linestyle) is expected def test_restore_automatic_style() -> None: