From 7b54f6246e95ed4c589a13305ab138e9a91efbb3 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Thu, 17 Sep 2026 16:46:54 +0200 Subject: [PATCH] fix(waveform): use thin line when downsampling no-line curves --- bec_widgets/cli/client.py | 1 + bec_widgets/widgets/plots/waveform/curve.py | 16 +++-- .../widgets/plots/waveform/waveform.py | 19 +++--- tests/unit_tests/test_waveform.py | 61 ++++++++++++++++++- 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 00db1dfe..6cdf6251 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -1056,6 +1056,7 @@ class Curve(RPCBase): Args: pen_style(Literal["solid", "dash", "dot", "dashdot", "none"]): Style of the pen. Use "none" to display markers without connecting lines. + During automatic downsampling, "none" uses a 1 px solid line without markers. """ @rpc_call diff --git a/bec_widgets/widgets/plots/waveform/curve.py b/bec_widgets/widgets/plots/waveform/curve.py index 497358d4..972a05f0 100644 --- a/bec_widgets/widgets/plots/waveform/curve.py +++ b/bec_widgets/widgets/plots/waveform/curve.py @@ -137,17 +137,23 @@ class Curve(BECConnector, pg.PlotDataItem): "none": QtCore.Qt.NoPen, } pen_style = pen_style_map.get(self.config.pen_style, QtCore.Qt.SolidLine) + line_fallback = self.config.pen_style == "none" and self.opts["autoDownsample"] + pen_width = self.config.pen_width + if line_fallback: + pen_style = QtCore.Qt.SolidLine + pen_width = 1 - pen = pg.mkPen(color=self.config.color, width=self.config.pen_width, style=pen_style) + pen = pg.mkPen(color=self.config.color, width=pen_width, style=pen_style) self.setPen(pen) - if self.config.symbol: + symbol = None if line_fallback else self.config.symbol + if symbol: symbol_color = self.config.symbol_color or self.config.color brush = pg.mkBrush(color=symbol_color) self.setSymbolBrush(brush) self.setSymbolSize(self.config.symbol_size) - self.setSymbol(self.config.symbol) + self.setSymbol(symbol) @property def dap_params(self): @@ -279,8 +285,7 @@ class Curve(BECConnector, pg.PlotDataItem): symbol(str): Symbol of the curve. """ self.config.symbol = symbol - self.setSymbol(symbol) - self.updateItems() + self.apply_config() def set_symbol_color(self, symbol_color: str): """ @@ -319,6 +324,7 @@ class Curve(BECConnector, pg.PlotDataItem): Args: pen_style(Literal["solid", "dash", "dot", "dashdot", "none"]): Style of the pen. Use "none" to display markers without connecting lines. + During automatic downsampling, "none" uses a 1 px solid line without markers. """ self.config.pen_style = pen_style self.apply_config() diff --git a/bec_widgets/widgets/plots/waveform/waveform.py b/bec_widgets/widgets/plots/waveform/waveform.py index 2c6fa30a..a99fd74a 100644 --- a/bec_widgets/widgets/plots/waveform/waveform.py +++ b/bec_widgets/widgets/plots/waveform/waveform.py @@ -1938,7 +1938,8 @@ class Waveform(PlotBase): Curve items, by deactivating the symbol and activating downsampling auto, method='mean', if the data length exceeds N points. If the data length is less than N points, the symbol will be activated and downsampling will be deactivated. Maximum points will be - 5x the limit. Curves without a connecting line retain their markers at any length. + 5x the limit. The "none" style uses a 1 px solid line without markers while + downsampling, retaining its configured appearance for smaller datasets. Args: curve(Curve): The curve to adjust. @@ -1949,21 +1950,25 @@ class Waveform(PlotBase): if limit <= 1: logger.warning("Limit must be greater than 1.") return - if curve.config.pen_style == "none" and curve.config.symbol is None: - curve.set_symbol("o") + if curve.config.pen_style == "none": + downsampling = data_length > limit + curve.setDownsampling(ds=None if downsampling else 1, auto=downsampling, method=method) + if not downsampling and curve.config.symbol is None: + curve.set_symbol("o") curve.apply_config() + curve.setClipToView(True) + return if data_length > limit: - if curve.config.pen_style != "none" and curve.config.symbol is not None: + if curve.config.symbol is not None: curve.set_symbol(None) if curve.config.pen_width > 3: curve.set_pen_width(3) curve.setDownsampling(ds=None, auto=True, method=method) curve.setClipToView(True) elif data_length <= limit: - if curve.config.pen_style != "none": - curve.set_symbol("o") + curve.set_symbol("o") curve.set_pen_width(4) - curve.setDownsampling(ds=1, auto=None, method=method) + curve.setDownsampling(ds=1, auto=False, method=method) curve.setClipToView(True) def setup_dap_for_scan(self): diff --git a/tests/unit_tests/test_waveform.py b/tests/unit_tests/test_waveform.py index a1e69a51..79eac068 100644 --- a/tests/unit_tests/test_waveform.py +++ b/tests/unit_tests/test_waveform.py @@ -1262,10 +1262,10 @@ def test_curve_no_line_style_preserves_markers(qtbot, mocked_client): np.testing.assert_array_equal(curve.scatter.getData(), [[2, 3, 4], [5, 6, 7]]) -@pytest.mark.parametrize("data_length", [500, 1500]) +@pytest.mark.parametrize("data_length", [1000, 1001]) @pytest.mark.parametrize("symbol", [None, "d"]) -def test_curve_no_line_style_keeps_async_markers(qtbot, mocked_client, data_length, symbol): - """Async data retain markers, including when automatic styling previously hid them.""" +def test_curve_no_line_style_async_downsampling(qtbot, mocked_client, data_length, symbol): + """No-line curves fall back to a thin line without markers above the async limit.""" wf = create_widget(qtbot, Waveform, client=mocked_client) curve = wf.plot( x=[1, 2, 3], @@ -1280,6 +1280,13 @@ def test_curve_no_line_style_keeps_async_markers(qtbot, mocked_client, data_leng wf._auto_adjust_async_curve_settings(curve, data_length) curve.setData(np.arange(data_length), np.arange(data_length)) + assert curve.config.pen_style == "none" + assert curve.config.pen_width == 3 + if data_length > 1000: + assert curve.curve.opts["pen"].style() == Qt.SolidLine + assert curve.curve.opts["pen"].widthF() == 1 + assert not curve.scatter.isVisible() + return assert curve.curve.opts["pen"].style() == Qt.NoPen assert curve.scatter.isVisible() assert curve.config.symbol == (symbol or "o") @@ -1288,6 +1295,54 @@ def test_curve_no_line_style_keeps_async_markers(qtbot, mocked_client, data_leng assert curve.scatter.opts["brush"].color().name() == "#654321" +def test_curve_no_line_downsampling_style_updates_and_restore(qtbot, mocked_client): + """Appearance updates keep the fallback, and smaller data restore the saved markers.""" + wf = create_widget(qtbot, Waveform, client=mocked_client) + curve = wf.plot(x=np.arange(1500), y=np.arange(1500)) + wf._auto_adjust_async_curve_settings(curve, 1500) + curve.set_pen_style("none") + assert curve.curve.opts["pen"].style() == Qt.SolidLine + assert curve.curve.opts["pen"].widthF() == 1 + assert not curve.scatter.isVisible() + + curve.set_color("#123456") + curve.set_pen_width(7) + curve.set_symbol("d") + curve.set_symbol_color("#654321") + curve.set_symbol_size(12) + assert curve.curve.opts["pen"].style() == Qt.SolidLine + assert curve.curve.opts["pen"].widthF() == 1 + assert curve.curve.opts["pen"].color().name() == "#123456" + assert not curve.scatter.isVisible() + assert curve.config.pen_style == "none" + assert curve.config.pen_width == 7 + assert curve.config.symbol == "d" + + wf._auto_adjust_async_curve_settings(curve, 1000) + curve.setData(np.arange(1000), np.arange(1000)) + assert not curve.opts["autoDownsample"] + assert curve.curve.opts["pen"].style() == Qt.NoPen + assert curve.config.pen_width == 7 + assert curve.scatter.isVisible() + assert curve.scatter.opts["symbol"] == "d" + assert curve.scatter.opts["size"] == 12 + assert curve.scatter.opts["brush"].color().name() == "#654321" + + +def test_curve_no_line_after_named_style_downsampling_ends(qtbot, mocked_client): + """Selecting no line after a named style leaves downsampling does not keep the fallback.""" + wf = create_widget(qtbot, Waveform, client=mocked_client) + curve = wf.plot(x=np.arange(1500), y=np.arange(1500)) + wf._auto_adjust_async_curve_settings(curve, 1500) + wf._auto_adjust_async_curve_settings(curve, 1000) + curve.setData(np.arange(1000), np.arange(1000)) + curve.set_pen_style("none") + + assert not curve.opts["autoDownsample"] + assert curve.curve.opts["pen"].style() == Qt.NoPen + assert curve.scatter.isVisible() + + def test_curve_no_line_style_restores_existing_styles(qtbot, mocked_client): """The no-line style is reversible, and legacy null styles still draw solid lines.""" wf = create_widget(qtbot, Waveform, client=mocked_client)