diff --git a/src/gui/plotter.py b/src/gui/plotter.py index 04eb29b..9b9975a 100644 --- a/src/gui/plotter.py +++ b/src/gui/plotter.py @@ -1,8 +1,8 @@ import copy import traceback import sys -from typing import Optional, Tuple, List, Any -from dataclasses import dataclass +from typing import Optional, Tuple, Callable, List, Any +from dataclasses import dataclass, field from PyQt5.QtWidgets import ( QWidget, @@ -28,8 +28,7 @@ class ChannelTimings(): ideal_time: float = 0.0 client_time: float = 0.0 worst_performance: float = 2 - worst_timeframe: list[float] = [0, 0] - + worst_timeframe: list = field(default_factory=lambda: [0, 0]) class PlotWidget(BasePlotWidget): @@ -43,29 +42,24 @@ class PlotWidget(BasePlotWidget): widgets_datapack = [self._build_widget(data_sample) for self._datastep, data_sample in enumerate(data)] except (KeyError, ValueError, TypeError) as e: - # Provide user-friendly log message, log traceback in debug. logger.error(f"Возникла проблема при сборке данных графика: {e}") logger.debug(traceback.format_exc()) error_label = QLabel("Произошла ошибка при формировании графиков. Пожалуйста, проверьте корректность данных.") widgets_datapack = [error_label] except Exception as e: - # Catch all remaining exceptions. logger.error(f"Непредвиденная ошибка при формировании графиков: {e}") logger.debug(traceback.format_exc()) error_label = QLabel("Непредвиденная ошибка при формировании графиков.") widgets_datapack = [error_label] finally: - # Notify mediator self._mediator.notify(self, widgets_datapack) - def _add_performance_label(self, - layout: QVBoxLayout, + def _build_performance_label(self, timings: ChannelTimings, - qt_items: dict) -> None: + qt_items: dict) -> QWidget: """ Добавляет QLabel с информацией о производительности. """ - # TODO: Почему PlotWidget создает Label? Вынести в другое место. tesla_TWC = round((1 - timings.TWC_time/timings.client_time)*100, 2) if timings.client_time else 0.0 tesla_ideal = round((1 - timings.ideal_time/timings.client_time)*100, 2) if timings.client_time else 0.0 TWC_ideal = round((timings.ideal_time/timings.TWC_time)*100, 2) if timings.TWC_time else 0.0 @@ -85,53 +79,39 @@ class PlotWidget(BasePlotWidget): label_layout.addWidget(kdip_label, alignment=Qt.AlignLeft) spacer = QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Minimum) label_layout.addSpacerItem(spacer) - self.set_style(label_widget) - layout.addWidget(label_widget) qt_items["performance label"] = label_widget qt_items["real performance"] = real_label qt_items["ideal performance"] = ideal_label qt_items["real to ideal performance"] = kdip_label + return label_widget def _build_widget(self, graphic_passport: GraphicPassport) -> QWidget: - # TODO: Исходя из сигнатуры метода, он создает и возвращает виджет с графиками. - # Простыня "result_widget, reg_items, curve_items, qt_items" не похожа на виджет с графиком. """ Собирает графический виджет для одного набора данных. """ - container_widget, container_layout, plot_layout, pyqt_container = self._generate_widget_container() - plotter = PlotItemGenerator(graphic_passport, len(self._plt_channels), self._stage_colors) + container_widget, container_layout, pyqt_container = self._generate_widget_container() + plot_layout = CustomPlotLayout(graphic_passport, len(self._plt_channels), self._stage_colors, self) + plot_layout.build(pyqt_container, self._plt_channels) - for widget_num, (channel, description) in enumerate(self._plt_channels.items()): - plot_item, plot_timings = plotter.generate_plot_item(widget_num, channel, description, pyqt_container) - - if widget_num == 0: - main_plot = plot_item - else: - # Связываем остальные графики с основным графиком - plot_item.setXLink(main_plot) - - if description["settings"]["performance"]: - self._add_performance_label(container_layout, plot_timings, pyqt_container.qt_items) - - plot_layout.addItem(plot_item, widget_num, 0) - - navigator = NavigatorPlot(plot_timings.worst_timeframe, main_plot) - if navigator is not None: - plot_layout.addItem(navigator, widget_num+1, 0) + if plot_layout.property("performance"): + perf_widget = self._build_performance_label( + plot_layout.property("performance"), + pyqt_container.qt_items + ) + container_layout.addWidget(perf_widget) container_layout.addWidget(plot_layout) container_widget.setProperty("pyqt_container", pyqt_container) return container_widget @staticmethod - def _generate_widget_container() -> Tuple[QWidget, QVBoxLayout, pg.GraphicsLayoutWidget, PlotItems]: + def _generate_widget_container() -> Tuple[QWidget, QVBoxLayout, PlotItems]: container_widget = QWidget() container_layout = QVBoxLayout(container_widget) - plot_layout = pg.GraphicsLayoutWidget() pyqt_container = PlotItems({"real":{}, "ideal":{}}, {"real":{}, "ideal":{}}, {}) - return (container_widget, container_layout, plot_layout, pyqt_container) + return (container_widget, container_layout, pyqt_container) def _update_status(self, widgsteps:int, pointsteps:int, cur_widg:int, cur_point:int): if self._datalen: @@ -148,14 +128,43 @@ class PlotWidget(BasePlotWidget): self._controller.update_progress(progress) +class CustomPlotLayout(pg.GraphicsLayoutWidget): + def __init__(self, + graphic_passport: GraphicPassport, + widget_steps: int, + colors: dict, parent: PlotWidget = None) -> None: + super().__init__() + self._plotter = PlotItemGenerator(graphic_passport, widget_steps, colors, parent) + self.setProperty("performance", None) + + def build(self, pyqt_container:PlotItems, plt_channels:dict) -> None: + + for widget_num, (channel, description) in enumerate(plt_channels.items()): + plot_item, plot_timings = self._plotter.generate_plot_item(widget_num, channel, description, pyqt_container) + if widget_num == 0: + main_plot = plot_item + else: + plot_item.setXLink(main_plot) + + if description["Settings"]["performance"]: + self.setProperty("performance", plot_timings) + + self.addItem(plot_item, widget_num, 0) + + navigator = NavigatorPlot(plot_timings.worst_timeframe, main_plot) + if navigator is not None: + self.addItem(navigator, widget_num+1, 0) + + class PlotItemGenerator: def __init__(self, graphic_passport: GraphicPassport, widget_steps: int, - colors: dict) -> None: + colors: dict, parent:PlotWidget = None) -> None: self._stage_colors = colors self._ideal_mode = graphic_passport.dataframe is None + self._parent = parent if not self._ideal_mode: dataframe_headers = graphic_passport.dataframe.columns.tolist() @@ -203,9 +212,8 @@ class PlotItemGenerator: is_last = (cur_point == len(points_pocket) - 1) if self._ideal_mode: - timings, point_events, point_data.timeframe = self._generate_synthetic_events(timings, ideal_data) + timings, point_data.events, point_data.timeframe = self._generate_synthetic_events(timings, ideal_data) else: - if settings["force compensation FE"]: force = point_data.useful_data["force"] k_hardness = useful_data.k_hardness @@ -217,23 +225,23 @@ class PlotItemGenerator: if settings["force accuracy"]: force = point_data.useful_data["force"] - self._add_force_accuracy_region(point_events["Welding"], force, plot_item) + self._add_force_accuracy_region(point_data.events["Welding"], force, plot_item) if settings["ideals"] and settings["mirror ME"]: - for stage in point_events.keys(): + for stage in point_data.events.keys(): ideal_data[stage] = self._shift_data("ME", description["Ideal_signals"], ideal_data[stage], lambda x: useful_data.range_ME-x) if settings["workpiece"]: self._add_workpiece(point_data, plot_item) if settings["ideals"]: - self._add_ideal_stage_regions(plot_item, ideal_data, point_events, pyqt_container.regions, 100) - self._add_ideal_signals(plot_item, legend, ideal_data, point_events, description["Ideal_signals"], pyqt_container.curves, is_last) + self._add_ideal_stage_regions(plot_item, ideal_data, point_data.events, pyqt_container.regions, 100) + self._add_ideal_signals(plot_item, legend, ideal_data, point_data.events, description["Ideal_signals"], pyqt_container.curves, is_last) if settings["performance"]: timings = self._calc_performance(timings, point_data, ideal_data, is_last) - self._update_status(widget_steps, point_steps, widget_num, cur_point) + self._parent._update_status(widget_steps, point_steps, widget_num, cur_point) # Добавляем реальные сигналы if not self._ideal_mode: @@ -245,7 +253,7 @@ class PlotItemGenerator: def _shift_data(valid_str: str, signals: list[dict], dataframe: pd.DataFrame, - func: function) -> pd.DataFrame: + func: Callable) -> pd.DataFrame: keys = dataframe.keys() for signal in signals: if valid_str in signal["name"] and signal["name"] in keys: @@ -453,12 +461,14 @@ class PlotItemGenerator: dataframe.loc[point_idxs] = self._shift_data("FE", real_signals, dataframe.loc[point_idxs], lambda x: x + F_comp) return dataframe + class NavigatorPlot(pg.PlotItem): def __init__(self, time_region:tuple[float, float], main_plot: pg.PlotItem): super().__init__() self._init_navigator(time_region, main_plot) + self._init_syncranisation(main_plot) @staticmethod def _downsample_data(x:list, y:list, max_points=5000): @@ -495,6 +505,17 @@ class NavigatorPlot(pg.PlotItem): region.setRegion([x_min, x_max]) region.blockSignals(False) + def _init_syncranisation(self, main_plot: pg.PlotItem) -> None: + """ + Связывает изменения навигатора и других графиков друг с другом + """ + self.ROI_region.sigRegionChanged.connect( + lambda: self._sync_main_plot_with_navigator(main_plot, self.ROI_region) + ) + main_plot.sigXRangeChanged.connect( + lambda _, plot=main_plot, region=self.ROI_region: self._sync_navigator_with_main(main_plot=plot, region=region) + ) + def _init_navigator(self, time_region:tuple[float, float], main_plot: pg.PlotItem) -> None: @@ -517,8 +538,4 @@ class NavigatorPlot(pg.PlotItem): self.addItem(self.ROI_region) self.getViewBox().setLimits(xMin=0, xMax=x[-1]) - # Связываем изменение региона навигатора с обновлением области просмотра основного графика - self.ROI_region.sigRegionChanged.connect(lambda: self._sync_main_plot_with_navigator(main_plot, self.ROI_region)) - main_plot.sigXRangeChanged.connect(lambda _, plot=main_plot, region=self.ROI_region: self._sync_navigator_with_main(main_plot=plot, region=region)) - - \ No newline at end of file + \ No newline at end of file diff --git a/src/gui/start_widget.py b/src/gui/start_widget.py index 29a27cb..60a992a 100644 --- a/src/gui/start_widget.py +++ b/src/gui/start_widget.py @@ -125,7 +125,7 @@ class CustomTabWidget(QTabWidget): #TODO: переписать обмен данными, засунуть ссылки куда-то еще def create_tab(self, plot_widget:QWidget) -> None: - plot_items:PlotItems = plot_widget.property("plot_items") + plot_items:PlotItems = plot_widget.property("pyqt_container") tab = QWidget() tab.setProperty("reg_items", plot_items.regions) tab.setProperty("curve_items", plot_items.curves) diff --git a/src/main.py b/src/main.py index 3e0008a..ffd240c 100644 --- a/src/main.py +++ b/src/main.py @@ -18,8 +18,8 @@ def main(): monitor = DirectoryMonitor() file_manager = FileManager(monitor=monitor) data_converter = DataConverter() - plot_widget_builder = PlotWidget() controller = Controller(file_manager=file_manager) + plot_widget_builder = PlotWidget(controller=controller) passport_former = PassportFormer() window = MainWindow() mediator = Mediator(data_converter, passport_former, plot_widget_builder, controller, file_manager)