feat(waveform): add no-line curve style

This commit is contained in:
wakonig_k committed 2026-09-17 16:23:13 +02:00
1 parent b76b95e7d8
commit e4b02bcdf3
6 files changed
+136 -15

No files matched your search

+7 -3
View File
@@ -974,7 +974,8 @@ class Curve(RPCBase):
- symbol_color: str
- symbol_size: int
- pen_width: int
- pen_style: Literal["solid", "dash", "dot", "dashdot"]
- pen_style: Literal["solid", "dash", "dot", "dashdot", "none"]
Use "none" to display markers without connecting lines.
"""
@rpc_call
@@ -1046,12 +1047,15 @@ class Curve(RPCBase):
"""
@rpc_call
def set_pen_style(self, pen_style: "Literal['solid', 'dash', 'dot', 'dashdot']"):
def set_pen_style(
self, pen_style: "Literal['solid', 'dash', 'dot', 'dashdot', 'none']"
) -> "None":
"""
Change the pen style of the curve.
Args:
pen_style(Literal["solid", "dash", "dot", "dashdot"]): Style of the pen.
pen_style(Literal["solid", "dash", "dot", "dashdot", "none"]): Style of the pen.
Use "none" to display markers without connecting lines.
"""
@rpc_call
+8 -5
View File
@@ -41,8 +41,8 @@ class CurveConfig(ConnectionConfig):
)
symbol_size: int | None = Field(7, description="The size of the symbol of the curve.")
pen_width: int | None = Field(4, description="The width of the pen of the curve.")
pen_style: Literal["solid", "dash", "dot", "dashdot"] | None = Field(
"solid", description="The style of the pen of the curve."
pen_style: Literal["solid", "dash", "dot", "dashdot", "none"] | None = Field(
"solid", description="The style of the curve's connecting line; 'none' disables the line."
)
source: Literal["device", "dap", "custom", "history"] = Field(
"custom", description="The source of the curve."
@@ -134,6 +134,7 @@ class Curve(BECConnector, pg.PlotDataItem):
"dash": QtCore.Qt.DashLine,
"dot": QtCore.Qt.DotLine,
"dashdot": QtCore.Qt.DashDotLine,
"none": QtCore.Qt.NoPen,
}
pen_style = pen_style_map.get(self.config.pen_style, QtCore.Qt.SolidLine)
@@ -238,7 +239,8 @@ class Curve(BECConnector, pg.PlotDataItem):
- symbol_color: str
- symbol_size: int
- pen_width: int
- pen_style: Literal["solid", "dash", "dot", "dashdot"]
- pen_style: Literal["solid", "dash", "dot", "dashdot", "none"]
Use "none" to display markers without connecting lines.
"""
# Mapping of keywords to setter methods
@@ -310,12 +312,13 @@ class Curve(BECConnector, pg.PlotDataItem):
self.config.pen_width = pen_width
self.apply_config()
def set_pen_style(self, pen_style: Literal["solid", "dash", "dot", "dashdot"]):
def set_pen_style(self, pen_style: Literal["solid", "dash", "dot", "dashdot", "none"]) -> None:
"""
Change the pen style of the curve.
Args:
pen_style(Literal["solid", "dash", "dot", "dashdot"]): Style of the pen.
pen_style(Literal["solid", "dash", "dot", "dashdot", "none"]): Style of the pen.
Use "none" to display markers without connecting lines.
"""
self.config.pen_style = pen_style
self.apply_config()
@@ -282,8 +282,10 @@ class CurveRow(QTreeWidgetItem):
# Style in col 5
self.style_combo = QComboBox()
self.style_combo.addItems(["solid", "dash", "dot", "dashdot"])
idx = self.style_combo.findText(self.config.pen_style)
for style in ("solid", "dash", "dot", "dashdot"):
self.style_combo.addItem(style, style)
self.style_combo.addItem("No line", "none")
idx = self.style_combo.findData(self.config.pen_style)
if idx >= 0:
self.style_combo.setCurrentIndex(idx)
self.tree.setItemWidget(self, 5, self.style_combo)
@@ -495,7 +497,7 @@ class CurveRow(QTreeWidgetItem):
# Common style fields
self.config.color = self.color_button.color
self.config.symbol_color = self.color_button.color
self.config.pen_style = self.style_combo.currentText()
self.config.pen_style = self.style_combo.currentData()
self.config.pen_width = self.width_spin.value()
self.config.symbol_size = self.symbol_spin.value()
@@ -1938,7 +1938,7 @@ 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.
5x the limit. Curves without a connecting line retain their markers at any length.
Args:
curve(Curve): The curve to adjust.
@@ -1949,15 +1949,19 @@ 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")
curve.apply_config()
if data_length > limit:
if curve.config.symbol is not None:
if curve.config.pen_style != "none" and 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:
curve.set_symbol("o")
if curve.config.pen_style != "none":
curve.set_symbol("o")
curve.set_pen_width(4)
curve.setDownsampling(ds=1, auto=None, method=method)
curve.setClipToView(True)
+35
View File
@@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch
import pytest
from bec_lib.scan_history import ScanHistory
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QComboBox, QVBoxLayout
from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_setting import CurveSetting
@@ -84,6 +85,40 @@ def test_curve_setting_accept_changes(curve_setting_fixture, qtbot):
send_spy.assert_called_once()
@pytest.mark.parametrize(
"style, label, expected_pen",
[
("none", "No line", Qt.NoPen),
("solid", "solid", Qt.SolidLine),
("dash", "dash", Qt.DashLine),
("dot", "dot", Qt.DotLine),
("dashdot", "dashdot", Qt.DashDotLine),
],
)
def test_curve_setting_style_round_trip(curve_setting_fixture, style, label, expected_pen):
"""Applying and restoring curve styles preserves the selection and rendered pen."""
settings, wf = curve_setting_fixture
wf.plot("bpm4i")
settings.refresh()
row = settings.curve_manager.all_items[0]
row.style_combo.setCurrentText(label)
settings.accept_changes()
serialized = wf.curve_json
assert json.loads(serialized)[0]["pen_style"] == style
wf.curve_json = serialized
curve = wf.curves[0]
curve.setData([1, 2, 3], [4, 5, 6])
assert curve.curve.opts["pen"].style() == expected_pen
assert curve.scatter.isVisible()
assert curve.scatter.opts["symbol"] == "o"
settings.refresh()
restored_row = settings.curve_manager.all_items[0]
assert restored_row.style_combo.currentText() == label
assert restored_row.style_combo.currentData() == style
def test_curve_setting_switch_device_mode(curve_setting_fixture, qtbot):
"""
If user chooses device mode from the combo, the device_x line edit should be enabled
+74 -1
View File
@@ -11,7 +11,7 @@ import pytest
from bec_lib import messages
from bec_lib.scan_items import ScanItem
from pyqtgraph.graphicsItems.DateAxisItem import DateAxisItem
from qtpy.QtCore import QTimer
from qtpy.QtCore import Qt, QTimer
from qtpy.QtWidgets import QApplication, QCheckBox, QDialog, QDialogButtonBox, QDoubleSpinBox
from bec_widgets.widgets.plots.plot_base import UIMode
@@ -1239,6 +1239,79 @@ def test_curve_set_appearance_methods(qtbot, mocked_client):
assert c.config.pen_style == "dashdot"
def test_curve_no_line_style_preserves_markers(qtbot, mocked_client):
"""Disabling the connecting line preserves markers across appearance and data updates."""
wf = create_widget(qtbot, Waveform, client=mocked_client)
curve = wf.plot(x=[1, 2, 3], y=[4, 5, 6], symbol="d")
curve.set_pen_style("none")
assert curve.opts["pen"].style() == Qt.NoPen
curve.set_color("#123456")
curve.set_symbol_color("#654321")
curve.set_symbol_size(12)
curve.set_pen_width(5)
curve.set_data([2, 3, 4], [5, 6, 7])
assert curve.config.pen_style == "none"
assert curve.curve.opts["pen"].style() == Qt.NoPen
assert curve.opts["pen"].color().name() == "#123456"
assert curve.scatter.isVisible()
assert curve.scatter.opts["symbol"] == "d"
assert curve.scatter.opts["size"] == 12
assert curve.scatter.opts["brush"].color().name() == "#654321"
np.testing.assert_array_equal(curve.scatter.getData(), [[2, 3, 4], [5, 6, 7]])
@pytest.mark.parametrize("data_length", [500, 1500])
@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."""
wf = create_widget(qtbot, Waveform, client=mocked_client)
curve = wf.plot(
x=[1, 2, 3],
y=[4, 5, 6],
pen_style="none",
pen_width=3,
symbol=symbol,
symbol_color="#654321",
symbol_size=12,
)
wf._auto_adjust_async_curve_settings(curve, data_length)
curve.setData(np.arange(data_length), np.arange(data_length))
assert curve.curve.opts["pen"].style() == Qt.NoPen
assert curve.scatter.isVisible()
assert curve.config.symbol == (symbol or "o")
assert curve.scatter.opts["symbol"] == (symbol or "o")
assert curve.scatter.opts["size"] == 12
assert curve.scatter.opts["brush"].color().name() == "#654321"
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)
curve = wf.plot(x=[1, 2, 3], y=[4, 5, 6], pen_style="none")
assert curve.curve.opts["pen"].style() == Qt.NoPen
for style, expected in (
("solid", Qt.SolidLine),
("dash", Qt.DashLine),
("dot", Qt.DotLine),
("dashdot", Qt.DashDotLine),
):
curve.set_pen_style(style)
assert curve.curve.opts["pen"].style() == expected
assert curve.curve.isVisible()
curve.set(pen_style="none")
assert curve.curve.opts["pen"].style() == Qt.NoPen
config = curve.config.model_dump()
config["pen_style"] = None
curve.apply_config(config)
assert curve.curve.opts["pen"].style() == Qt.SolidLine
def test_curve_set_custom_data(qtbot, mocked_client):
"""
Test that custom curves allow setting new data via set_data.