import copy from typing import List, Dict, Any from copy import deepcopy from streamlit.delta_generator import DeltaGenerator from tests.test_init import kwargs from utility_classes.base_report_page import BaseReportPage from utility_classes.figure_with_max_y import FigureWithMaxY from page_classes.home_page_class import HomePage class ComparerPage(BaseReportPage): """ Orchestrates a dual-column layout to allow dynamic side-by-side report comparison. Leverages the BaseReportPage lifecycle to intercept data generation between two child columns. If identical report types are selected, it extracts their max values and synchronizes their Y-axes globally before allowing the final rendering phase, ensuring an accurate visual comparison. :param report_configs: Mapping of report page classes to their respective instantiation arguments. :type report_configs: dict """ def __init__(self, report_configs): super().__init__("comparer-page") # Ensure that the popping of homepage and comparer page do not affect the instantiation of those two pages self.report_configs = copy.deepcopy(report_configs) self.report_configs.pop(HomePage, None) self.report_configs.pop(ComparerPage, None) self.report_name_map = {} for report_page in self.report_configs.keys(): self.report_name_map[report_page.get_page_name()] = report_page def render(self, container: DeltaGenerator): """ Splits the layout and manages the synchronization pipeline for both child reports. :param container: The main Streamlit container allocated for the comparer. :type container: DeltaGenerator """ left_col, right_col = container.columns([0.5, 0.5]) left_comparer = ComparerColumn(self.report_name_map, self.report_configs, "left") right_comparer = ComparerColumn(self.report_name_map, self.report_configs, "right") # Render the controls of each report left_params = left_comparer.render_controls(left_col) right_params = right_comparer.render_controls(right_col) # Here we are going to generate the graph figures for the reports, and determine if they # have any graphs for which the y axis's should be matched if left_params and right_params: left_outputs = left_comparer.generate_figures(left_params) right_outputs = right_comparer.generate_figures(right_params) if left_outputs and right_outputs: left_instance = left_comparer.selected_report_instance right_instance = right_comparer.selected_report_instance # Only sync if the user is comparing two of the same report type if type(left_instance) == type(right_instance): # Agnostically ask the report which figures to sync keys_to_sync = left_instance.get_syncable_figure_keys() for key in keys_to_sync: left_fig:FigureWithMaxY = left_outputs.get(key) right_fig:FigureWithMaxY = right_outputs.get(key) if left_fig and right_fig: # Get the global max global_max = max(left_fig['max_y'], right_fig['max_y']) # Apply the sync with a 5% buffer at the top if global_max > 0: y_range = [0, global_max * 1.05] left_fig['figure'].update_layout(yaxis=dict(range=y_range)) right_fig['figure'].update_layout(yaxis=dict(range=y_range)) # Render the outputs, regardless of anything had to be synced left_comparer.render_figures(left_col, left_outputs) right_comparer.render_figures(right_col, right_outputs) class ComparerColumn(BaseReportPage): """ Acts as an interactive proxy wrapper for a single report within a comparison layout. Handles dynamic instantiation of a user-selected report and forcibly overrides its instance ID. This guarantees that Streamlit widget keys remain completely unique even if the user selects the exact same report class in both comparison columns. :param report_name_map: Dictionary mapping UI display names to BaseReportPage classes. :type report_name_map: dict :param report_configs: Dictionary mapping classes to their config keyword arguments. :type report_configs: dict :param column_id: An identifier (e.g., 'left', 'right') prepended to prevent widget collisions. :type column_id: str """ def __init__(self, report_name_map: dict, report_configs: dict, column_id: str): super().__init__(f"comparer-column-{column_id}") self.report_name_map = report_name_map self.report_name_map.pop(HomePage, None) self.report_configs = report_configs self.column_id = column_id self.selected_report_instance = None def render_controls(self, container: DeltaGenerator) -> Dict[str, Any]: """ Draws a selection menu to instantiate the desired report, then renders its specific controls. :param container: The Streamlit column container. :type container: DeltaGenerator :return: The parameters generated by the selected report's controls. :rtype: Dict[str, Any] """ selected_report = container.selectbox( label="Select the report to render", options=list(self.report_name_map.keys()), key=self.get_widget_key("report_selectbox") ) report_page_class = self.report_name_map[selected_report] report_page_parameters = self.report_configs[report_page_class] self.selected_report_instance = report_page_class(**report_page_parameters) self.selected_report_instance.instance_id = f'{self.column_id}_{selected_report}' return self.selected_report_instance.render_controls(container) def generate_figures(self, parameters: Dict[str, Any]) -> Dict[str, Any]: """ Delegates figure generation to the currently instantiated report. :param parameters: User inputs specific to the selected report. :type parameters: Dict[str, Any] :return: The output data payload from the selected report. :rtype: Dict[str, Any] """ if self.selected_report_instance and parameters is not None: return self.selected_report_instance.generate_figures(parameters) return None def render_figures(self, container: DeltaGenerator, output_data: Dict[str, Any]): """ Delegates the final drawing phase to the currently instantiated report. :param container: The Streamlit column container. :type container: DeltaGenerator :param output_data: The display data, potentially mutated by the parent ComparerPage. :type output_data: Dict[str, Any] """ if self.selected_report_instance and output_data is not None: self.selected_report_instance.render_figures(container, output_data)