153 lines
6.2 KiB
Python
153 lines
6.2 KiB
Python
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() |