Phase 1 project prototype
This commit is contained in:
parent
29215e2bd2
commit
2c9ae1c312
29 changed files with 2967 additions and 22 deletions
162
src/admin_analytics/dashboard/pages/compensation.py
Normal file
162
src/admin_analytics/dashboard/pages/compensation.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
"""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,
|
||||
)
|
||||
|
||||
_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 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
|
||||
]
|
||||
|
||||
# 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["cpi_index"],
|
||||
mode="lines+markers", name="CPI-U",
|
||||
line={"color": "#FFD200", "dash": "dash"},
|
||||
))
|
||||
cpi_fig.update_layout(
|
||||
title="Top Compensation vs CPI-U (Indexed, Base Year = 100)",
|
||||
xaxis_title="Year", yaxis_title="Index",
|
||||
template="plotly_white", height=380,
|
||||
)
|
||||
|
||||
return html.Div([
|
||||
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
|
||||
short_names = [n.split(",")[0][:20] if "," in n else n.split()[-1][:20]
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue