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

238 lines
11 KiB
Python

from typing import List, Dict, Any
import logging
import pandas as pd
from fiscalyear import FiscalYear
from streamlit.delta_generator import DeltaGenerator
import streamlit as st
from constants_module import TRAINING_COUNT_COLUMNS, NEOSERRA_COLUMNS, OUT_COLUMNS
from streamlit_constants import DASHBOARD_CONFIG_OBJECT_KEY
from utility_classes.base_report_page import BaseReportPage
from cached_function_wrappers.shared import get_df_centers
from cached_function_wrappers.trainings_cached_functions import cached_generate_cleaned_trainings_dataset
from section_1_graph_library_module import (
make_center_attendee_statistics_charts
)
from shared_tools_module import StatChartVariants
from utility_classes.figure_with_max_y import find_fig_max_y_and_generate_wrapper, FigureWithMaxY
from utility_classes.dashboard_config_parser import DashboardConfig, ExportModulePair
class TrainingEventAttendeeCounts(BaseReportPage):
"""
Concrete implementation of a report page analyzing network-wide training attendance.
This class manages the pipeline for comparing total event attendance against specific
subsets (like first-time and pre-planning attendees). It handles temporal filtering
and orchestrates multiple chart variants to provide both absolute and proportional metrics.
:param kwargs: Arbitrary keyword arguments passed to the parent BaseReportPage constructor.
"""
def __init__(self, **kwargs):
"""
Initializes temporal boundaries and establishes configuration state.
Captures the current and previous fiscal years for UI parameterization and extracts
the global application configuration from the session state to locate correct data endpoints.
:param kwargs: Arbitrary keyword arguments.
"""
super().__init__("Network Wide Training Event Attendee Counts")
self.fiscal_year = FiscalYear.current()
self.prev_fiscal_year = self.fiscal_year.prev_fiscal_year
self.fiscal_year_text = f'FY{str(self.fiscal_year.fiscal_year)[2:]}'
self.prev_fiscal_year_text = f'FY{str(self.prev_fiscal_year.fiscal_year)[2:]}'
# Grab the app config so we can use it to get the export module urls
self.app_config: DashboardConfig = st.session_state[DASHBOARD_CONFIG_OBJECT_KEY]
self.logger = logging.getLogger(__name__)
def get_fiscal_year_export_url(self, selected_fiscal_year):
"""
Resolves the specific dataset endpoint URL corresponding to the selected temporal state.
:param selected_fiscal_year: The formatted string representing the user's chosen fiscal year.
:type selected_fiscal_year: Any
:return: The URL for the trainings dataset export.
:rtype: str
"""
export_urls:ExportModulePair = self.app_config.get_trainings_urls()
if selected_fiscal_year == self.fiscal_year_text:
return export_urls.current_fy
else:
return export_urls.prev_fy
@staticmethod
def get_page_name():
"""
Provides the static human-readable identifier for this report component.
Used by the dashboard orchestrator to populate navigation elements.
:return: The display name of the report.
:rtype: str
"""
return "Training Event Attendee Counts"
def render_controls(self, container: DeltaGenerator) -> Dict[str, Any]:
"""
Defines the input state parameters and establishes a safe execution boundary.
Renders selection controls for fiscal year and target centers. Implements a fail-fast
pattern that halts the Streamlit execution tree if the underlying dataset fails to load,
preventing rendering errors in subsequent pipeline stages.
:param container: The Streamlit container to draw the input widgets onto.
:type container: DeltaGenerator
:return: A dictionary containing 'selected_fiscal_year' and 'selected_centers'.
:rtype: Dict[str, Any]
"""
report_settings_expander = container.expander(
label="Report Options",
expanded=True,
key=self.get_widget_key("report_settings_expander")
)
report_settings_expander.markdown("## Dataset Options")
report_settings_expander.markdown("These settings will modify the input dataset used to generate the graphs.")
selected_fiscal_year = report_settings_expander.selectbox(
label="Fiscal Year",
options=[self.prev_fiscal_year_text, self.fiscal_year_text],
index=1,
key=self.get_widget_key("selected_fiscal_year_selectbox")
)
reportable_only = report_settings_expander.checkbox(label="Reportable only?", value=True, key=self.get_widget_key("reportable_only_checkbox"))
include_future_events = report_settings_expander.checkbox(label="Include Future Events?", value=False, key=self.get_widget_key("include_future_events_checkbox"))
include_on_demand = report_settings_expander.checkbox(label="Include On-Demand Events?", value=True, key=self.get_widget_key("include_on_demand_checkbox"))
export_url = self.get_fiscal_year_export_url(selected_fiscal_year)
try:
all_centers = get_df_centers(export_url)
except Exception as e:
self.logger.exception(f"Failed to fetch the dataset for this page: {e}")
container.error(
"Failed to get the list of all centers for the dataset for this page. A detailed error message has been added to the logs.")
st.stop()
selected_centers = report_settings_expander.multiselect(label="Centers", options=all_centers,
default=all_centers,
key=self.get_widget_key("selected_centers_multiselect"))
return {
"selected_fiscal_year":selected_fiscal_year,
"selected_centers":selected_centers,
"reportable_only":reportable_only,
"include_future_events":include_future_events,
"include_on_demand":include_on_demand
}
def generate_figures(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
"""
Executes the data processing pipeline and constructs the visualization artifacts.
Fetches the cleaned trainings dataset and generates a suite of chart variants (both
absolute counts and percentages). It actively computes the maximum Y-axis boundaries
for the quantity-based charts to enable parent orchestrators to synchronize external axes.
:param parameters: The parameter state dictionary captured from render_controls.
:type parameters: Dict[str, Any]
:return: A dictionary mapping identifiers to FigureWithMaxY objects and the raw dataframe.
:rtype: Dict[str, Any]
"""
selected_fiscal_year: str = parameters["selected_fiscal_year"]
selected_centers: List[str] = parameters["selected_centers"]
reportable_only:bool = parameters["reportable_only"]
include_future_events:bool = parameters["include_future_events"]
include_on_demand:bool = parameters["include_on_demand"]
export_url = self.get_fiscal_year_export_url(selected_fiscal_year)
trainings_df = cached_generate_cleaned_trainings_dataset(
export_url,
reportable_only=reportable_only,
allowed_centers=selected_centers,
include_future_events=include_future_events,
include_on_demand=include_on_demand
)
try:
attendee_figs = make_center_attendee_statistics_charts(
trainings_df,
title_prefix="PASBDC*",
fiscal_year_tag=selected_fiscal_year,
col_neo_center=NEOSERRA_COLUMNS.center,
col_neo_attendees_total=NEOSERRA_COLUMNS.attendees_total,
col_neo_primary_topic=NEOSERRA_COLUMNS.primary_training_topic,
preplanning_val=OUT_COLUMNS.val_preplanning
)
except Exception as e:
self.logger.exception(f"Failed to generate figures for this page: {e}")
st.error(f"Failed to generate the figures for this page. A detailed error has been added to the logs. {self.app_config.get_errors_contact_string()}")
st.stop()
return {
'attendees_total': find_fig_max_y_and_generate_wrapper(attendee_figs[StatChartVariants.TOTAL_COUNT]),
'attendees_percent': FigureWithMaxY(figure=attendee_figs[StatChartVariants.TOTAL_PERCENT]),
'attendees_first_pre_only': find_fig_max_y_and_generate_wrapper(attendee_figs[StatChartVariants.FIRST_AND_PREPLANNING_ONLY]),
'attendees_first_pre_only_percent': FigureWithMaxY(figure=attendee_figs[StatChartVariants.FIRST_AND_PREPLANNING_ONLY_PERCENT], max_y=0.0),
'trainings_df':trainings_df
}
def render_figures(self, container: DeltaGenerator, output_data: Dict[str, Any]):
"""
Maps the generated visual artifacts to a defined spatial layout within the Streamlit UI.
Arranges the absolute count and percentage charts into a comparative 2x2 grid layout
for immediate visual contrast, and exposes the underlying dataset via an expander for auditing.
:param container: The Streamlit layout container for the visuals.
:type container: DeltaGenerator
:param output_data: The dictionary of computed figures and dataframes from generate_figures.
:type output_data: Dict[str, Any]
"""
attendees_total = output_data.get("attendees_total")["figure"]
attendees_percent = output_data.get("attendees_percent")["figure"]
attendees_first_pre_only = output_data.get("attendees_first_pre_only")["figure"]
attendees_first_pre_only_percent = output_data.get("attendees_first_pre_only_percent")["figure"]
trainings_df:pd.DataFrame = output_data.get("trainings_df")
left_col, right_col = container.columns([0.5,0.5])
left_col.plotly_chart(attendees_total, key=self.get_widget_key("attendees_total"))
right_col.plotly_chart(attendees_percent, key=self.get_widget_key("attendees_percent"))
left_col, right_col = container.columns([0.5,0.5])
left_col.plotly_chart(attendees_first_pre_only, key=self.get_widget_key("attendees_first_pre_only"))
right_col.plotly_chart(attendees_first_pre_only_percent, key=self.get_widget_key("attendees_first_pre_only_percent"))
dataset_expander = container.expander(
label="Source Dataset",
expanded=True,
key=self.get_widget_key("dataset_expander")
)
dataset_expander.markdown("## Source Data")
dataset_expander.markdown("### Neoserra Trainings Dataset")
dataset_expander.write(trainings_df)
def get_syncable_figure_keys(self) -> List[str]:
"""
Declares the specific visualization objects that support dynamic external Y-axis scaling.
Explicitly filters out the percentage-based charts to ensure external synchronization
is only applied to absolute quantity charts, preventing visual distortion.
:return: A list of dictionary keys corresponding to absolute count figures.
:rtype: List[str]
"""
return ["attendees_total", "attendees_first_pre_only"]