From 96a88d23154d7f5578ee742c91feb658a74d7ede Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 1 Sep 2023 00:59:11 +0200 Subject: [PATCH] refactor: changed the .yaml structure and the logic of the whole app how to access --- bec_widgets/examples/extreme/config.yaml | 70 ++++--- bec_widgets/examples/extreme/extreme.py | 222 ++++++++++++++--------- 2 files changed, 183 insertions(+), 109 deletions(-) diff --git a/bec_widgets/examples/extreme/config.yaml b/bec_widgets/examples/extreme/config.yaml index 6a66bbd3..38c704ce 100644 --- a/bec_widgets/examples/extreme/config.yaml +++ b/bec_widgets/examples/extreme/config.yaml @@ -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' \ No newline at end of file + - 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 \ No newline at end of file diff --git a/bec_widgets/examples/extreme/extreme.py b/bec_widgets/examples/extreme/extreme.py index 5c21f6b0..4d5cd430 100644 --- a/bec_widgets/examples/extreme/extreme.py +++ b/bec_widgets/examples/extreme/extreme.py @@ -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())