Files
testing123/streamlit_dashboard/page_classes/report_comparer_page_class.py
2026-05-21 08:40:24 -04:00

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)