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:
@ -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)
|
@ -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()
|
@ -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_()
|
@ -0,0 +1 @@
|
||||
{'files': ['waveform_demo2.py']}
|
@ -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__
|
Reference in New Issue
Block a user