mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-09 02:07:55 +01:00
refactor: changed the .yaml structure and the logic of the whole app how to access
This commit is contained in:
@@ -1,32 +1,46 @@
|
||||
plot_settings:
|
||||
background_color: "white"
|
||||
num_columns: 2
|
||||
|
||||
xy_pairs: [["samx", ["gauss_bpm", "gauss_adc1"]],
|
||||
["samx", ["gauss_adc1", "gauss_adc2"]]]
|
||||
background_color: "black"
|
||||
num_columns: 3
|
||||
colormap: "plasma" #TODO has to be hooked up to the plot
|
||||
|
||||
plot_data:
|
||||
- BPM plot:
|
||||
- x:
|
||||
- signal:
|
||||
- name: "samx"
|
||||
- entry: "samx"
|
||||
- label: 'Motor X' # will serve as x label
|
||||
- y:
|
||||
- signal:
|
||||
- name: "gauss_bpm"
|
||||
- entry: "gauss_bpm"
|
||||
- label: 'BPM' # will serve as y label
|
||||
- plot_name: "BPM plot"
|
||||
x:
|
||||
label: 'Motor X'
|
||||
signals:
|
||||
- name: "samx"
|
||||
entry: "samx"
|
||||
y:
|
||||
label: 'BPM'
|
||||
signals:
|
||||
- name: "gauss_bpm"
|
||||
entry: "gauss_bpm"
|
||||
- name: "gauss_adc1"
|
||||
entry: "gauss_adc1"
|
||||
|
||||
- ADC plot:
|
||||
- name: "gauss_adc1"
|
||||
- x:
|
||||
- signal:
|
||||
- name: "samy"
|
||||
- entry: "samy"
|
||||
- label: 'Motor Y'
|
||||
- y:
|
||||
- signal:
|
||||
- name: "gauss_adc"
|
||||
- entry: ["gauss_adc1", "gauss_adc2"]
|
||||
- label: 'ADC'
|
||||
- plot_name: "ADC plot"
|
||||
x:
|
||||
label: 'Motor Y'
|
||||
signals:
|
||||
- name: "samy"
|
||||
# entry: "samy" # here I also forgot to specify entry
|
||||
y:
|
||||
label: 'ADC'
|
||||
signals:
|
||||
- name: "gauss_bpm"
|
||||
entry: "gauss_bpm"
|
||||
- name: "samx"
|
||||
# I will not specify entry, because I want to take hint from gauss_adc2
|
||||
- plot_name: "Multi"
|
||||
x:
|
||||
label: 'Motor X'
|
||||
signals:
|
||||
- name: "samx"
|
||||
entry: "samx"
|
||||
y:
|
||||
label: 'Multi'
|
||||
signals:
|
||||
- name: "gauss_bpm"
|
||||
entry: "gauss_bpm"
|
||||
- name: "samx"
|
||||
entry: ["samx","setpoint"] #multiple entries for one device
|
||||
@@ -14,38 +14,47 @@ from bec_widgets.qt_utils import Crosshair, Colors
|
||||
# TODO implement:
|
||||
# - implement scanID database for visualizing previous scans
|
||||
# - change how dap is handled in bec_dispatcher to handle more workers
|
||||
# - YAML config -> plot settings
|
||||
# - YAML config -> xy pairs -> multiple subsignals for different devices
|
||||
# - Internal logic -> if user specify
|
||||
|
||||
|
||||
class PlotApp(QWidget):
|
||||
"""
|
||||
Main class for PlotApp, designed to plot multiple signals in a grid layout.
|
||||
Main class for PlotApp, designed to plot multiple signals in a grid layout
|
||||
based on a flexible YAML configuration.
|
||||
|
||||
Attributes:
|
||||
update_signal (pyqtSignal): Signal to trigger plot updates.
|
||||
xy_pairs (list of tuples): List of tuples containing x-y pairs for each plot.
|
||||
Each tuple has the x-value as its first element and
|
||||
a list of y-values as its second element.
|
||||
plot_data (list of dict): List of dictionaries containing plot configurations.
|
||||
Each dictionary specifies x and y signals, including their
|
||||
name and entry, for a particular plot.
|
||||
|
||||
Args:
|
||||
xy_pairs (list of lists): List of x-y pairs specifying the signals to plot.
|
||||
Each tuple consists of an x-value string and a list
|
||||
of y-value strings.
|
||||
Example: [["x1", ["y1", "y2"]], ["x2", ["y3"]]]
|
||||
plot_settings (dict): Dictionary containing global plot settings such as background color.
|
||||
plot_data (list of dict): List of dictionaries specifying the signals to plot.
|
||||
Each dictionary should contain:
|
||||
- 'x': Dictionary specifying the x-axis settings including
|
||||
a 'signals' list with 'name' and 'entry' fields.
|
||||
If there are multiple entries for one device name, they can be passed as a list.
|
||||
- 'y': Similar to 'x', but for the y-axis.
|
||||
Example:
|
||||
[
|
||||
{
|
||||
'plot_name': 'Plot 1',
|
||||
'x': {'label': 'X Label', 'signals': [{'name': 'x1', 'entry': 'x1_entry'}]},
|
||||
'y': {'label': 'Y Label', 'signals': [{'name': 'y1', 'entry': 'y1_entry'}]}
|
||||
},
|
||||
...
|
||||
]
|
||||
parent (QWidget, optional): Parent widget.
|
||||
"""
|
||||
|
||||
update_signal = pyqtSignal()
|
||||
update_dap_signal = pyqtSignal()
|
||||
|
||||
def __init__(self, plot_settings: dict, xy_pairs: list, plot_data: dict, parent=None):
|
||||
def __init__(self, plot_settings: dict, plot_data: list, parent=None):
|
||||
super(PlotApp, self).__init__(parent)
|
||||
|
||||
# YAML config
|
||||
self.plot_settings = plot_settings
|
||||
self.xy_pairs = xy_pairs
|
||||
self.plot_data = plot_data
|
||||
|
||||
# Setting global plot settings
|
||||
@@ -111,34 +120,35 @@ class PlotApp(QWidget):
|
||||
"""
|
||||
self.glw.clear()
|
||||
self.plots = {}
|
||||
self.grid_coordinates = [] # List to keep track of grid positions for each plot
|
||||
self.grid_coordinates = []
|
||||
|
||||
num_plots = len(self.xy_pairs)
|
||||
num_rows = num_plots // num_columns # Calculate the number of full rows
|
||||
last_row_cols = num_plots % num_columns # Number of plots in the last row
|
||||
remaining_space = num_columns - last_row_cols # Remaining space in the last row
|
||||
num_plots = len(self.plot_data)
|
||||
num_rows = num_plots // num_columns
|
||||
last_row_cols = num_plots % num_columns
|
||||
remaining_space = num_columns - last_row_cols
|
||||
|
||||
for i, (x, ys) in enumerate(self.xy_pairs):
|
||||
row, col = i // num_columns, i % num_columns # Calculate grid position
|
||||
for i, plot_config in enumerate(self.plot_data):
|
||||
row, col = i // num_columns, i % num_columns
|
||||
colspan = 1
|
||||
|
||||
colspan = 1 # Default colspan
|
||||
|
||||
# Check if we are in the last row and there's remaining space
|
||||
if row == num_rows and remaining_space > 0:
|
||||
if last_row_cols == 1:
|
||||
colspan = num_columns # Stretch across all columns
|
||||
colspan = num_columns
|
||||
else:
|
||||
colspan = remaining_space // last_row_cols + 1 # Proportional stretch
|
||||
remaining_space -= colspan - 1 # Update remaining space
|
||||
last_row_cols -= 1 # Update remaining plots
|
||||
colspan = remaining_space // last_row_cols + 1
|
||||
remaining_space -= colspan - 1
|
||||
last_row_cols -= 1
|
||||
|
||||
plot = self.glw.addPlot(
|
||||
row=row, col=col, colspan=colspan, title=list(self.plot_data[i].keys())[0]
|
||||
)
|
||||
plot.setLabel("bottom", x)
|
||||
plot.setLabel("left", ", ".join(ys))
|
||||
plot_name = plot_config.get("plot_name", "")
|
||||
x_label = plot_config["x"].get("label", "")
|
||||
y_label = plot_config["y"].get("label", "")
|
||||
|
||||
plot = self.glw.addPlot(row=row, col=col, colspan=colspan, title=plot_name)
|
||||
plot.setLabel("bottom", x_label)
|
||||
plot.setLabel("left", y_label)
|
||||
plot.addLegend()
|
||||
self.plots[(x, tuple(ys))] = plot
|
||||
|
||||
self.plots[plot_name] = plot
|
||||
self.grid_coordinates.append((row, col))
|
||||
|
||||
self.init_curves()
|
||||
@@ -151,30 +161,44 @@ class PlotApp(QWidget):
|
||||
the curve objects for each x and y signal pair. It also updates the row labels
|
||||
in `self.tableWidget_crosshair` to include the grid position for each y-value.
|
||||
"""
|
||||
self.curves_data = {} # Nested dictionary to hold curves
|
||||
self.curves_data = {}
|
||||
row_labels = []
|
||||
|
||||
row_labels = [] # List to keep track of row labels for the table
|
||||
|
||||
for idx, ((x, ys), plot) in enumerate(self.plots.items()):
|
||||
for idx, plot_config in enumerate(self.plot_data):
|
||||
plot_name = plot_config.get("plot_name", "")
|
||||
plot = self.plots[plot_name]
|
||||
plot.clear()
|
||||
self.curves_data[(x, tuple(ys))] = []
|
||||
colors_ys = Colors.golden_angle_color(colormap="plasma", num=len(ys))
|
||||
|
||||
row, col = self.grid_coordinates[idx] # Retrieve the grid position for this plot
|
||||
y_configs = plot_config["y"]["signals"]
|
||||
colors_ys = Colors.golden_angle_color(
|
||||
colormap=self.plot_settings["colormap"], num=len(y_configs)
|
||||
)
|
||||
|
||||
for i, (signal, color) in enumerate(zip(ys, colors_ys)):
|
||||
pen_curve = mkPen(color=color, width=2, style=QtCore.Qt.DashLine)
|
||||
brush_curve = mkBrush(color=color)
|
||||
curve_data = pg.PlotDataItem(
|
||||
symbolSize=5,
|
||||
symbolBrush=brush_curve,
|
||||
pen=pen_curve,
|
||||
skipFiniteCheck=True,
|
||||
name=f"{signal}",
|
||||
)
|
||||
self.curves_data[(x, tuple(ys))].append(curve_data)
|
||||
plot.addItem(curve_data)
|
||||
row_labels.append(f"{signal} - [{row},{col}]") # Add row label with grid position
|
||||
curve_list = []
|
||||
for i, (y_config, color) in enumerate(zip(y_configs, colors_ys)):
|
||||
y_name = y_config["name"]
|
||||
y_entries = y_config.get("entry", [y_name])
|
||||
|
||||
if not isinstance(y_entries, list):
|
||||
y_entries = [y_entries]
|
||||
|
||||
for y_entry in y_entries:
|
||||
pen_curve = mkPen(color=color, width=2, style=QtCore.Qt.DashLine)
|
||||
brush_curve = mkBrush(color=color)
|
||||
|
||||
curve_data = pg.PlotDataItem(
|
||||
symbolSize=5,
|
||||
symbolBrush=brush_curve,
|
||||
pen=pen_curve,
|
||||
skipFiniteCheck=True,
|
||||
name=f"{y_name} ({y_entry})",
|
||||
)
|
||||
|
||||
curve_list.append((y_name, y_entry, curve_data))
|
||||
plot.addItem(curve_data)
|
||||
row_labels.append(f"{y_name} ({y_entry}) - {plot_name}")
|
||||
|
||||
self.curves_data[plot_name] = curve_list
|
||||
|
||||
self.tableWidget_crosshair.setRowCount(len(row_labels))
|
||||
self.tableWidget_crosshair.setVerticalHeaderLabels(row_labels)
|
||||
@@ -182,8 +206,8 @@ class PlotApp(QWidget):
|
||||
|
||||
def hook_crosshair(self):
|
||||
"""Attach crosshairs to each plot and connect them to the update_table method."""
|
||||
self.crosshairs = {} # Store crosshairs for each plot
|
||||
for (x, ys), plot in self.plots.items():
|
||||
self.crosshairs = {}
|
||||
for plot_name, plot in self.plots.items():
|
||||
crosshair = Crosshair(plot, precision=3)
|
||||
crosshair.coordinatesChanged1D.connect(
|
||||
lambda x, y, plot=plot: self.update_table(
|
||||
@@ -195,7 +219,7 @@ class PlotApp(QWidget):
|
||||
self.tableWidget_crosshair, x, y, column=1, plot=plot
|
||||
)
|
||||
)
|
||||
self.crosshairs[(x, tuple(ys))] = crosshair
|
||||
self.crosshairs[plot_name] = crosshair
|
||||
|
||||
def update_table(
|
||||
self, table_widget: QTableWidget, x: float, y_values: list, column: int, plot: pg.PlotItem
|
||||
@@ -213,17 +237,18 @@ class PlotApp(QWidget):
|
||||
This method calculates the correct row in the table for each y-value
|
||||
and updates the cell at (row, column) with the new x and y coordinates.
|
||||
"""
|
||||
plot_key = [key for key, value in self.plots.items() if value == plot][0]
|
||||
_, ys = plot_key # Extract the y-values for the current plot
|
||||
plot_name = [name for name, value in self.plots.items() if value == plot][0]
|
||||
|
||||
# Find the starting row for the ys of the current plot
|
||||
starting_row = 0
|
||||
for _, other_ys in self.xy_pairs:
|
||||
if other_ys == list(ys):
|
||||
for plot_config in self.plot_data:
|
||||
if plot_config.get("plot_name", "") == plot_name:
|
||||
break
|
||||
starting_row += len(other_ys)
|
||||
for y_config in plot_config.get("y", {}).get("signals", []):
|
||||
y_entries = y_config.get("entry", [y_config.get("name", "")])
|
||||
if not isinstance(y_entries, list):
|
||||
y_entries = [y_entries]
|
||||
starting_row += len(y_entries)
|
||||
|
||||
# Update the table rows corresponding to the ys of the current plot
|
||||
for i, y in enumerate(y_values):
|
||||
row = starting_row + i
|
||||
table_widget.setItem(row, column, QTableWidgetItem(f"({x}, {y})"))
|
||||
@@ -231,10 +256,19 @@ class PlotApp(QWidget):
|
||||
|
||||
def update_plot(self) -> None:
|
||||
"""Update the plot data based on the stored data dictionary."""
|
||||
for (x, ys), curves in self.curves_data.items():
|
||||
data_x = self.data.get((x, tuple(ys)), {}).get("x", [])
|
||||
for i, curve in enumerate(curves):
|
||||
data_y = self.data.get((x, tuple(ys)), {}).get(ys[i], [])
|
||||
for plot_name, curve_list in self.curves_data.items():
|
||||
for y_name, y_entry, curve in curve_list:
|
||||
x_config = next(
|
||||
(pc["x"] for pc in self.plot_data if pc.get("plot_name") == plot_name), {}
|
||||
)
|
||||
x_signal_config = x_config["signals"][0]
|
||||
x_name = x_signal_config.get("name", "")
|
||||
x_entry = x_signal_config.get("entry", x_name)
|
||||
|
||||
key = (x_name, x_entry, y_name, y_entry)
|
||||
data_x = self.data.get(key, {}).get("x", [])
|
||||
data_y = self.data.get(key, {}).get("y", [])
|
||||
|
||||
curve.setData(data_x, data_y)
|
||||
|
||||
@pyqtSlot(dict, dict)
|
||||
@@ -252,18 +286,47 @@ class PlotApp(QWidget):
|
||||
|
||||
if current_scanID != self.scanID:
|
||||
self.scanID = current_scanID
|
||||
self.data = {} # Wipe the data for a new scan
|
||||
self.init_curves() # Re-initialize the curves
|
||||
self.data = {}
|
||||
self.init_curves()
|
||||
|
||||
for x, ys in self.xy_pairs:
|
||||
data_x = msg["data"].get(x, {}).get(x, {}).get("value", None)
|
||||
if data_x is not None:
|
||||
self.data.setdefault((x, tuple(ys)), {}).setdefault("x", []).append(data_x)
|
||||
for plot_config in self.plot_data:
|
||||
x_config = plot_config["x"]
|
||||
x_signal_config = x_config["signals"][0]
|
||||
x_name = x_signal_config.get("name", "")
|
||||
x_entry_list = x_signal_config.get("entry", [])
|
||||
|
||||
for y in ys:
|
||||
data_y = msg["data"].get(y, {}).get(y, {}).get("value", None)
|
||||
if data_y is not None:
|
||||
self.data.setdefault((x, tuple(ys)), {}).setdefault(y, []).append(data_y)
|
||||
if not x_entry_list:
|
||||
x_entry_list = dev[x_name]._hints if hasattr(dev[x_name], "_hints") else [x_name]
|
||||
|
||||
if not isinstance(x_entry_list, list):
|
||||
x_entry_list = [x_entry_list]
|
||||
|
||||
y_configs = plot_config["y"]["signals"]
|
||||
|
||||
for x_entry in x_entry_list:
|
||||
for y_config in y_configs:
|
||||
y_name = y_config.get("name", "")
|
||||
y_entry_list = y_config.get("entry", [])
|
||||
|
||||
if not y_entry_list:
|
||||
y_entry_list = (
|
||||
dev[y_name]._hints if hasattr(dev[y_name], "_hints") else [y_name]
|
||||
)
|
||||
|
||||
if not isinstance(y_entry_list, list):
|
||||
y_entry_list = [y_entry_list]
|
||||
|
||||
for y_entry in y_entry_list:
|
||||
key = (x_name, x_entry, y_name, y_entry)
|
||||
|
||||
data_x = msg["data"].get(x_name, {}).get(x_entry, {}).get("value", None)
|
||||
data_y = msg["data"].get(y_name, {}).get(y_entry, {}).get("value", None)
|
||||
|
||||
if data_x is not None:
|
||||
self.data.setdefault(key, {}).setdefault("x", []).append(data_x)
|
||||
|
||||
if data_y is not None:
|
||||
self.data.setdefault(key, {}).setdefault("y", []).append(data_y)
|
||||
|
||||
self.update_signal.emit()
|
||||
|
||||
@@ -286,7 +349,6 @@ if __name__ == "__main__":
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
plot_settings = config.get("plot_settings", {})
|
||||
xy_pairs = config.get("xy_pairs", [])
|
||||
plot_data = config.get("plot_data", {})
|
||||
except FileNotFoundError:
|
||||
print(f"The file {args.config} was not found.")
|
||||
@@ -295,8 +357,6 @@ if __name__ == "__main__":
|
||||
print(f"An error occurred while loading the config file: {e}")
|
||||
exit(1)
|
||||
|
||||
# TODO PUT RAISE ERROR HERE to check for xy_pairs
|
||||
|
||||
# BECclient global variables
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
@@ -306,7 +366,7 @@ if __name__ == "__main__":
|
||||
queue = client.queue
|
||||
|
||||
app = QApplication([])
|
||||
plotApp = PlotApp(xy_pairs=xy_pairs, plot_settings=plot_settings, plot_data=plot_data)
|
||||
plotApp = PlotApp(plot_settings=plot_settings, plot_data=plot_data)
|
||||
|
||||
# Connecting signals from bec_dispatcher
|
||||
bec_dispatcher.connect_slot(plotApp.on_scan_segment, MessageEndpoints.scan_segment())
|
||||
|
||||
Reference in New Issue
Block a user