first commit
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
import sys
|
||||
from typing import List, Dict, Any
|
||||
import logging
|
||||
|
||||
import pandas as pd
|
||||
from fiscalyear import FiscalYear
|
||||
from matplotlib.pyplot import figure
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
import streamlit as st
|
||||
from plotly.graph_objects import Figure
|
||||
|
||||
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_primary_training_topic_pie_charts,
|
||||
make_primary_training_topic_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 TrainingsPrimaryTopicsPage(BaseReportPage):
|
||||
"""
|
||||
Concrete implementation of a report page analyzing network-wide primary training topics.
|
||||
|
||||
This class manages the data pipeline for evaluating the distribution of course subjects
|
||||
across the network. It employs a dual-visualization strategy: utilizing pie charts to
|
||||
show massive macro-proportions (like the dominance of introductory courses) and bar charts
|
||||
to provide granular visibility into smaller, niche training topics that would otherwise
|
||||
be unreadable in a proportion-based chart.
|
||||
|
||||
:param kwargs: Arbitrary keyword arguments passed to the parent BaseReportPage constructor.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Initializes the fiscal period boundaries and application configuration state.
|
||||
|
||||
Captures the current and previous fiscal years to manage the report's time-based
|
||||
filtering logic and extracts the global dashboard configuration to resolve the
|
||||
necessary external data endpoints.
|
||||
|
||||
:param kwargs: Arbitrary keyword arguments.
|
||||
"""
|
||||
|
||||
super().__init__("Network Wide Training Primary Topics")
|
||||
|
||||
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 external dataset endpoint based on the active fiscal period.
|
||||
|
||||
Maps the user's selected fiscal year to the appropriate data URL, ensuring the data
|
||||
pipeline fetches the correct historical or current training topic records.
|
||||
|
||||
:param selected_fiscal_year: The formatted string representing the user's chosen fiscal year.
|
||||
:type selected_fiscal_year: Any
|
||||
:return: The URL for the corresponding 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 display identifier for this report module.
|
||||
|
||||
Utilized by dashboard orchestrators to construct routing and navigation menus.
|
||||
|
||||
:return: The human-readable name of the report.
|
||||
:rtype: str
|
||||
"""
|
||||
return "Training Primary Topics"
|
||||
|
||||
def render_controls(self, container: DeltaGenerator) -> Dict[str, Any]:
|
||||
"""
|
||||
Defines the user input interface and establishes a safe execution boundary.
|
||||
|
||||
Renders selection widgets for the fiscal period and target centers. Implements a strict
|
||||
fail-fast pattern that halts the Streamlit execution sequence if the baseline dataset
|
||||
fails to load, preventing downstream visual rendering errors.
|
||||
|
||||
:param container: The Streamlit container to attach the input widgets to.
|
||||
:type container: DeltaGenerator
|
||||
:return: A dictionary containing the user-selected fiscal year and 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 analytical data pipeline and constructs the visualization objects.
|
||||
|
||||
Fetches the trainings dataset and generates two distinct sets of figures: pie charts
|
||||
for overall topic proportions and bar charts targeting smaller volume topics. Computes
|
||||
strict max-Y values exclusively for the absolute count bar charts to support external
|
||||
dynamic axis synchronization.
|
||||
|
||||
:param parameters: The parameter state dictionary captured from the render_controls phase.
|
||||
: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:
|
||||
topic_figs = make_primary_training_topic_statistics_charts(
|
||||
trainings_df,
|
||||
center="Network Wide",
|
||||
network_label="PASBDC*",
|
||||
fiscal_year_tag=selected_fiscal_year,
|
||||
col_neo_primary_topic=NEOSERRA_COLUMNS.primary_training_topic,
|
||||
col_neo_attendees_total=NEOSERRA_COLUMNS.attendees_total
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Failed to generate the topic bar figures for this page. Got {e}")
|
||||
st.error(f"Failed to generate the topic bar figures for this page. A detailed error has been added to the logs. {self.app_config.get_errors_contact_string()}")
|
||||
st.stop()
|
||||
try:
|
||||
topic_pies = make_primary_training_topic_pie_charts(
|
||||
trainings_df,
|
||||
center="Network Wide",
|
||||
network_label="PASBDC*",
|
||||
fiscal_year_tag=selected_fiscal_year,
|
||||
col_neo_attendees_total=NEOSERRA_COLUMNS.attendees_total,
|
||||
col_neo_primary_topic=NEOSERRA_COLUMNS.primary_training_topic
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Failed to generate the topic pie charts for this page. Got {e}.")
|
||||
st.error(f"Failed to generate the topic pie charts for this page. A detailed error has been added to the logs. {self.app_config.get_errors_contact_string()}")
|
||||
st.stop()
|
||||
|
||||
return {
|
||||
'topic_pie_total': FigureWithMaxY(figure=topic_pies[StatChartVariants.TOTAL_PERCENT], max_y=0.0),
|
||||
'topic_pie_no_first': FigureWithMaxY(figure=topic_pies[StatChartVariants.NO_FIRST_STEPS_PERCENT], max_y=0.0),
|
||||
'small_topics_total': find_fig_max_y_and_generate_wrapper(topic_figs[StatChartVariants.SMALL_BARS_TRAININGS]),
|
||||
'small_topics_percent': FigureWithMaxY(figure=topic_figs[StatChartVariants.SMALL_BARS_TRAININGS_PERCENT], max_y=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 UI.
|
||||
|
||||
Arranges the charts sequentially using a 2x2 grid layout, placing macro-level pie charts
|
||||
on top and granular bar charts on the bottom. This spatial grouping allows users to easily
|
||||
contrast total training volumes with specialized subsets. Exposes the raw underlying dataset
|
||||
via an expander module for data 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]
|
||||
"""
|
||||
|
||||
topic_pie_total = output_data.get("topic_pie_total")["figure"]
|
||||
topic_pie_no_first = output_data.get("topic_pie_no_first")["figure"]
|
||||
small_topics_total = output_data.get("small_topics_total")["figure"]
|
||||
small_topics_percent = output_data.get("small_topics_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(topic_pie_total, key=self.get_widget_key("topic_pie_total"))
|
||||
right_col.plotly_chart(topic_pie_no_first, key=self.get_widget_key("topic_pie_no_first"))
|
||||
left_col.plotly_chart(small_topics_total, key=self.get_widget_key("small_topics_total"))
|
||||
right_col.plotly_chart(small_topics_percent, key=self.get_widget_key("small_topics_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 figures that permit dynamic external Y-axis scaling.
|
||||
|
||||
Explicitly isolates the absolute count bar chart ('small_topics_total') for synchronization.
|
||||
It intentionally filters out pie charts and percentage-based bar charts, as applying external
|
||||
axis limits to these would completely break their visual representation.
|
||||
|
||||
:return: A list of dictionary keys corresponding to absolute count figures.
|
||||
:rtype: List[str]
|
||||
"""
|
||||
return ["small_topics_total"]
|
||||
Reference in New Issue
Block a user