Spaces:
Running
Running
File size: 4,412 Bytes
51dde21 213c9db 51dde21 213c9db 51dde21 213c9db 5fcca14 02b0ee6 51dde21 02b0ee6 51dde21 bd4238e 51dde21 bd4238e 51dde21 213c9db 51dde21 5fcca14 51dde21 5fcca14 51dde21 5fcca14 51dde21 5fcca14 51dde21 5fcca14 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | """Download helpers, table rendering, and column label formatters."""
from __future__ import annotations
import importlib.util
import io
import re
from typing import TYPE_CHECKING
from shiny import ui
if TYPE_CHECKING:
import pandas as pd
_EXCEL_ENGINE: str | None = next(
(e for e in ("openpyxl", "xlsxwriter") if importlib.util.find_spec(e) is not None),
None,
)
# Matches leading emoji glyphs only (not general non-alphanumeric text), so
# labels are never mistaken for a decorative prefix. Kept in sync with the
# equivalent _EMOJI_PREFIX pattern in src/visuals.py.
_EMOJI_PREFIX = re.compile(
r"^[\U0001F000-\U0001FAFF☀-➿️]+\s*",
)
def metric_display_name(metric_key: str, metrics: dict[str, str]) -> str:
"""Return a human-readable metric label with leading icons stripped."""
label = metrics.get(metric_key, metric_key.replace("_", " ").title())
return _EMOJI_PREFIX.sub("", label).strip()
def readable_column_name(col: str, metrics: dict[str, str]) -> str:
"""Convert a raw dataset column name into a readable table header."""
exact: dict[str, str] = {
"code_1": "SSYK Major Group",
"occupation": "Occupation",
"year": "Year",
"month": "Month",
"gender": "Gender",
"emp_count": "Employment ('000)",
"weight_sum": "Weight Sum",
"chg_1m": "Emp Change 1mo ('000)",
"chg_3m": "Emp Change 3mo ('000)",
"chg_6m": "Emp Change 6mo ('000)",
"pct_chg_1m": "Emp Change 1mo (%)",
"pct_chg_3m": "Emp Change 3mo (%)",
"pct_chg_6m": "Emp Change 6mo (%)",
}
if col in exact:
return exact[col]
col_l = col.lower()
if col_l.startswith("pctl_") and col_l.endswith("_wavg"):
metric_key = col[5:-5]
return f"{metric_display_name(metric_key, metrics)} Percentile (Weighted Avg)"
if col_l.endswith("_wavg"):
metric_key = col[:-5]
return f"{metric_display_name(metric_key, metrics)} (Weighted Avg)"
if col_l.endswith("_avg"):
metric_key = col[:-4]
return f"{metric_display_name(metric_key, metrics)} (Average)"
if col_l.endswith("_level_exposure"):
metric_key = col[: -len("_level_exposure")]
return f"{metric_display_name(metric_key, metrics)} Exposure Level"
fallback = col.replace("_", " ").title()
return (
fallback.replace("Ssyk", "SSYK").replace("Ai", "AI").replace("Daioe", "DAIOE")
)
def as_great_table_html(df: pd.DataFrame, metrics: dict[str, str]) -> ui.TagChild:
"""Render a pandas DataFrame as Great Tables HTML with readable headers."""
import pandas as pd
from great_tables import GT
if df.empty:
return ui.p("No data available for the selected filters.")
df_display = df.rename(
columns={c: readable_column_name(c, metrics) for c in df.columns},
)
float_cols = [
c
for c in df_display.columns
if c != "Year" and pd.api.types.is_float_dtype(df_display[c])
]
gt = (
GT(df_display)
.opt_row_striping()
.tab_options(
table_font_names=["Nunito Sans", "Arial", "sans-serif"],
table_width="100%",
)
.opt_stylize(style=2, color="blue")
)
if float_cols:
gt = gt.fmt_number(columns=float_cols, decimals=2)
return ui.HTML(gt.as_raw_html())
def download_extension(fmt: str) -> str:
"""Map a download format name to its file extension."""
return {"csv": "csv", "parquet": "parquet", "excel": "xlsx"}.get(fmt, "csv")
def download_media_type(fmt: str) -> str:
"""Return the browser media type for a download format."""
if fmt == "parquet":
return "application/octet-stream"
if fmt == "excel":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
return "text/csv; charset=utf-8"
def export_filtered_data(df: pd.DataFrame, fmt: str) -> bytes:
"""Serialise a DataFrame to csv, parquet, or excel bytes for a Shiny download."""
if fmt == "parquet":
return df.to_parquet(index=False)
if fmt == "excel":
if _EXCEL_ENGINE is None:
raise ModuleNotFoundError("Excel export requires openpyxl or xlsxwriter.")
buffer = io.BytesIO()
df.to_excel(buffer, index=False, engine=_EXCEL_ENGINE)
return buffer.getvalue()
return df.to_csv(index=False).encode()
|