"""Page 2: Executive Compensation.""" import dash import duckdb from dash import html, dcc, Input, Output, dash_table import plotly.express as px import plotly.graph_objects as go from admin_analytics.dashboard.queries import ( query_top_earners, query_comp_by_role, query_comp_vs_cpi, query_comp_cagr, query_aggregate_comp, query_aggregate_comp_cagr, ) _NO_DATA = html.Div( "No IRS 990 data loaded. Run: admin-analytics ingest irs990", style={"textAlign": "center", "padding": "40px", "color": "#888"}, ) # Roles to highlight in trend chart _KEY_ROLES = ["PRESIDENT", "PROVOST", "VP_FINANCE", "VP_RESEARCH", "VP_ADVANCEMENT", "CFO"] def _kpi_card(title: str, value: str, subtitle: str = "") -> html.Div: return html.Div( [ html.H4(title, style={"margin": "0", "color": "#666", "fontSize": "14px"}), html.H2(value, style={"margin": "5px 0", "color": "#00539F"}), html.P(subtitle, style={"margin": "0", "color": "#999", "fontSize": "12px"}), ], style={ "flex": "1", "padding": "20px", "backgroundColor": "#f8f9fa", "borderRadius": "8px", "textAlign": "center", "margin": "0 8px", }, ) def layout(conn: duckdb.DuckDBPyConnection): all_earners = query_top_earners(conn) if all_earners.height == 0: return _NO_DATA years = sorted(all_earners["tax_year"].unique().to_list()) year_options = [{"label": "All Years", "value": "all"}] + [ {"label": str(y), "value": y} for y in years ] # KPI cards cagr = query_comp_cagr(conn) agg_cagr = query_aggregate_comp_cagr(conn) kpi_cards = [] if cagr: kpi_cards.append(_kpi_card( "President Compensation", f"${cagr['end_comp']:,}", f"Tax year {cagr['end_year']}", )) kpi_cards.append(_kpi_card( "President CAGR", f"{cagr['cagr_pct']}%", f"Annualized growth, {cagr['start_year']}-{cagr['end_year']}", )) if agg_cagr: kpi_cards.append(_kpi_card( "Top-10 Total Compensation", f"${agg_cagr['end_comp']:,}", f"Tax year {agg_cagr['end_year']}", )) kpi_cards.append(_kpi_card( "Top-10 CAGR", f"{agg_cagr['cagr_pct']}%", f"Annualized growth, {agg_cagr['start_year']}-{agg_cagr['end_year']}", )) kpi_row = html.Div(kpi_cards, style={"display": "flex", "marginBottom": "24px"}) if kpi_cards else html.Div() # Compensation by role trend role_df = query_comp_by_role(conn) role_fig = go.Figure() if role_df.height > 0: role_pd = role_df.to_pandas() for role in _KEY_ROLES: subset = role_pd[role_pd["canonical_role"] == role] if len(subset) > 0: role_fig.add_trace(go.Scatter( x=subset["tax_year"], y=subset["total_compensation"], mode="lines+markers", name=role.replace("_", " ").title(), )) role_fig.update_layout( title="Compensation Trends by Role", xaxis_title="Tax Year", yaxis_title="Total Compensation ($)", template="plotly_white", height=420, ) # Comp vs CPI indexed cpi_df = query_comp_vs_cpi(conn) cpi_fig = go.Figure() if cpi_df.height > 0: cpi_pd = cpi_df.to_pandas() cpi_fig.add_trace(go.Scatter( x=cpi_pd["year"], y=cpi_pd["comp_index"], mode="lines+markers", name="Top Compensation", line={"color": "#00539F"}, )) cpi_fig.add_trace(go.Scatter( x=cpi_pd["year"], y=cpi_pd["agg_index"], mode="lines+markers", name="Top-10 Aggregate", line={"color": "#E07A5F"}, )) cpi_fig.add_trace(go.Scatter( x=cpi_pd["year"], y=cpi_pd["cpi_index"], mode="lines+markers", name="CPI-U", line={"color": "#FFD200", "dash": "dash"}, )) cpi_fig.update_layout( title="Compensation vs CPI-U (Indexed, Base Year = 100)", xaxis_title="Year", yaxis_title="Index", template="plotly_white", height=380, ) return html.Div([ kpi_row, html.Div( [ html.Label("Filter by Tax Year: ", style={"fontWeight": "bold"}), dcc.Dropdown( id="comp-year-dropdown", options=year_options, value="all", style={"width": "200px", "display": "inline-block"}, ), ], style={"marginBottom": "16px"}, ), dash_table.DataTable( id="comp-table", columns=[ {"name": "Year", "id": "tax_year"}, {"name": "Name", "id": "person_name"}, {"name": "Title", "id": "title"}, {"name": "Role", "id": "canonical_role"}, {"name": "Base", "id": "base_compensation", "type": "numeric", "format": dash_table.Format.Format().group(True)}, {"name": "Bonus", "id": "bonus_compensation", "type": "numeric", "format": dash_table.Format.Format().group(True)}, {"name": "Total", "id": "total_compensation", "type": "numeric", "format": dash_table.Format.Format().group(True)}, ], data=all_earners.to_pandas().to_dict("records"), page_size=15, sort_action="native", filter_action="native", style_table={"overflowX": "auto"}, style_cell={"textAlign": "left", "padding": "8px", "fontSize": "13px"}, style_header={"fontWeight": "bold", "backgroundColor": "#f0f0f0"}, ), html.Div( [ html.Div(dcc.Graph(id="comp-breakdown-chart"), style={"flex": "1"}), html.Div(dcc.Graph(figure=cpi_fig), style={"flex": "1"}), ], style={"display": "flex", "gap": "16px", "marginTop": "16px"}, ), dcc.Graph(figure=role_fig), ]) def register_callbacks(app: dash.Dash, conn: duckdb.DuckDBPyConnection) -> None: """Register interactive callbacks for the compensation page.""" @app.callback( [Output("comp-table", "data"), Output("comp-breakdown-chart", "figure")], Input("comp-year-dropdown", "value"), ) def update_compensation(year_value): year = None if year_value == "all" else int(year_value) earners = query_top_earners(conn, year=year) # Table data table_data = earners.to_pandas().to_dict("records") if earners.height > 0 else [] # Breakdown chart — stacked bar of comp components breakdown_fig = go.Figure() if earners.height > 0: ep = earners.to_pandas().head(10) # top 10 by total comp _SUFFIXES = {"JR", "SR", "II", "III", "IV", "JR.", "SR."} def _short_name(n): if "," in n: return n.split(",")[0][:20] parts = n.split() while len(parts) > 1 and parts[-1].upper().rstrip(".") in _SUFFIXES: parts.pop() return parts[-1][:20] if parts else n[:20] short_names = [_short_name(n) for n in ep["person_name"]] for comp_type, label, color in [ ("base_compensation", "Base", "#00539F"), ("bonus_compensation", "Bonus", "#FFD200"), ("deferred_compensation", "Deferred", "#7FB069"), ("nontaxable_benefits", "Benefits", "#E07A5F"), ("other_compensation", "Other", "#999"), ]: if comp_type in ep.columns: breakdown_fig.add_trace(go.Bar( x=short_names, y=ep[comp_type], name=label, marker_color=color, )) breakdown_fig.update_layout(barmode="stack") title_suffix = f" ({year})" if year else " (All Years)" breakdown_fig.update_layout( title=f"Compensation Breakdown — Top 10{title_suffix}", xaxis_title="", yaxis_title="$", template="plotly_white", height=380, ) return table_data, breakdown_fig