mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
feat: BECFigure and BECPlotBase created
This commit is contained in:
1
bec_widgets/widgets/figure/__init__.py
Normal file
1
bec_widgets/widgets/figure/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .figure import FigureConfig, BECFigure
|
245
bec_widgets/widgets/figure/figure.py
Normal file
245
bec_widgets/widgets/figure/figure.py
Normal file
@ -0,0 +1,245 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
import itertools
|
||||
import os
|
||||
from typing import Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from PyQt6.QtWidgets import QVBoxLayout, QMainWindow
|
||||
from pydantic import Field
|
||||
from pyqtgraph.Qt import uic
|
||||
from qtpy.QtWidgets import QApplication, QWidget
|
||||
|
||||
from bec_widgets.utils import BECDispatcher, BECConnector, ConnectionConfig
|
||||
from bec_widgets.widgets.plots import WidgetConfig, BECPlotBase
|
||||
|
||||
|
||||
class FigureConfig(ConnectionConfig):
|
||||
"""Configuration for BECFigure. Inheriting from ConnectionConfig widget_class and gui_id"""
|
||||
|
||||
theme: Literal["dark", "light"] = Field("dark", description="The theme of the figure widget.")
|
||||
num_columns: int = Field(1, description="The number of columns in the figure widget.")
|
||||
widgets: dict[str, WidgetConfig] = Field(
|
||||
{}, description="The list of widgets to be added to the figure widget."
|
||||
)
|
||||
|
||||
|
||||
class BECFigure(BECConnector):
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None,
|
||||
config: Optional[FigureConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
graphics_layout_widget: Optional[pg.GraphicsLayoutWidget] = None,
|
||||
):
|
||||
if config is None:
|
||||
config = FigureConfig(widget_class=self.__class__.__name__)
|
||||
else:
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
# pg.GraphicsLayoutWidget.__init__(self, parent) #in case of inheritance
|
||||
|
||||
self.glw = (
|
||||
graphics_layout_widget
|
||||
if graphics_layout_widget
|
||||
else pg.GraphicsLayoutWidget(parent=parent)
|
||||
)
|
||||
|
||||
self.widgets = {}
|
||||
|
||||
# TODO just testing adding plot
|
||||
self.add_widget("widget_1", row=0, col=0, title="Plot 1")
|
||||
self.widgets["widget_1"].plot(
|
||||
np.linspace(0, 10, 100), np.sin(np.linspace(0, 10, 100)), label="sin(x)"
|
||||
)
|
||||
|
||||
def show(self): # TODO check if useful for anything
|
||||
self.window = QMainWindow()
|
||||
self.window.setCentralWidget(self.glw)
|
||||
self.window.show()
|
||||
|
||||
def close(self): # TODO check if useful for anything
|
||||
if hasattr(self, "window"):
|
||||
self.window.close()
|
||||
|
||||
def add_widget(self, widget_id: str = None, row: int = None, col: int = None, **kwargs):
|
||||
# Generate unique widget_id if not provided
|
||||
if not widget_id:
|
||||
widget_id = self._generate_unique_widget_id()
|
||||
|
||||
# Check if id is available
|
||||
if widget_id in self.widgets:
|
||||
print(f"Widget with ID {widget_id} already exists.") # TODO change to raise error)
|
||||
return
|
||||
|
||||
# Crete widget instance and its config
|
||||
widget_config = WidgetConfig(
|
||||
parent_figure_id=self.gui_id, widget_class="BECPlotBase", gui_id=widget_id, **kwargs
|
||||
)
|
||||
|
||||
plot_item = pg.PlotItem()
|
||||
widget = BECPlotBase(plot_item=plot_item, config=widget_config)
|
||||
|
||||
# Check if position is occupied
|
||||
if row is not None and col is not None:
|
||||
print("adding plot")
|
||||
if self.glw.getItem(row, col):
|
||||
print(
|
||||
f"Position at row {row} and column {col} is already occupied."
|
||||
) # TODO change to raise error
|
||||
return
|
||||
else:
|
||||
widget_config.row = row
|
||||
widget_config.column = col
|
||||
|
||||
# Add widget to the figure
|
||||
self.glw.addItem(widget.plt, row=row, col=col)
|
||||
else:
|
||||
row, col = self._find_next_empty_position()
|
||||
widget_config.row = row
|
||||
widget_config.column = col
|
||||
|
||||
# Add widget to the figure
|
||||
self.glw.addItem(widget.plt, row=row, col=col)
|
||||
|
||||
# Saving config for future referencing
|
||||
self.config.widgets[widget_id] = widget_config
|
||||
self.widgets[widget_id] = widget
|
||||
|
||||
def __getitem__(self, key: tuple | str):
|
||||
if isinstance(key, tuple) and len(key) == 2:
|
||||
return self._get_widget_by_coordinates(*key)
|
||||
elif isinstance(key, str):
|
||||
widget = self.widgets.get(key)
|
||||
if widget is None:
|
||||
raise KeyError(f"No widget with ID {key}")
|
||||
return self.widgets.get(key)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Key must be a string (widget id) or a tuple of two integers (coordinates)"
|
||||
)
|
||||
|
||||
def _get_widget_by_coordinates(self, row: int, col: int) -> BECPlotBase:
|
||||
"""
|
||||
Get widget by its coordinates in the figure.
|
||||
Args:
|
||||
row(int): the row coordinate
|
||||
col(int): the column coordinate
|
||||
|
||||
Returns:
|
||||
BECPlotBase: the widget at the given coordinates
|
||||
"""
|
||||
widget = self.glw.getItem(row, col)
|
||||
if widget is None:
|
||||
raise KeyError(f"No widget at coordinates ({row}, {col})")
|
||||
return widget
|
||||
|
||||
def _add_waveform1d(self, widget_id: str = None, row: int = None, col: int = None, **kwargs):
|
||||
"""
|
||||
Add a 1D waveform widget to the figure.
|
||||
Args:
|
||||
widget_id:
|
||||
row:
|
||||
col:
|
||||
**kwargs:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
|
||||
def _find_next_empty_position(self):
|
||||
"""Find the next empty position (new row) in the figure."""
|
||||
row, col = 0, 0
|
||||
while self.glw.getItem(row, col):
|
||||
row += 1
|
||||
return row, col
|
||||
|
||||
def _generate_unique_widget_id(self):
|
||||
"""Generate a unique widget ID."""
|
||||
existing_ids = set(self.widgets.keys())
|
||||
for i in itertools.count(1):
|
||||
widget_id = f"widget_{i}"
|
||||
if widget_id not in existing_ids:
|
||||
return widget_id
|
||||
|
||||
|
||||
##################################################
|
||||
##################################################
|
||||
# Debug window
|
||||
##################################################
|
||||
##################################################
|
||||
|
||||
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
||||
from qtconsole.inprocess import QtInProcessKernelManager
|
||||
|
||||
|
||||
class JupyterConsoleWidget(RichJupyterWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.kernel_manager = QtInProcessKernelManager()
|
||||
self.kernel_manager.start_kernel(show_banner=False)
|
||||
self.kernel_client = self.kernel_manager.client()
|
||||
self.kernel_client.start_channels()
|
||||
|
||||
self.kernel_manager.kernel.shell.push({"np": np, "pg": pg})
|
||||
|
||||
def shutdown_kernel(self):
|
||||
self.kernel_client.stop_channels()
|
||||
self.kernel_manager.shutdown_kernel()
|
||||
|
||||
|
||||
class DebugWindow(QWidget):
|
||||
"""Debug window for BEC widgets"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
uic.loadUi(os.path.join(current_path, "figure_debug_minimal.ui"), self)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
# console push
|
||||
self.console.kernel_manager.kernel.shell.push({"fig": self.figure})
|
||||
|
||||
def _init_ui(self):
|
||||
# Plotting window
|
||||
self.glw_1_layout = QVBoxLayout(self.glw) # Create a new QVBoxLayout
|
||||
graphics_layout_widget = (
|
||||
pg.GraphicsLayoutWidget()
|
||||
) # create a new pg.GraphicsLayoutWidget instance
|
||||
self.figure = BECFigure(
|
||||
graphics_layout_widget=graphics_layout_widget, parent=self
|
||||
) # Create a new BECDeviceMonitor
|
||||
self.glw_1_layout.addWidget(self.figure.glw) # Add BECDeviceMonitor to the layout
|
||||
|
||||
self.console_layout = QVBoxLayout(self.widget_console)
|
||||
self.console = JupyterConsoleWidget()
|
||||
self.console_layout.addWidget(self.console)
|
||||
self.console.set_default_style("linux")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
win = DebugWindow()
|
||||
win.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
# if __name__ == "__main__": # pragma: no cover
|
||||
# from PyQt6.QtWidgets import QApplication
|
||||
#
|
||||
# app = QApplication([])
|
||||
#
|
||||
# fig = BECFigure()
|
||||
# fig.show()
|
||||
#
|
||||
# app.exec()
|
2
bec_widgets/widgets/plots/__init__.py
Normal file
2
bec_widgets/widgets/plots/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .plot_base import AxisConfig, WidgetConfig, BECPlotBase
|
||||
from .waveform1d import Waveform1DConfig, BECWaveform1D
|
187
bec_widgets/widgets/plots/plot_base.py
Normal file
187
bec_widgets/widgets/plots/plot_base.py
Normal file
@ -0,0 +1,187 @@
|
||||
from typing import Optional, Literal
|
||||
|
||||
import pyqtgraph as pg
|
||||
import numpy as np
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
|
||||
|
||||
class AxisConfig(BaseModel):
|
||||
title: Optional[str] = Field(None, description="The title of the axes.")
|
||||
x_label: Optional[str] = Field(None, description="The label for the x-axis.")
|
||||
y_label: Optional[str] = Field(None, description="The label for the y-axis.")
|
||||
x_scale: Literal["linear", "log"] = Field("linear", description="The scale of the x-axis.")
|
||||
y_scale: Literal["linear", "log"] = Field("linear", description="The scale of the y-axis.")
|
||||
x_lim: Optional[tuple] = Field(None, description="The limits of the x-axis.")
|
||||
y_lim: Optional[tuple] = Field(None, description="The limits of the y-axis.")
|
||||
x_grid: bool = Field(False, description="Show grid on the x-axis.")
|
||||
y_grid: bool = Field(False, description="Show grid on the y-axis.")
|
||||
|
||||
|
||||
class WidgetConfig(ConnectionConfig):
|
||||
parent_figure_id: Optional[str] = Field(None, description="The parent figure of the plot.")
|
||||
|
||||
# Coordinates in the figure
|
||||
row: int = Field(0, description="The row coordinate in the figure.")
|
||||
column: int = Field(0, description="The column coordinate in the figure.")
|
||||
|
||||
# Appearance settings
|
||||
axis: AxisConfig = Field(
|
||||
default_factory=AxisConfig, description="The axis configuration of the plot."
|
||||
)
|
||||
|
||||
|
||||
class BECPlotBase(BECConnector): # , pg.PlotItem):
|
||||
USER_ACCESS = [
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
"set_y_label",
|
||||
"set_x_scale",
|
||||
"set_y_scale",
|
||||
"set_x_lim",
|
||||
"set_y_lim",
|
||||
"plot",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None, # TODO decide if needed for this class
|
||||
plot_item: Optional[pg.PlotItem] = None,
|
||||
config: Optional[WidgetConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
):
|
||||
if config is None:
|
||||
config = WidgetConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
self.plt = plot_item if plot_item else pg.PlotItem(parent=parent)
|
||||
|
||||
def set(self, **kwargs) -> None:
|
||||
"""
|
||||
Set the properties of the plot widget.
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
"""
|
||||
|
||||
# Mapping of keywords to setter methods
|
||||
method_map = {
|
||||
"title": self.set_title,
|
||||
"x_label": self.set_x_label,
|
||||
"y_label": self.set_y_label,
|
||||
"x_scale": self.set_x_scale,
|
||||
"y_scale": self.set_y_scale,
|
||||
"x_lim": self.set_x_lim,
|
||||
"y_lim": self.set_y_lim,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
method_map[key](value)
|
||||
else:
|
||||
print(f"Warning: '{key}' is not a recognized property.")
|
||||
|
||||
def apply_axis_config(self):
|
||||
"""Apply the axis configuration to the plot widget."""
|
||||
config_mappings = {
|
||||
"title": self.config.axis.title,
|
||||
"x_label": self.config.axis.x_label,
|
||||
"y_label": self.config.axis.y_label,
|
||||
"x_scale": self.config.axis.x_scale,
|
||||
"y_scale": self.config.axis.y_scale,
|
||||
}
|
||||
|
||||
self.set(**{k: v for k, v in config_mappings.items() if v is not None})
|
||||
|
||||
def set_title(self, title: str):
|
||||
"""
|
||||
Set the title of the plot widget.
|
||||
Args:
|
||||
title(str): Title of the plot widget.
|
||||
"""
|
||||
self.plt.setTitle(title)
|
||||
self.config.axis.title = title
|
||||
|
||||
def set_x_label(self, label: str):
|
||||
"""
|
||||
Set the label of the x-axis.
|
||||
Args:
|
||||
label(str): Label of the x-axis.
|
||||
"""
|
||||
self.plt.setLabel("bottom", label)
|
||||
self.config.axis.x_label = label
|
||||
|
||||
def set_y_label(self, label: str):
|
||||
"""
|
||||
Set the label of the y-axis.
|
||||
Args:
|
||||
label(str): Label of the y-axis.
|
||||
"""
|
||||
self.plt.setLabel("left", label)
|
||||
self.config.axis.y_label = label
|
||||
|
||||
def set_x_scale(self, scale: Literal["linear", "log"] = "linear"):
|
||||
"""
|
||||
Set the scale of the x-axis.
|
||||
Args:
|
||||
scale(Literal["linear", "log"]): Scale of the x-axis.
|
||||
"""
|
||||
self.plt.setLogMode(x=(scale == "log"))
|
||||
self.config.axis.x_scale = scale
|
||||
|
||||
def set_y_scale(self, scale: Literal["linear", "log"] = "linear"):
|
||||
"""
|
||||
Set the scale of the y-axis.
|
||||
Args:
|
||||
scale(Literal["linear", "log"]): Scale of the y-axis.
|
||||
"""
|
||||
self.plt.setLogMode(y=(scale == "log"))
|
||||
self.config.axis.y_scale = scale
|
||||
|
||||
def set_x_lim(self, x_lim: tuple) -> None:
|
||||
"""
|
||||
Set the limits of the x-axis.
|
||||
Args:
|
||||
x_lim(tuple): Limits of the x-axis.
|
||||
"""
|
||||
self.plt.setXRange(x_lim[0], x_lim[1])
|
||||
self.config.axis.x_lim = x_lim
|
||||
|
||||
def set_y_lim(self, y_lim: tuple) -> None:
|
||||
"""
|
||||
Set the limits of the y-axis.
|
||||
Args:
|
||||
y_lim(tuple): Limits of the y-axis.
|
||||
"""
|
||||
self.plt.setYRange(y_lim[0], y_lim[1])
|
||||
self.config.axis.y_lim = y_lim
|
||||
|
||||
def set_grid(self, x: bool = False, y: bool = False):
|
||||
"""
|
||||
Set the grid of the plot widget.
|
||||
Args:
|
||||
x(bool): Show grid on the x-axis.
|
||||
y(bool): Show grid on the y-axis.
|
||||
"""
|
||||
self.plt.showGrid(x, y)
|
||||
self.config.axis.x_grid = x
|
||||
self.config.axis.y_grid = y
|
||||
|
||||
def plot(self, data_x: list | np.ndarray, data_y: list | np.ndarray, label: str = None):
|
||||
"""
|
||||
Plot custom data on the plot widget. These data are not saved in config.
|
||||
Args:
|
||||
data_x(list|np.ndarray): x-axis data
|
||||
data_y(list|np.ndarray): y-axis data
|
||||
label(str): label of the plot
|
||||
"""
|
||||
self.plt.plot(data_x, data_y, name=label)
|
Reference in New Issue
Block a user