first commit
This commit is contained in:
153
streamlit_dashboard/utility_classes/base_report_page.py
Normal file
153
streamlit_dashboard/utility_classes/base_report_page.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import logging
|
||||
from typing import List, Any, Dict
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import streamlit as st
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
from utility_classes.dashboard_config_parser import DashboardConfig
|
||||
from streamlit_constants import DASHBOARD_CONFIG_OBJECT_KEY
|
||||
|
||||
class Renderable(ABC):
|
||||
"""
|
||||
Abstract base class for UI components drawn to a Streamlit container.
|
||||
|
||||
Provides a strict contract for rendering and manages unique identifier generation.
|
||||
This structure is critical in Streamlit to prevent widget state collisions
|
||||
when multiple instances of the same UI component are rendered simultaneously.
|
||||
|
||||
:param instance_id: A unique identifier for this specific rendered instance.
|
||||
:type instance_id: str
|
||||
"""
|
||||
def __init__(self, instance_id: str):
|
||||
self.instance_id = instance_id
|
||||
|
||||
@abstractmethod
|
||||
def render(self, container: DeltaGenerator):
|
||||
"""
|
||||
Renders the component's UI elements onto the specified Streamlit container.
|
||||
|
||||
:param container: The Streamlit layout element where this component will be drawn.
|
||||
:type container: DeltaGenerator
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_widget_key(self, widget_unique_id: str) -> str:
|
||||
"""
|
||||
Generates a globally unique Streamlit widget key for this specific instance.
|
||||
|
||||
:param widget_unique_id: The local identifier for the specific widget.
|
||||
:type widget_unique_id: str
|
||||
:return: A concatenated string combining the instance ID and widget ID.
|
||||
:rtype: str
|
||||
"""
|
||||
return f'{self.instance_id}_{widget_unique_id}'
|
||||
|
||||
class BaseReportPage(Renderable):
|
||||
"""
|
||||
Standardizes the rendering pipeline for all analytical report pages.
|
||||
|
||||
This class breaks down UI generation into a predictable, synchronous lifecycle:
|
||||
1. Render controls (capture inputs).
|
||||
2. Generate figures (process data).
|
||||
3. Render figures (display outputs).
|
||||
This strict pipeline allows higher-level orchestrators to intercept the process
|
||||
mid-cycle (e.g., to mutate generated figures before they are drawn).
|
||||
|
||||
:param title: The title of the page, acting as its base instance ID.
|
||||
:type title: str
|
||||
"""
|
||||
def __init__(self, title:str):
|
||||
# set the instance id to the title of the page
|
||||
super().__init__(title)
|
||||
self.title = title
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.app_config:DashboardConfig = st.session_state[DASHBOARD_CONFIG_OBJECT_KEY]
|
||||
|
||||
@staticmethod
|
||||
def get_page_name():
|
||||
"""
|
||||
Retrieves the human-readable name of the report for UI selection menus.
|
||||
|
||||
:return: The display name of the report.
|
||||
:rtype: str
|
||||
"""
|
||||
return ""
|
||||
|
||||
def render_controls(self, container: DeltaGenerator) -> Dict[str, Any]:
|
||||
"""
|
||||
Renders the input widgets required to parameterize the report.
|
||||
|
||||
:param container: The Streamlit container to draw the widgets onto.
|
||||
:type container: DeltaGenerator
|
||||
:return: A dictionary of user-selected parameters, or None if inputs are invalid.
|
||||
:rtype: Dict[str, Any]
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def generate_figures(self, parameters: Dict[str, Any]):
|
||||
"""
|
||||
Processes parameters and generates the underlying data/figures for the report.
|
||||
|
||||
:param parameters: The dictionary of user inputs captured from render_controls.
|
||||
:type parameters: Dict[str, Any]
|
||||
:return: A dictionary of computed outputs or Plotly figures.
|
||||
:rtype: Dict[str, Any]
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def render_figures(self, container: DeltaGenerator, output_data: Dict[str, Any]):
|
||||
"""
|
||||
Draws the previously generated figures and data onto the screen.
|
||||
|
||||
:param container: The Streamlit container to draw the visuals onto.
|
||||
:type container: DeltaGenerator
|
||||
:param output_data: The computed results returned by generate_figures.
|
||||
:type output_data: Dict[str, Any]
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_syncable_figure_keys(self) -> List[str]:
|
||||
"""
|
||||
Identifies which generated figures support external axis synchronization for when
|
||||
two of the same report are displayed side by side.
|
||||
|
||||
This should really only be used for graphs for which axis synchronization makes sense
|
||||
(like bar charts that show quantities not percentages)
|
||||
|
||||
:return: A list of dictionary keys corresponding to figures in output_data
|
||||
that can have their Y-axes scaled dynamically by a parent orchestrator.
|
||||
:rtype: List[str]
|
||||
"""
|
||||
# Default to no synchronization
|
||||
return []
|
||||
|
||||
def render(self, container: DeltaGenerator):
|
||||
"""
|
||||
Executes the standardized report lifecycle.
|
||||
|
||||
Sequentially chains control rendering, data generation, and figure drawing,
|
||||
passing state safely between each phase and halting if user parameters are missing.
|
||||
|
||||
:param container: The Streamlit layout container for the entire report.
|
||||
:type container: DeltaGenerator
|
||||
"""
|
||||
parameters = self.render_controls(container)
|
||||
|
||||
# Only proceed if the user has provided valid inputs
|
||||
if parameters is not None:
|
||||
print("Got parameters")
|
||||
output_data = self.generate_figures(parameters)
|
||||
if output_data is not None:
|
||||
print("got output data")
|
||||
self.render_figures(container, output_data)
|
||||
else:
|
||||
self.logger.error(
|
||||
"No output figures or objects were provided to render this page, if no renderable figures are desired, an empty dictionary should be returned.")
|
||||
st.error(
|
||||
f"No objects to render this page were provided. A detailed error has been added to the logs. {self.app_config.get_errors_contact_string()}")
|
||||
st.stop()
|
||||
else:
|
||||
self.logger.error("No parameters were provided to render this page, if no parameters are desired an empty dictionary should be returned.")
|
||||
st.error(f"No parameters to render this page were provided. A detailed error has been added to the logs. {self.app_config.get_errors_contact_string()}")
|
||||
st.stop()
|
||||
Reference in New Issue
Block a user