File size: 6,957 Bytes
ec6e9a7
 
 
 
 
3e12d11
ec6e9a7
3e12d11
 
 
b5554e2
3e12d11
 
 
 
 
 
69a3b98
 
3e12d11
ec6e9a7
69a3b98
 
3e12d11
 
 
69a3b98
3e12d11
 
 
 
69a3b98
3e12d11
69a3b98
42ba7e4
 
 
 
 
 
 
3e12d11
 
 
 
 
 
 
69a3b98
 
3e12d11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42ba7e4
ec6e9a7
 
 
 
42ba7e4
3e12d11
 
 
 
 
 
ec6e9a7
 
3e12d11
 
 
ec6e9a7
 
 
 
 
 
3e12d11
 
 
 
 
ec6e9a7
 
 
3e12d11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ec6e9a7
3e12d11
 
a108e61
 
 
 
3e12d11
 
 
 
 
 
 
ec6e9a7
3e12d11
 
 
 
 
 
 
 
 
ec6e9a7
 
3e12d11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ec6e9a7
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# Shiny Express app for exploring SCB employment by occupation.
#
# This file defines the UI controls (sidebar) and the reactive filters that
# drive the Plotly output. Data is loaded once at startup via `load_payload()`
# and then filtered client-side.

from pathlib import Path

from shiny import reactive
from shiny.express import input, ui
from shinywidgets import output_widget, render_plotly
from src.config import (
    DEFAULT_LEVEL,
    DEFAULT_YEAR_RANGE,
    LEVEL_OPTIONS,
    GLOBAL_YEAR_MIN,
    GLOBAL_YEAR_MAX,
)

from src.data_manager import load_payload
from src.plot_helper import employment_multi_plot


# Helpers for UI mapping
LEVEL_CHOICES = {value: label for label, value in LEVEL_OPTIONS}
YEAR_RANGE_DEFAULT = list(range(DEFAULT_YEAR_RANGE[0], DEFAULT_YEAR_RANGE[1] + 1))

# ======================================================
#  UI LAYOUT
# ======================================================
css_file = Path(__file__).parent / "css" / "theme.css"

ui.include_css(css_file)

ui.tags.head(
    ui.tags.link(
        rel="stylesheet",
        href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css",
    )
)

ui.page_opts(
    fillable=False,
    fillable_mobile=True,
    full_width=True,
    id="page",
    lang="en",
)


with ui.sidebar(open="desktop", position="right"):
    ui.input_select(
        "level", "Select Occupation level", LEVEL_CHOICES, selected=DEFAULT_LEVEL
    )
    ui.input_selectize(
        "selectize",
        "Select Occupation title(s)",
        {},
        multiple=True,
        options=(
            {
                "placeholder": "Statisticians...",
                "create": False,
                "plugins": ["clear_button"],
            }
        ),
    )
    # ui.input_radio_buttons(
    #     "count_mode",
    #     "Employed persons display",
    #     {"raw": "Raw counts", "index": "Index to base year"},
    #     selected="raw",
    # )
    # with ui.panel_conditional("input.count_mode == 'index'"):
    #     ui.input_select(
    #         "base_year",
    #         "Base year",
    #         YEAR_RANGE_DEFAULT,
    #         selected=2022,
    #     )

    ui.input_slider(
        "year_range",
        "Year range",
        min=GLOBAL_YEAR_MIN,
        max=GLOBAL_YEAR_MAX,
        value=DEFAULT_YEAR_RANGE,
        step=1,
        sep="",
    )
    ui.input_action_button(
        "reset_filters",
        "Reset filters",
        icon=ui.tags.i(class_="fas fa-rotate-left"),
        class_="btn-primary mt-3 w-100",
    )


# ======================================================
#  REACTIVE STATE
# ======================================================

# Load the (cached) pipeline output once at startup; filters operate on this in-memory DataFrame.
payload = load_payload()


@reactive.effect
@reactive.event(input.reset_filters)
def _reset_filters():
    # Reset UI inputs back to defaults (this does not trigger a data reload).
    ui.update_select("level", selected=DEFAULT_LEVEL)
    ui.update_slider("year_range", value=DEFAULT_YEAR_RANGE)
    ui.update_selectize("selectize", selected=[])


# Build Selectize choices per selected level
@reactive.calc
def level_label_choices():
    # Shiny choices are `{value: label}`; we use the plain label as the value returned by the input,
    # while displaying `code - label` in the dropdown.
    df = payload
    lvl = int(input.level())
    subset = df[df["level"] == lvl][["code", "label"]].dropna().drop_duplicates()
    choices_list = []
    for _, row in subset.iterrows():
        key = row["label"]
        value = f"{row['code']} - {row['label']}"
        choices_list.append((key, value))

    # Sort by the code (extract code from display value)
    choices_list.sort(key=lambda x: x[1].split(" - ")[0])

    # Convert to dictionary while maintaining order
    return {key: value for key, value in choices_list}


# keep selectize choices in sync with level selection
@reactive.effect
def _sync_selectize_choices():
    choices = level_label_choices()
    current = input.selectize() or []

    # Prune selections that no longer exist after switching levels.
    valid_selected = [s for s in current if s in choices]

    # # apply a default when nothing valid remains
    # if not valid_selected and choices:
    #     # pick the first option (or slice for multiple defaults)
    #     valid_selected = [next(iter(choices))]

    ui.update_selectize("selectize", choices=choices, selected=valid_selected)


# Filtered data based on UI inputs
@reactive.calc
def filtered_data():
    df = payload
    level = int(input.level())
    year_min, year_max = input.year_range()
    selected_titles = input.selectize()

    idx_level = df["level"] == level
    idx_year = df["year"].between(year_min, year_max)

    # If no titles selected, return empty dataframe
    if not selected_titles:
        # Returning an empty frame allows the plot helper to render a friendly placeholder.
        return df[idx_level & idx_year & (df["label"] == "")].copy()

    idx_title = df["label"].isin(selected_titles)
    filtered_df = df[idx_level & idx_year & idx_title]

    return filtered_df


# # Warning message for no selections
# with ui.div(style="margin: 20px;"):

#     @render.ui
#     def selection_status():
#         if not input.selectize():
#             return ui.div(
#                 ui.tags.div(
#                     "⚠️ Please select at least one occupation title to view data.",
#                     style="background-color: #fff3cd; color: #856404; padding: 15px; border: 1px solid #ffeaa7; border-radius: 5px; text-align: center; font-weight: bold;",
#                 )
#             )
#         else:
#             return ui.div()  # Return empty div when selections exist


# @render_plotly
# def data_table():
#     df = filtered_data()

#     # Show message if no data available
#     if df.empty:
#         fig = go.Figure()
#         fig.add_annotation(
#             text="No data available. Please select occupation titles.",
#             xref="paper",
#             yref="paper",
#             x=0.5,
#             y=0.5,
#             showarrow=False,
#             font=dict(size=16),
#         )
#         fig.update_layout(
#             xaxis=dict(visible=False), yaxis=dict(visible=False), plot_bgcolor="white"
#         )
#         return fig

#     fig = go.Figure(
#         data=go.Table(
#             header=dict(
#                 values=list(df.columns), fill_color="paleturquoise", align="left"
#             ),
#             cells=dict(
#                 values=[df[col] for col in df.columns],
#                 fill_color="lavender",
#                 align="left",
#             ),
#         )
#     )
#     return fig


with ui.div(style="display:flex; justify-content:center;"):
    output_widget("employment_plot")

    @render_plotly
    def employment_plot2():
        return employment_multi_plot(filtered_data(), level=input.level())