first commit
This commit is contained in:
159
streamlit_dashboard/page_classes/report_comparer_page_class.py
Normal file
159
streamlit_dashboard/page_classes/report_comparer_page_class.py
Normal file
@@ -0,0 +1,159 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user