Spaces:
Runtime error
Runtime error
| from pathlib import Path | |
| import polars as pl | |
| from shiny import reactive, render | |
| from shiny.express import app_opts, ui | |
| from shiny.express import input as app_input | |
| from shinywidgets import render_widget | |
| from src.calcs import ( | |
| get_comp_radar, | |
| get_comp_summary, | |
| get_comparison_employment, | |
| get_occ_ai_exposure, | |
| get_occ_employment_by_sex, | |
| get_occ_summary, | |
| ) | |
| from src.setup import ( | |
| INTRO_MD, | |
| METRICS, | |
| OCC_CHOICES, | |
| OCCS, | |
| SEXES, | |
| YEAR_MAX, | |
| YEAR_MIN, | |
| YEARS, | |
| as_great_table_html, | |
| download_extension, | |
| download_media_type, | |
| export_filtered_data, | |
| first_cols, | |
| lf, | |
| ) | |
| from src.visuals import ( | |
| build_ai_exposure_bar, | |
| build_comp_radar_plot, | |
| build_comparison_employment_plot, | |
| build_sex_chart, | |
| build_value_boxes, | |
| ) | |
| LOGOS_PATH = Path(__file__).parent / "logos" | |
| app_opts(static_assets={"/logos": LOGOS_PATH}) | |
| ui.page_opts( | |
| title="AI Exposure & Monthly Employment Explorer", | |
| fillable=True, | |
| theme=ui.Theme.from_brand(__file__), | |
| ) | |
| _DEFAULT_OCC = OCCS[0] if OCCS else None | |
| # ββ Tab navigation ββββββββββββββββββββββββββββββββββββββββββββ | |
| with ui.navset_pill(id="main_tabs"): | |
| # ββ Tab 1: Occupation View ββββββββββββββββββββββββββββββββ | |
| with ui.nav_panel("Occupation View"): | |
| with ui.layout_sidebar(): | |
| with ui.sidebar(title="Occupation View", width=280): | |
| ui.div( | |
| ui.img( | |
| src="/logos/lab.svg", | |
| alt="AI-Econ Lab logo", | |
| style="width:100%; max-width:180px;", | |
| ), | |
| style="text-align:center; margin-bottom:1rem;", | |
| ) | |
| ui.markdown(INTRO_MD) | |
| ui.hr() | |
| ui.input_select( | |
| "occ_occupation", | |
| "Occupation", | |
| choices=OCC_CHOICES, | |
| selected=_DEFAULT_OCC, | |
| ) | |
| ui.input_select( | |
| "occ_year", | |
| "Year (snapshot)", | |
| choices={str(y): str(y) for y in YEARS}, | |
| selected=str(YEAR_MAX), | |
| ) | |
| ui.hr() | |
| ui.p("Employment trend filters:", class_="fw-semibold mb-1 small") | |
| ui.input_slider( | |
| "occ_year_range", | |
| "Year Range", | |
| min=YEAR_MIN, | |
| max=YEAR_MAX, | |
| value=[YEAR_MIN, YEAR_MAX], | |
| sep="", | |
| ) | |
| ui.input_checkbox_group( | |
| "occ_sexes", | |
| "Sex", | |
| choices=SEXES, | |
| selected=SEXES, | |
| ) | |
| # Value boxes | |
| def occ_value_boxes(): | |
| summary = occ_summary() | |
| if summary is None: | |
| return ui.p( | |
| "No data for the selected occupation and year.", | |
| class_="text-muted p-3", | |
| ) | |
| return build_value_boxes(summary, app_input.occ_occupation()) | |
| # AI Exposure bar chart | |
| with ui.card(full_screen=True): | |
| def occ_ai_bar(): | |
| df = occ_ai_exposure().to_pandas() | |
| return build_ai_exposure_bar( | |
| df, | |
| app_input.occ_occupation(), | |
| int(app_input.occ_year()), | |
| ) | |
| # Monthly employment trend by sex | |
| with ui.card(full_screen=True): | |
| def occ_sex_chart(): | |
| df = occ_emp_by_sex().to_pandas() | |
| return build_sex_chart(df, app_input.occ_occupation()) | |
| # ββ Tab 2: Comparison View ββββββββββββββββββββββββββββββββ | |
| with ui.nav_panel("Comparison View"): | |
| with ui.layout_sidebar(): | |
| with ui.sidebar(title="Comparison View", width=280): | |
| ui.input_selectize( | |
| "comp_occupations", | |
| "Occupations (up to 5)", | |
| choices=OCC_CHOICES, | |
| multiple=True, | |
| options={"maxItems": 5}, | |
| ) | |
| ui.input_checkbox_group( | |
| "comp_sexes", | |
| "Sex", | |
| choices=SEXES, | |
| selected=SEXES, | |
| ) | |
| ui.input_select( | |
| "comp_year", | |
| "Year (AI snapshot)", | |
| choices={str(y): str(y) for y in YEARS}, | |
| selected=str(YEAR_MAX), | |
| ) | |
| # Employment summary table | |
| with ui.card(): | |
| ui.card_header("Employment Summary") | |
| def comp_summary_table(): | |
| occs = list(app_input.comp_occupations() or []) | |
| sexes = list(app_input.comp_sexes() or []) | |
| if not occs or not sexes: | |
| return ui.p( | |
| "Select at least one occupation.", | |
| class_="text-muted p-3", | |
| ) | |
| df = get_comp_summary( | |
| lf, occs, sexes, int(app_input.comp_year()), | |
| ).to_pandas() | |
| return as_great_table_html(df, METRICS) | |
| # Employment change line chart | |
| with ui.card(full_screen=True): | |
| def comp_employment_chart(): | |
| df = comparison_data().to_pandas() | |
| return build_comparison_employment_plot(df) | |
| # AI percentile radar chart | |
| with ui.card(full_screen=True): | |
| def comp_radar_chart(): | |
| df = comp_radar_data().to_pandas() | |
| return build_comp_radar_plot(df, METRICS) | |
| # ββ Tab 3: Download βββββββββββββββββββββββββββββββββββββββ | |
| with ui.nav_panel("Download"): | |
| with ui.layout_sidebar(): | |
| with ui.sidebar(title="Download Filters", width=280): | |
| ui.input_slider( | |
| "dl_year_range", | |
| "Year Range", | |
| min=YEAR_MIN, | |
| max=YEAR_MAX, | |
| value=[YEAR_MIN, YEAR_MAX], | |
| sep="", | |
| ) | |
| ui.input_checkbox_group( | |
| "dl_sexes", | |
| "Sex", | |
| choices=SEXES, | |
| selected=SEXES, | |
| ) | |
| ui.input_selectize( | |
| "dl_occupations", | |
| "Occupation (blank = all)", | |
| choices=OCC_CHOICES, | |
| multiple=True, | |
| ) | |
| ui.input_select( | |
| "dl_format", | |
| "Format", | |
| choices={"csv": "CSV", "parquet": "Parquet", "excel": "Excel"}, | |
| selected="csv", | |
| ) | |
| async def download_data(): | |
| df = download_frame().to_pandas() | |
| yield export_filtered_data(df, app_input.dl_format()) | |
| # Row count | |
| def dl_row_count(): | |
| n = len(download_frame()) | |
| return ui.p(f"{n:,} rows match the current filters.", class_="text-muted") | |
| # Data preview | |
| with ui.card(full_screen=True): | |
| ui.card_header("Data Preview (first 50 rows)") | |
| def dl_preview(): | |
| df = download_frame().head(50) | |
| all_cols = df.columns | |
| ordered = [c for c in first_cols if c in all_cols] | |
| rest = [c for c in all_cols if c not in ordered] | |
| return as_great_table_html( | |
| df.select(ordered + rest).to_pandas(), METRICS, | |
| ) | |
| # ββ Reactive calculations βββββββββββββββββββββββββββββββββββββ | |
| def occ_summary(): | |
| return get_occ_summary(lf, app_input.occ_occupation(), int(app_input.occ_year())) | |
| def occ_ai_exposure(): | |
| return get_occ_ai_exposure(lf, app_input.occ_occupation(), int(app_input.occ_year())) | |
| def occ_emp_by_sex(): | |
| yr = app_input.occ_year_range() | |
| return get_occ_employment_by_sex( | |
| lf, | |
| app_input.occ_occupation(), | |
| (yr[0], yr[1]), | |
| list(app_input.occ_sexes() or []), | |
| ) | |
| def comparison_data(): | |
| occs = list(app_input.comp_occupations() or []) | |
| sexes = list(app_input.comp_sexes() or []) | |
| if not occs or not sexes: | |
| return pl.DataFrame(schema={ | |
| "year": pl.Int64, | |
| "month": pl.String, | |
| "occupation": pl.String, | |
| "emp_count": pl.Float64, | |
| "pct_chg_1m": pl.Float64, | |
| }) | |
| return get_comparison_employment(lf, occs, sexes) | |
| def comp_radar_data(): | |
| occs = list(app_input.comp_occupations() or []) | |
| if not occs: | |
| return pl.DataFrame() | |
| return get_comp_radar(lf, occs, int(app_input.comp_year())) | |
| def download_frame(): | |
| yr = app_input.dl_year_range() | |
| q = lf.filter( | |
| (pl.col("year") >= yr[0]) | |
| & (pl.col("year") <= yr[1]) | |
| & (pl.col("sex").is_in(list(app_input.dl_sexes() or []))), | |
| ) | |
| if app_input.dl_occupations(): | |
| q = q.filter(pl.col("occupation").is_in(list(app_input.dl_occupations()))) | |
| return q.collect() | |