0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 11:41:49 +02:00

WIP Waveform Demo with curve object

This commit is contained in:
2025-01-14 15:00:11 +01:00
parent ce8c7d7a96
commit c2bb6ad21d
5 changed files with 298 additions and 0 deletions

View File

@ -0,0 +1,55 @@
from typing import Optional, Literal
import pyqtgraph as pg
from qtpy.QtCore import Qt
from pydantic import BaseModel, Field
class CurveConfig(BaseModel):
label: str = Field(..., description="Label/ID of this curve")
color: str = Field("blue", description="Curve color")
symbol: Optional[str] = Field(None, description="Symbol e.g. 'o', 'x'")
pen_width: int = Field(2, description="Pen width in px")
pen_style: Literal["solid", "dash", "dot", "dashdot"] = "solid"
# You can add device/signal if desired:
# signals: Optional[Signal] = None
# etc.
class Config:
model_config = {"validate_assignment": True}
pen_style_map = {
"solid": Qt.SolidLine,
"dash": Qt.DashLine,
"dot": Qt.DotLine,
"dashdot": Qt.DashDotLine,
}
class BECCurve(pg.PlotDataItem):
"""
A custom PlotDataItem that holds a reference to a Pydantic-based CurveConfig.
"""
def __init__(self, config: CurveConfig, parent=None):
super().__init__(name=config.label) # set the PlotDataItem name
self.config = config
self._parent = parent # optional reference to the WaveformPlot
# now apply config to actual PlotDataItem
self.apply_config()
def apply_config(self):
style = pen_style_map.get(self.config.pen_style, Qt.SolidLine)
pen = pg.mkPen(color=self.config.color, width=self.config.pen_width, style=style)
self.setPen(pen)
if self.config.symbol is not None:
self.setSymbol(self.config.symbol)
else:
self.setSymbol(None)
def set_data_custom(self, x, y):
# If you only want to allow custom data if config.source == "custom", etc.
self.setData(x, y)

View File

@ -0,0 +1,17 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.plots_next_gen.waveform.demo_2.waveform_demo2_plugin import (
WaveformPlotDemo2Plugin,
)
QPyDesignerCustomWidgetCollection.addCustomWidget(WaveformPlotDemo2Plugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@ -0,0 +1,144 @@
import json
from typing import List
from qtpy.QtWidgets import QWidget, QVBoxLayout
from qtpy.QtCore import Property
import pyqtgraph as pg
from bec_widgets.widgets.plots_next_gen.waveform.demo_2.demo_curve import BECCurve, CurveConfig
class WaveformPlotDemo2(QWidget):
"""
A Plot widget that stores multiple curves in a single JSON property (`curvesJson`).
Internally, we keep a list of (CurveConfig, BECCurve).
"""
def __init__(self, parent=None):
super().__init__(parent)
self._curves_json = "[]"
self._curves: List[BECCurve] = [] # the actual PlotDataItems
self._curve_configs: List[CurveConfig] = []
layout = QVBoxLayout(self)
self.plot_item = pg.PlotItem()
self.plot_widget = pg.PlotWidget(plotItem=self.plot_item)
layout.addWidget(self.plot_widget)
self.plot_item.addLegend()
# ------------------------------------------------------------------------
# QProperty: curvesJson
# ------------------------------------------------------------------------
def getCurvesJson(self) -> str:
return self._curves_json
def setCurvesJson(self, val: str):
if self._curves_json != val:
self._curves_json = val
self._rebuild_curves_from_json()
curvesJson = Property(str, fget=getCurvesJson, fset=setCurvesJson)
# ------------------------------------------------------------------------
# Internal method: parse JSON -> create/update BECCurve objects
# ------------------------------------------------------------------------
def _rebuild_curves_from_json(self):
# 1) Remove existing items from the plot
for c in self._curves:
self.plot_item.removeItem(c)
self._curves.clear()
self._curve_configs.clear()
# 2) Parse JSON
try:
raw_list = json.loads(self._curves_json)
if not isinstance(raw_list, list):
raise ValueError("curvesJson must be a JSON list.")
except Exception:
raw_list = []
# 3) Convert each raw dict -> CurveConfig -> BECCurve
for entry in raw_list:
try:
cfg = CurveConfig(**entry)
except Exception:
# fallback or skip
continue
curve_obj = BECCurve(config=cfg, parent=self)
# For demonstration, set some dummy data
xdata = [0, 1, 2, 3, 4]
ydata = [val + hash(cfg.label) % 3 for val in xdata]
curve_obj.setData(xdata, ydata)
self.plot_item.addItem(curve_obj)
self._curves.append(curve_obj)
self._curve_configs.append(cfg)
# ------------------------------------------------------------------------
# CLI / dynamic methods to add, remove, or modify curves at runtime
# ------------------------------------------------------------------------
def list_curve_labels(self) -> list[str]:
return [cfg.label for cfg in self._curve_configs]
def get_curve(self, label: str) -> BECCurve:
# Return the actual BECCurve object (or a config, or both)
for c in self._curves:
if c.config.label == label:
return c
raise ValueError(f"No curve with label='{label}'")
def add_curve(self, cfg: CurveConfig):
"""
Add a new curve from code. We just insert the new config
into the list, then re-serialize to JSON => triggers rebuild
"""
# insert new config to the internal list
self._curve_configs.append(cfg)
self._sync_json_from_configs()
def remove_curve(self, label: str):
for i, c in enumerate(self._curve_configs):
if c.label == label:
self._curve_configs.pop(i)
break
else:
raise ValueError(f"No curve with label='{label}' found to remove.")
self._sync_json_from_configs()
def set_curve_property(self, label: str, **kwargs):
"""
For example, set_curve_property("Curve1", color="red", pen_width=4)
We'll update the pydantic model, then re-sync to JSON, rebuild.
"""
c = self._find_config(label)
for k, v in kwargs.items():
setattr(c, k, v) # pydantic assignment
self._sync_json_from_configs()
def _find_config(self, label: str) -> CurveConfig:
for cfg in self._curve_configs:
if cfg.label == label:
return cfg
raise ValueError(f"No config with label='{label}' found.")
def _sync_json_from_configs(self):
"""
Re-serialize our internal curve configs -> JSON string,
call setCurvesJson(...) => triggers the rebuild in the same widget
so the user and Designer stay in sync
"""
raw_list = [cfg.dict() for cfg in self._curve_configs]
new_json = json.dumps(raw_list, indent=2)
self.setCurvesJson(new_json)
if __name__ == "__main__":
from qtpy.QtWidgets import QApplication
app = QApplication([])
w = WaveformPlotDemo2()
w.show()
w.add_curve(CurveConfig(label="Curve1", color="red"))
w.add_curve(CurveConfig(label="Curve2", color="blue"))
app.exec_()

View File

@ -0,0 +1 @@
{'files': ['waveform_demo2.py']}

View File

@ -0,0 +1,81 @@
"""
waveform_plot_plugin.py
Registers WaveformPlotDemo2 with Qt Designer,
including the WaveformPlotDemo2TaskMenu extension.
"""
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.plots_next_gen.waveform.demo_2.waveform_demo2 import WaveformPlotDemo2
DOM_XML = """
<ui language='c++'>
<widget class='WaveformPlotDemo2' name='WaveformPlotDemo2'>
<property name='geometry'>
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>200</height>
</rect>
</property>
<property name='deviceName'>
<string>MyDevice</string>
</property>
<property name='someFlag'>
<bool>false</bool>
</property>
<property name='curvesJson'>
<string>[{"label": "DefaultCurve", "color": "red"}]</string>
</property>
</widget>
</ui>
"""
class WaveformPlotDemo2Plugin(QDesignerCustomWidgetInterface):
"""
Exposes WaveformPlotDemo2 to Designer, plus sets up the Task Menu extension
for "Edit Configuration..." popup.
"""
def __init__(self):
super().__init__()
self._initialized = False
def initialize(self, form_editor):
self._form_editor = form_editor
def isInitialized(self):
return self._initialized
def createWidget(self, parent):
return WaveformPlotDemo2(parent)
def name(self):
return "WaveformPlotDemo2"
def group(self):
return "Waveform Widgets"
def icon(self):
# If you have a real icon, load it here
return QIcon()
def toolTip(self):
return "A multi-property WaveformPlotDemo2 example"
def whatsThis(self):
return self.toolTip()
def isContainer(self):
return False
def domXml(self):
return DOM_XML
def includeFile(self):
# The Python import path for your waveforms
# E.g. "my_widgets.waveform.waveform_plot"
return __name__