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()