160 lines
7.1 KiB
Python
160 lines
7.1 KiB
Python
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)
|