From 3226d4e950b3cd1be796323890ac1095833de0aa Mon Sep 17 00:00:00 2001 From: yluo0664 Date: Mon, 27 Oct 2025 21:14:12 +1100 Subject: [PATCH 01/15] feat: Move the Apply filter of Month and Hour to the sidebar - Added Global filter UI - Removed the original Apply filter in the pages - Added grey fill to the heatmap --- pages/explorer.py | 324 ++++++++---------------------- pages/lib/charts_data_explorer.py | 65 +++++- pages/lib/charts_sun.py | 25 ++- pages/lib/global_element_ids.py | 53 +++-- pages/lib/layout.py | 252 ++++++++++++++++++++++- pages/lib/template_graphs.py | 226 ++++++++++++++++----- pages/lib/utils.py | 8 +- pages/natural_ventilation.py | 128 ++++-------- pages/outdoor.py | 189 +++++++++-------- pages/psy-chart.py | 93 +++------ pages/select.py | 39 +++- pages/summary.py | 10 +- pages/sun.py | 65 ++++-- pages/t_rh.py | 65 ++++-- pages/wind.py | 212 +++++-------------- tests/node/cypress/e2e/spec.cy.js | 4 +- 16 files changed, 971 insertions(+), 787 deletions(-) diff --git a/pages/explorer.py b/pages/explorer.py index 2614d10..6225175 100644 --- a/pages/explorer.py +++ b/pages/explorer.py @@ -116,63 +116,7 @@ def section_one(): w="33%", children=dmc.Stack( children=[ - dmc.Button( - "Apply month and hour filter", - id=ElementIds.SEC1_TIME_FILTER_INPUT, - color="blue", - ), - dmc.Group( - children=[ - dmc.Title("Month Range", order=5), - dmc.Stack( - flex=1, - children=dcc.RangeSlider( - id=ElementIds.SEC1_MONTH_SLIDER, - min=1, - max=12, - step=1, - value=[1, 12], - marks={1: "1", 12: "12"}, - tooltip={ - "always_visible": False, - "placement": "top", - }, - allowCross=False, - ), - ), - dcc.Checklist( - id=ElementIds.INVERT_MONTH_EXPLORE_DESCRIPTIVE, - options=[{"label": "Invert", "value": "invert"}], - value=[], - ), - ], - ), - dmc.Group( - children=[ - dmc.Title("Hour Range", order=5), - dmc.Stack( - flex=1, - children=dcc.RangeSlider( - id=ElementIds.SEC1_HOUR_SLIDER, - min=0, - max=24, - step=1, - value=[0, 24], - marks={0: "0", 24: "24"}, - tooltip={ - "always_visible": False, - "placement": "topLeft", - }, - allowCross=False, - ), - ), - dcc.Checklist( - id=ElementIds.INVERT_HOUR_EXPLORE_DESCRIPTIVE, - options=[{"label": "Invert", "value": "invert"}], - value=[], - ), - ], - ), + # Month and hour filter moved to global sidebar ], ), ) @@ -193,7 +137,7 @@ def section_two_inputs(): id_button=IdButtons.CUSTOM_HEATMAP_CHART_LABEL, ), dmc.SimpleGrid( - cols=3, + cols=2, spacing="md", children=[ dmc.Group( @@ -210,69 +154,6 @@ def section_two_inputs(): ], align="flex-start", ), - dmc.Stack( - [ - dmc.Button( - "Apply month and hour filter", - id=ElementIds.SEC2_TIME_FILTER_INPUT, - color="blue", - ), - dmc.Group( - [ - dmc.Title("Month Range", order=5), - dmc.Stack( - dcc.RangeSlider( - id=ElementIds.SEC2_MONTH_SLIDER, - min=1, - max=12, - step=1, - value=[1, 12], - marks={1: "1", 12: "12"}, - tooltip={ - "always_visible": False, - "placement": "topLeft", - }, - ), - flex=1, - ), - dcc.Checklist( - id=ElementIds.INVERT_MONTH_EXPLORE_HEATMAP, - options=[ - {"label": "Invert", "value": "invert"} - ], - value=[], - ), - ], - ), - dmc.Group( - [ - dmc.Title("Hour Range", order=5), - dmc.Stack( - dcc.RangeSlider( - id=ElementIds.SEC2_HOUR_SLIDER, - min=0, - max=24, - step=1, - value=[0, 24], - marks={0: "0", 24: "24"}, - tooltip={ - "always_visible": False, - "placement": "topLeft", - }, - ), - flex=1, - ), - dcc.Checklist( - id=ElementIds.INVERT_HOUR_EXPLORE_HEATMAP, - options=[ - {"label": "Invert", "value": "invert"} - ], - value=[], - ), - ], - ), - ], - ), dmc.Stack( [ dmc.Button( @@ -330,7 +211,7 @@ def section_two_inputs(): def section_two(): """Return the two graphs in section two.""" return dmc.Stack( - id=ElementIds.TAB6_SEC2_CONTAINER, + id=ElementIds.EXPLORER_SEC2_CONTAINER, children=[ section_two_inputs(), dcc.Loading( @@ -366,7 +247,7 @@ def section_two(): def section_three_inputs(): return dmc.SimpleGrid( - cols=3, + cols=2, children=[ dmc.Stack( [ @@ -375,7 +256,7 @@ def section_three_inputs(): dmc.Title("X Variable:", order=5), dmc.Stack( dropdown( - id=ElementIds.TAB6_SEC3_VAR_X_DROPDOWN, + id=ElementIds.EXPLORER_SEC3_VAR_X_DROPDOWN, options=explore_dropdown_names, value="DBT", ), @@ -388,7 +269,7 @@ def section_three_inputs(): dmc.Title("Y Variable:", order=5), dmc.Stack( dropdown( - id=ElementIds.TAB6_SEC3_VAR_Y_DROPDOWN, + id=ElementIds.EXPLORER_SEC3_VAR_Y_DROPDOWN, options=explore_dropdown_names, value=Variables.RH.col_name, ), @@ -401,7 +282,7 @@ def section_three_inputs(): dmc.Title("Color By:", order=5), dmc.Stack( dropdown( - id=ElementIds.TAB6_SEC3_COLORBY_DROPDOWN, + id=ElementIds.EXPLORER_SEC3_COLORBY_DROPDOWN, options=explore_dropdown_names, value="glob_hor_rad", ), @@ -411,64 +292,11 @@ def section_three_inputs(): ), ], ), - dmc.Stack( - [ - dmc.Button( - "Apply month and hour filter", - id=ElementIds.TAB6_SEC3_TIME_FILTER_INPUT, - color="blue", - ), - dmc.Group( - [ - dmc.Title("Month Range", order=5), - dmc.Stack( - dcc.RangeSlider( - id=ElementIds.TAB6_SEC3_QUERY_MONTH_SLIDER, - min=1, - max=12, - value=[1, 12], - marks={1: "1", 12: "12"}, - ), - flex=1, - ), - dcc.Checklist( - id=ElementIds.INVERT_MONTH_EXPLORE_MORE_CHARTS, - options=[{"label": "Invert", "value": "invert"}], - value=[], - ), - ], - ), - dmc.Group( - [ - dmc.Title("Hour Range", order=5), - dmc.Stack( - dcc.RangeSlider( - id=ElementIds.TAB6_SEC3_QUERY_HOUR_SLIDER, - min=0, - max=24, - value=[0, 24], - marks={0: "0", 24: "24"}, - tooltip={ - "always_visible": False, - "placement": "topLeft", - }, - ), - flex=1, - ), - dcc.Checklist( - id=ElementIds.INVERT_HOUR_EXPLORE_MORE_CHARTS, - options=[{"label": "Invert", "value": "invert"}], - value=[], - ), - ], - ), - ], - ), dmc.Stack( [ dmc.Button( "Apply filter", - id=ElementIds.TAB6_SEC3_DATA_FILTER_INPUT, + id=ElementIds.EXPLORER_SEC3_DATA_FILTER_INPUT, color="blue", ), dmc.Group( @@ -476,7 +304,7 @@ def section_three_inputs(): dmc.Title("Filter Variable:", order=5), dmc.Stack( dropdown( - id=ElementIds.TAB6_SEC3_FILTER_VAR_DROPDOWN, + id=ElementIds.EXPLORER_SEC3_FILTER_VAR_DROPDOWN, options=explore_dropdown_names, value=Variables.RH.col_name, ), @@ -489,7 +317,7 @@ def section_three_inputs(): dmc.Title("Min Value:", order=5), dmc.Stack( dmc.NumberInput( - id=ElementIds.TAB6_SEC3_MIN_VAL, + id=ElementIds.EXPLORER_SEC3_MIN_VAL, placeholder="Enter a number for the min val", value=0, ), @@ -502,7 +330,7 @@ def section_three_inputs(): dmc.Title("Max Value:", order=5), dmc.Stack( dmc.NumberInput( - id=ElementIds.TAB6_SEC3_MAX_VAL, + id=ElementIds.EXPLORER_SEC3_MAX_VAL, placeholder="Enter a number for the max val", value=100, ), @@ -549,6 +377,7 @@ def section_three(): Input(ElementIds.ID_EXPLORER_DF_STORE, "modified_timestamp"), Input(ElementIds.SEC1_VAR_DROPDOWN, "value"), Input(ElementIds.ID_EXPLORER_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_EXPLORER_DF_STORE, "data"), @@ -556,9 +385,16 @@ def section_three(): State(ElementIds.ID_EXPLORER_SI_IP_UNIT_STORE, "data"), ], ) -def update_tab_yearly(_, var, global_local, df, meta, si_ip): +def update_tab_yearly(_, var, global_local, global_filter_data, df, meta, si_ip): """Update the contents of tab size. Passing in the info from the dropdown and the general info.""" + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + target_columns = [var, Variables.ADAPTIVE_CMF_80_LOW.col_name, Variables.ADAPTIVE_CMF_80_UP.col_name, + Variables.ADAPTIVE_CMF_90_LOW.col_name, Variables.ADAPTIVE_CMF_90_UP.col_name, + Variables.ADAPTIVE_CMF_RMT.col_name] + df = apply_global_month_hour_filter(df, global_filter_data, target_columns) + if df[var].mean() == 99990.0: return dmc.Alert( """The selected variable is not available, @@ -583,6 +419,7 @@ def update_tab_yearly(_, var, global_local, df, meta, si_ip): Input(ElementIds.ID_EXPLORER_DF_STORE, "modified_timestamp"), Input(ElementIds.SEC1_VAR_DROPDOWN, "value"), Input(ElementIds.ID_EXPLORER_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_EXPLORER_DF_STORE, "data"), @@ -590,8 +427,12 @@ def update_tab_yearly(_, var, global_local, df, meta, si_ip): State(ElementIds.ID_EXPLORER_SI_IP_UNIT_STORE, "data"), ], ) -def update_tab_daily(_, var, global_local, df, meta, si_ip): +def update_tab_daily(_, var, global_local, global_filter_data, df, meta, si_ip): """Update the contents of tab size. Passing in the info from the dropdown and the general info.""" + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data) + custom_inputs = generate_custom_inputs(var) units = generate_units(si_ip) return ( @@ -610,6 +451,7 @@ def update_tab_daily(_, var, global_local, df, meta, si_ip): Input(ElementIds.ID_EXPLORER_DF_STORE, "modified_timestamp"), Input(ElementIds.SEC1_VAR_DROPDOWN, "value"), Input(ElementIds.ID_EXPLORER_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_EXPLORER_DF_STORE, "data"), @@ -617,8 +459,13 @@ def update_tab_daily(_, var, global_local, df, meta, si_ip): State(ElementIds.ID_EXPLORER_SI_IP_UNIT_STORE, "data"), ], ) -def update_tab_heatmap(_, var, global_local, df, meta, si_ip): +def update_tab_heatmap(_, var, global_local, global_filter_data, df, meta, si_ip): + """Update the contents of tab size. Passing in the info from the dropdown and the general info.""" """Update the contents of tab size. Passing in the info from the dropdown and the general info.""" + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data) + custom_inputs = generate_custom_inputs(var) units = generate_units(si_ip) return ( @@ -641,54 +488,56 @@ def update_tab_heatmap(_, var, global_local, df, meta, si_ip): [ Input(ElementIds.ID_EXPLORER_DF_STORE, "modified_timestamp"), Input(ElementIds.SEC2_VAR_DROPDOWN, "value"), - Input(ElementIds.SEC2_TIME_FILTER_INPUT, "n_clicks"), Input(ElementIds.SEC2_DATA_FILTER_INPUT, "n_clicks"), Input(ElementIds.NORMALIZE, "value"), Input(ElementIds.ID_EXPLORER_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], # General [ State(ElementIds.ID_EXPLORER_DF_STORE, "data"), - State(ElementIds.SEC2_MONTH_SLIDER, "value"), - State(ElementIds.SEC2_HOUR_SLIDER, "value"), State(ElementIds.SEC2_DATA_FILTER_VAR, "value"), State(ElementIds.SEC2_MIN_VAL, "value"), State(ElementIds.SEC2_MAX_VAL, "value"), State(ElementIds.ID_EXPLORER_META_STORE, "data"), - State(ElementIds.INVERT_MONTH_EXPLORE_HEATMAP, "value"), - State(ElementIds.INVERT_HOUR_EXPLORE_HEATMAP, "value"), State(ElementIds.ID_EXPLORER_SI_IP_UNIT_STORE, "data"), ], ) def update_heatmap( _, var, - time_filter, data_filter, normalize, global_local, + global_filter_data, df, - month, - hour, filter_var, min_val, max_val, meta, - invert_month, - invert_hour, si_ip, ): - df = filter_df_by_month_and_hour( - df, time_filter, month, hour, invert_month, invert_hour, var - ) - data_filter_info = [data_filter, filter_var, min_val, max_val] + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + df = apply_global_month_hour_filter(df, global_filter_data, var) + + filter_state = get_global_filter_state(global_filter_data) + month_range = filter_state["month_range"] + hour_range = filter_state["hour_range"] + invert_month_global = filter_state["invert_month"] + invert_hour_global = filter_state["invert_hour"] + + start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( + month_range, hour_range, invert_month_global, invert_hour_global + ) + else: + # Use default values when global filter is not active + start_month, end_month, start_hour, end_hour = 1, 12, 0, 24 - start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( - month, hour, invert_month, invert_hour - ) + data_filter_info = [data_filter, filter_var, min_val, max_val] month = [start_month, end_month] hour = [start_hour, end_hour] - time_filter_info = [time_filter, month, hour] + time_filter_info = [True, month, hour] heat_map = custom_heatmap( df, global_local, var, time_filter_info, data_filter_info, si_ip @@ -754,23 +603,19 @@ def update_heatmap( [Output(ElementIds.THREE_VAR, "children"), Output(ElementIds.TWO_VAR, "children")], [ Input(ElementIds.ID_EXPLORER_DF_STORE, "modified_timestamp"), - Input(ElementIds.TAB6_SEC3_VAR_X_DROPDOWN, "value"), - Input(ElementIds.TAB6_SEC3_VAR_Y_DROPDOWN, "value"), - Input(ElementIds.TAB6_SEC3_COLORBY_DROPDOWN, "value"), - Input(ElementIds.TAB6_SEC3_TIME_FILTER_INPUT, "n_clicks"), - Input(ElementIds.TAB6_SEC3_DATA_FILTER_INPUT, "n_clicks"), + Input(ElementIds.EXPLORER_SEC3_VAR_X_DROPDOWN, "value"), + Input(ElementIds.EXPLORER_SEC3_VAR_Y_DROPDOWN, "value"), + Input(ElementIds.EXPLORER_SEC3_COLORBY_DROPDOWN, "value"), + Input(ElementIds.EXPLORER_SEC3_DATA_FILTER_INPUT, "n_clicks"), Input(ElementIds.ID_EXPLORER_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_EXPLORER_DF_STORE, "data"), - State(ElementIds.TAB6_SEC3_QUERY_MONTH_SLIDER, "value"), - State(ElementIds.TAB6_SEC3_QUERY_HOUR_SLIDER, "value"), - State(ElementIds.TAB6_SEC3_FILTER_VAR_DROPDOWN, "value"), - State(ElementIds.TAB6_SEC3_MIN_VAL, "value"), - State(ElementIds.TAB6_SEC3_MAX_VAL, "value"), + State(ElementIds.EXPLORER_SEC3_FILTER_VAR_DROPDOWN, "value"), + State(ElementIds.EXPLORER_SEC3_MIN_VAL, "value"), + State(ElementIds.EXPLORER_SEC3_MAX_VAL, "value"), State(ElementIds.ID_EXPLORER_META_STORE, "data"), - State(ElementIds.INVERT_MONTH_EXPLORE_MORE_CHARTS, "value"), - State(ElementIds.INVERT_HOUR_EXPLORE_MORE_CHARTS, "value"), State(ElementIds.ID_EXPLORER_SI_IP_UNIT_STORE, "data"), ], ) @@ -779,18 +624,14 @@ def update_more_charts( var_x, var_y, color_by, - time_filter, data_filter, global_local, + global_filter_data, df, - month, - hour, data_filter_var, min_val, max_val, meta, - invert_month, - invert_hour, si_ip, ): """Update the contents of tab size. Passing in the info from the dropdown and the general info.""" @@ -798,9 +639,14 @@ def update_more_charts( # if (min_val3 is None or max_val3 is None) and data_filter3: # raise PreventUpdate - df = filter_df_by_month_and_hour( - df, time_filter, month, hour, invert_month, invert_hour, df.columns - ) + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data) + else: + # Use local filtering when global filter is not active + df = filter_df_by_month_and_hour( + df, True, [1, 12], [0, 24], [], [], df.columns + ) data_filter_info = [data_filter, data_filter_var, min_val, max_val] if data_filter and (min_val is None or max_val is None): @@ -853,38 +699,30 @@ def update_more_charts( [ Input(ElementIds.ID_EXPLORER_DF_STORE, "modified_timestamp"), Input(ElementIds.SEC1_VAR_DROPDOWN, "value"), - Input(ElementIds.SEC1_TIME_FILTER_INPUT, "n_clicks"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_EXPLORER_DF_STORE, "data"), State(ElementIds.ID_EXPLORER_SI_IP_UNIT_STORE, "data"), - State(ElementIds.SEC1_MONTH_SLIDER, "value"), - State(ElementIds.SEC1_HOUR_SLIDER, "value"), - State(ElementIds.INVERT_MONTH_EXPLORE_DESCRIPTIVE, "value"), - State(ElementIds.INVERT_HOUR_EXPLORE_DESCRIPTIVE, "value"), ], ) def update_table( _, dd_value, - __, + global_filter_data, df, si_ip, - month_range, - hour_range, - invert_month, - invert_hour, ): - start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( - month_range, hour_range, invert_month, invert_hour - ) + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + filtered_df = apply_global_month_hour_filter(df, global_filter_data) + # Filter out the filtered rows to avoid empty columns + if '_is_filtered' in filtered_df.columns: + filtered_df = filtered_df[~filtered_df['_is_filtered']] + else: + # Use default values when global filter is not active + filtered_df = df - filtered_df = df[ - (df[Variables.MONTH.col_name] >= start_month) - & (df[Variables.MONTH.col_name] <= end_month) - & (df[Variables.HOUR.col_name] >= start_hour) - & (df[Variables.HOUR.col_name] <= end_hour) - ] return summary_table_tmp_rh_tab( filtered_df[ [ diff --git a/pages/lib/charts_data_explorer.py b/pages/lib/charts_data_explorer.py index f8541d5..cfee251 100644 --- a/pages/lib/charts_data_explorer.py +++ b/pages/lib/charts_data_explorer.py @@ -60,8 +60,69 @@ def custom_heatmap(df, global_local, var, time_filter_info, data_filter_info, si f" when the {filter_name} is between {min_val} and {max_val} {filter_unit}" ) - fig = go.Figure( - data=go.Heatmap( + fig = go.Figure() + + has_filter_marker = '_is_filtered' in df.columns + + if has_filter_marker and df['_is_filtered'].any(): + filtered_mask = df['_is_filtered'] + if filtered_mask.any(): + original_col = f'_{var}_original' + if original_col in df.columns: + filtered_z = df[original_col].copy() + else: + filtered_z = df[var].copy() + + filtered_z[~filtered_mask] = None + + fig.add_trace(go.Heatmap( + y=df[Variables.HOUR.col_name], + x=df[Variables.DOY.col_name], + z=filtered_z, + colorscale=[[0, 'lightgray'], [1, 'gray']], + zmin=range_z[0], + zmax=range_z[1], + showscale=False, + connectgaps=False, + hoverongaps=False, + customdata=np.stack((df[Variables.MONTH.col_name], df[Variables.DAY.col_name]), axis=-1), + hovertemplate=( + "Filtered Data
" + + "Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" + " %{y}:00
" + ), + name="filtered", + )) + + normal_mask = ~filtered_mask + normal_z = df[var].copy() + normal_z[filtered_mask] = None + + fig.add_trace(go.Heatmap( + y=df[Variables.HOUR.col_name], + x=df[Variables.DOY.col_name], + z=normal_z, + colorscale=var_color, + zmin=range_z[0], + zmax=range_z[1], + connectgaps=False, + hoverongaps=False, + customdata=np.stack((df[Variables.MONTH.col_name], df[Variables.DAY.col_name]), axis=-1), + hovertemplate=( + "" + + var + + ": %{z:.2f} " + + var_unit + + "
" + + "Month: %{customdata[0]}
" + + "Day: %{customdata[1]}
" + + "Hour: %{y}:00
" + ), + name="", + colorbar=dict(title=var_unit), + )) + else: + fig.add_trace(go.Heatmap( y=df[Variables.HOUR.col_name], x=df[Variables.DOY.col_name], z=df[var], diff --git a/pages/lib/charts_sun.py b/pages/lib/charts_sun.py index b476636..dd7eea0 100644 --- a/pages/lib/charts_sun.py +++ b/pages/lib/charts_sun.py @@ -33,6 +33,8 @@ def monthly_solar(epw_df, si_ip): .median() .reset_index() ) + + # Always show 12 months in horizontal layout fig = make_subplots( rows=1, cols=12, @@ -40,18 +42,19 @@ def monthly_solar(epw_df, si_ip): shared_yaxes=True, ) - for i in range(12): + for month_num in range(1, 13): + col_idx = month_num # We only need legend entries for the first pair, since the others repeat. - is_first = i == 0 + is_first = col_idx == 1 fig.add_trace( go.Scatter( x=g_h_rad_month_ave.loc[ - g_h_rad_month_ave[Variables.MONTH.col_name] == i + 1, + g_h_rad_month_ave[Variables.MONTH.col_name] == month_num, Variables.HOUR.col_name, ], y=g_h_rad_month_ave.loc[ - g_h_rad_month_ave[Variables.MONTH.col_name] == i + 1, + g_h_rad_month_ave[Variables.MONTH.col_name] == month_num, Variables.GLOB_HOR_RAD.col_name, ], fill="tozeroy", @@ -61,7 +64,7 @@ def monthly_solar(epw_df, si_ip): name="Global", showlegend=is_first, customdata=epw_df.loc[ - epw_df[Variables.MONTH.col_name] == i + 1, + epw_df[Variables.MONTH.col_name] == month_num, Variables.MONTH_NAMES.col_name, ], hovertemplate=( @@ -78,17 +81,17 @@ def monthly_solar(epw_df, si_ip): ), ), row=1, - col=i + 1, + col=col_idx, ) fig.add_trace( go.Scatter( x=dif_h_rad_month_ave.loc[ - dif_h_rad_month_ave[Variables.MONTH.col_name] == i + 1, + dif_h_rad_month_ave[Variables.MONTH.col_name] == month_num, Variables.HOUR.col_name, ], y=dif_h_rad_month_ave.loc[ - dif_h_rad_month_ave[Variables.MONTH.col_name] == i + 1, + dif_h_rad_month_ave[Variables.MONTH.col_name] == month_num, Variables.DIF_HOR_RAD.col_name, ], fill="tozeroy", @@ -98,7 +101,7 @@ def monthly_solar(epw_df, si_ip): name="Diffuse", showlegend=is_first, customdata=epw_df.loc[ - epw_df[Variables.MONTH.col_name] == i + 1, + epw_df[Variables.MONTH.col_name] == month_num, Variables.MONTH_NAMES.col_name, ], hovertemplate=( @@ -115,10 +118,10 @@ def monthly_solar(epw_df, si_ip): ), ), row=1, - col=i + 1, + col=col_idx, ) - fig.update_xaxes(range=[0, 25], row=1, col=i + 1) + fig.update_xaxes(range=[0, 25], row=1, col=col_idx) if si_ip == UnitSystem.SI: fig.update_yaxes(range=[0, 1000]) diff --git a/pages/lib/global_element_ids.py b/pages/lib/global_element_ids.py index 63709a8..c88b3df 100644 --- a/pages/lib/global_element_ids.py +++ b/pages/lib/global_element_ids.py @@ -74,22 +74,22 @@ class ElementIds(str, Enum): SEC2_MONTH_SLIDER = "sec2-month-slider" SEC2_MIN_VAL = "sec2-min-val" SEC2_MAX_VAL = "sec2-max-val" - TAB7_DROPDOWN = "tab7-dropdown" + OUTDOOR_DROPDOWN = "outdoor-dropdown" ID_T_RH_SI_IP_UNIT_STORE = "si-ip-unit-store" SWITCHES_INPUT = "switches-input" TABLE_TMP_HUM = "table-tmp-hum" TABLE_DATA_EXPLORER = "table-data-explorer" - TAB6_SEC2_CONTAINER = "tab6-sec2-container" - TAB6_SEC3_DATA_FILTER_INPUT = "tab6-sec3-data-filter-input" - TAB6_SEC3_FILTER_VAR_DROPDOWN = "tab6-sec3-filter-var-dropdown" - TAB6_SEC3_MIN_VAL = "tab6-sec3-min-val" - TAB6_SEC3_MAX_VAL = "tab6-sec3-max-val" - TAB6_SEC3_TIME_FILTER_INPUT = "tab6-sec3-time-filter-input" - TAB6_SEC3_QUERY_HOUR_SLIDER = "tab6-sec3-query-hour-slider" - TAB6_SEC3_QUERY_MONTH_SLIDER = "tab6-sec3-query-month-slider" - TAB6_SEC3_VAR_X_DROPDOWN = "tab6-sec3-var-x-dropdown" - TAB6_SEC3_VAR_Y_DROPDOWN = "tab6-sec3-var-y-dropdown" - TAB6_SEC3_COLORBY_DROPDOWN = "tab6-sec3-colorby-dropdown" + EXPLORER_SEC2_CONTAINER = "explorer-sec2-container" + EXPLORER_SEC3_DATA_FILTER_INPUT = "explorer-sec3-data-filter-input" + EXPLORER_SEC3_FILTER_VAR_DROPDOWN = "explorer-sec3-filter-var-dropdown" + EXPLORER_SEC3_MIN_VAL = "explorer-sec3-min-val" + EXPLORER_SEC3_MAX_VAL = "explorer-sec3-max-val" + EXPLORER_SEC3_TIME_FILTER_INPUT = "explorer-sec3-time-filter-input" + EXPLORER_SEC3_QUERY_HOUR_SLIDER = "explorer-sec3-query-hour-slider" + EXPLORER_SEC3_QUERY_MONTH_SLIDER = "explorer-sec3-query-month-slider" + EXPLORER_SEC3_VAR_X_DROPDOWN = "explorer-sec3-var-x-dropdown" + EXPLORER_SEC3_VAR_Y_DROPDOWN = "explorer-sec3-var-y-dropdown" + EXPLORER_SEC3_COLORBY_DROPDOWN = "explorer-sec3-colorby-dropdown" MONTH_HOUR_FILTER_OUTDOOR_COMFORT = "month-hour-filter-outdoor-comfort" TWO_VAR = "two-var" THREE_VAR = "three-var" @@ -111,9 +111,9 @@ class ElementIds(str, Enum): CUSTOM_SUN_VIEW_DROPDOWN = "custom-sun-view-dropdown" CUSTOM_SUN_VAR_DROPDOWN = "custom-sun-var-dropdown" CUSTOM_SUNPATH = "custom-sunpath" - TAB_EXPLORE_DROPDOWN = "tab4-explore-dropdown" - TAB4_DAILY = "tab4-daily" - TAB4_HEATMAP = "tab4-heatmap" + SUN_EXPLORE_DROPDOWN = "sun-explore-dropdown" + SUN_DAILY = "sun-daily" + SUN_HEATMAP = "sun-heatmap" STATIC_SECTION = "static-section" TAB_FOUR_CONTAINER = "tab-four-container" MONTHLY_SOLAR = "monthly-solar" @@ -134,7 +134,7 @@ class ElementIds(str, Enum): SUMMER_WIND_ROSE_TEXT = "summer-wind-rose-text" FALL_WIND_ROSE = "fall-wind-rose" FALL_WIND_ROSE_TEXT = "fall-wind-rose-text" - TAB5_DAILY_CONTAINER = "tab5-daily-container" + WIND_DAILY_CONTAINER = "wind-daily-container" DAILY_WIND_ROSE_OUTER_CONTAINER = "daily-wind-rose-outer-container" MORNING_WIND_ROSE = "morning-wind-rose" MORNING_WIND_ROSE_TEXT = "morning-wind-rose-text" @@ -142,11 +142,6 @@ class ElementIds(str, Enum): NOON_WIND_ROSE_TEXT = "noon-wind-rose-text" NIGHT_WIND_ROSE = "night-wind-rose" NIGHT_WIND_ROSE_TEXT = "night-wind-rose-text" - TAB5_CUSTOM_DROPDOWN_CONTAINER = "tab5-custom-dropdown-container" - TAB5_CUSTOM_START_MONTH = "tab5-custom-start-month" - TAB5_CUSTOM_START_HOUR = "tab5-custom-start-hour" - TAB5_CUSTOM_END_MONTH = "tab5-custom-end-month" - TAB5_CUSTOM_END_HOUR = "tab5-custom-end-hour" CUSTOM_WIND_ROSE = "custom-wind-rose" WIND_ROSE = "wind-rose" WIND_SPEED = "wind-speed" @@ -174,7 +169,7 @@ class ElementIds(str, Enum): ID_SELECT_LINES_STORE = "lines-store" TAB_TWO_CONTAINER = "tab-two-container" ID_SUMMARY_SI_IP_RADIO_INPUT = "si-ip-radio-input" - TAB2_SCE1_CONTAINER = "tab2-sec1-container" + SUMMARY_SCE1_CONTAINER = "summary-sec1-container" LOCATION_INFO = "location-info" WORLD_MAP = "world-map" DOWN_EPW_BUTTON = "download-epw-button" @@ -233,3 +228,17 @@ class ElementIds(str, Enum): APP_SHELL = "appshell" NAVBAR_CONTAINER = "navbar-container" TOOLS_MENU_EXPANDED = "tools-menu-expanded" + + # Tools Menu - Unified Filter Controls + TOOLS_APPLY_FILTER = "tools-apply-filter" + TOOLS_APPLY_MONTH_HOUR_FILTER = "tools-apply-month-hour-filter" + TOOLS_FILTER_VAR_DROPDOWN = "tools-filter-var-dropdown" + TOOLS_FILTER_MIN_VAL = "tools-filter-min-val" + TOOLS_FILTER_MAX_VAL = "tools-filter-max-val" + TOOLS_MONTH_SLIDER = "tools-month-slider" + TOOLS_HOUR_SLIDER = "tools-hour-slider" + TOOLS_INVERT_MONTH = "tools-invert-month" + TOOLS_INVERT_HOUR = "tools-invert-hour" + TOOLS_FILTER_SECTION = "tools-filter-section" + TOOLS_MONTH_HOUR_SECTION = "tools-month-hour-section" + TOOLS_GLOBAL_FILTER_STORE = "tools-global-filter-store" \ No newline at end of file diff --git a/pages/lib/layout.py b/pages/lib/layout.py index 90c0f49..7cfc014 100644 --- a/pages/lib/layout.py +++ b/pages/lib/layout.py @@ -5,6 +5,7 @@ from pages.lib.global_variables import Variables from config import DocLinks, UnitSystem from pages.lib.global_element_ids import ElementIds +from pages.lib.utils import determine_month_and_hour_filter class NavBarIcons: @@ -37,6 +38,110 @@ def get_icon(cls, page_name): """Get icon for a page name.""" return cls._ICON_MAP.get(page_name, "tabler:circle") +# global filters +def create_tools_filter_components(): + # Apply month and hour filter + apply_month_hour_section = dmc.Stack( + id=ElementIds.TOOLS_MONTH_HOUR_SECTION, + children=[ + dmc.Divider( + label="Filter function", size="xs", color="blue", mb="xs" + ), + dmc.Button( + "Apply month and hour filter", + id=ElementIds.TOOLS_APPLY_MONTH_HOUR_FILTER, + color="blue", + variant="light", + size="xs", + mb="xs", + ), + dmc.Stack( + [ + dmc.Text("Month Range:", size="xs", c="dimmed"), + dmc.Stack( + [ + dcc.RangeSlider( + id=ElementIds.TOOLS_MONTH_SLIDER, + min=1, + max=12, + step=1, + value=[1, 12], + marks={1: "1", 12: "12"}, + tooltip={ + "always_visible": False, + "placement": "top", + }, + allowCross=False, + ), + dmc.Group( + [ + dmc.Switch( + id=ElementIds.TOOLS_INVERT_MONTH, + label="Invert", + checked=False, + size="xs", + color="blue", + style={"fontSize": "0.7rem"}, + ), + ], + justify="flex-end", + ), + ], + gap="xs", + ), + ], + gap="xs", + mb="xs", + ), + dmc.Stack( + [ + dmc.Text("Hour Range:", size="xs", c="dimmed"), + dmc.Stack( + [ + dcc.RangeSlider( + id=ElementIds.TOOLS_HOUR_SLIDER, + min=0, + max=24, + step=1, + value=[0, 24], + marks={0: "0", 24: "24"}, + tooltip={ + "always_visible": False, + "placement": "top", + }, + allowCross=False, + ), + dmc.Group( + [ + dmc.Switch( + id=ElementIds.TOOLS_INVERT_HOUR, + label="Invert", + checked=False, + size="xs", + color="blue", + style={"fontSize": "0.7rem"}, + ), + ], + justify="flex-end", + ), + ], + gap="xs", + ), + ], + gap="xs", + ), + ], + gap="xs", + p="xs", + style={"backgroundColor": "#f8f9fa", "borderRadius": "6px", "border": "1px solid #e9ecef"}, + ) + + return dmc.Stack( + children=[ + apply_month_hour_section, + ], + gap="sm", + ) def create_navbar(): nav_link_styles = { @@ -69,7 +174,7 @@ def create_navbar(): styles=nav_link_styles, ) for page in dash.page_registry.values() - if page[Variables.NAME.col_name] not in ["404"] + if page[Variables.NAME.col_name] not in ["404", "Changelog"] ] parent_group = dmc.NavLink( @@ -138,10 +243,12 @@ def create_navbar(): ], ) + filter_components = create_tools_filter_components() + # Tools controls_group = dmc.NavLink( label="Tools Menu", - children=[controls_stack], + children=[controls_stack, filter_components], id=ElementIds.NAV_GROUP_CONTROLS, variant="light", childrenOffset=0, @@ -317,6 +424,17 @@ def create_stores(): dcc.Store( id=ElementIds.TOOLS_MENU_EXPANDED, data=False, storage_type="session" ), + dcc.Store( + id=ElementIds.TOOLS_GLOBAL_FILTER_STORE, + data={ + "month_range": [1, 12], + "hour_range": [0, 24], + "invert_month": [], + "invert_hour": [], + "filter_active": False + }, + storage_type="session" + ), dcc.Interval( id=ElementIds.ID_LAYOUT_INTERVAL_COMPONENT, interval=12 * 1000, @@ -406,7 +524,7 @@ def toggle_navbar_and_width( [ Output(f"nav-{page[Variables.PATH.col_name].replace('/', '')}", "active") for page in dash.page_registry.values() - if page[Variables.NAME.col_name] not in ["404"] + if page[Variables.NAME.col_name] not in ["404", "Changelog"] ], Input(ElementIds.MAIN_URL, "pathname"), prevent_initial_call=True, @@ -415,7 +533,7 @@ def update_nav_active_state(pathname): return [ pathname == page[Variables.PATH.col_name] for page in dash.page_registry.values() - if page[Variables.NAME.col_name] not in ["404"] + if page[Variables.NAME.col_name] not in ["404", "Changelog"] ] @@ -426,3 +544,129 @@ def update_nav_active_state(pathname): ) def show_alert_after_delay(n_intervals): return {"display": "block" if n_intervals == 1 else "none"} + + +@callback( + Output(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), + [ + Input(ElementIds.TOOLS_APPLY_MONTH_HOUR_FILTER, "n_clicks"), + ], + [ + State(ElementIds.TOOLS_MONTH_SLIDER, "value"), + State(ElementIds.TOOLS_HOUR_SLIDER, "value"), + State(ElementIds.TOOLS_INVERT_MONTH, "checked"), + State(ElementIds.TOOLS_INVERT_HOUR, "checked"), + State(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), + ], + prevent_initial_call=True, +) +def update_global_filter_state( + apply_clicks, month_range, hour_range, invert_month, invert_hour, current_data +): + if apply_clicks is None: + apply_clicks = 0 + + if apply_clicks > 0: + current_data["filter_active"] = True + + current_data.update({ + "month_range": month_range or [1, 12], + "hour_range": hour_range or [0, 24], + "invert_month": ["invert"] if invert_month else [], + "invert_hour": ["invert"] if invert_hour else [], + }) + + return current_data + + +def get_global_filter_state(filter_store_data): + if not filter_store_data: + return { + "filter_active": False, + "month_range": [1, 12], + "hour_range": [0, 24], + "invert_month": False, + "invert_hour": False, + } + + return { + "filter_active": filter_store_data.get("filter_active", False), + "month_range": filter_store_data.get("month_range", [1, 12]), + "hour_range": filter_store_data.get("hour_range", [0, 24]), + "invert_month": bool(filter_store_data.get("invert_month", [])), + "invert_hour": bool(filter_store_data.get("invert_hour", [])), + } + + +def apply_global_month_hour_filter(df, filter_store_data, target_columns=None): + filter_state = get_global_filter_state(filter_store_data) + + if not filter_state["filter_active"]: + df_copy = df.copy() + df_copy['_is_filtered'] = False + return df_copy + + month_range = filter_state["month_range"] + hour_range = filter_state["hour_range"] + invert_month = filter_state["invert_month"] + invert_hour = filter_state["invert_hour"] + + start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( + month_range, hour_range, invert_month, invert_hour + ) + + df_copy = df.copy() + + if target_columns is None: + target_columns = [Variables.DBT.col_name] + elif isinstance(target_columns, str): + target_columns = [target_columns] + + month_mask = None + if start_month <= end_month: + month_mask = (df_copy[Variables.MONTH.col_name] < start_month) | (df_copy[Variables.MONTH.col_name] > end_month) + else: + month_mask = (df_copy[Variables.MONTH.col_name] >= end_month) & (df_copy[Variables.MONTH.col_name] <= start_month) + + hour_mask = None + if start_hour <= end_hour: + hour_mask = (df_copy[Variables.HOUR.col_name] < start_hour) | (df_copy[Variables.HOUR.col_name] > end_hour) + else: + hour_mask = (df_copy[Variables.HOUR.col_name] >= end_hour) & (df_copy[Variables.HOUR.col_name] <= start_hour) + + df_copy['_is_filtered'] = month_mask | hour_mask + + for target_col in target_columns: + df_copy[f'_{target_col}_original'] = df_copy[target_col] + + from pages.lib.template_graphs import time_filtering + time_filtering(df_copy, start_month, end_month, Variables.MONTH.col_name, target_col) + time_filtering(df_copy, start_hour, end_hour, Variables.MONTH.col_name, target_col) + + return df_copy + + +@callback( + [ + Output(ElementIds.TOOLS_MONTH_SLIDER, "value"), + Output(ElementIds.TOOLS_HOUR_SLIDER, "value"), + Output(ElementIds.TOOLS_INVERT_MONTH, "checked"), + Output(ElementIds.TOOLS_INVERT_HOUR, "checked"), + ], + [ + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), + ], + prevent_initial_call=False, +) +def sync_sliders_with_global_state(global_filter_data): + if not global_filter_data: + return [1, 12], [0, 24], False, False + + return ( + global_filter_data.get("month_range", [1, 12]), + global_filter_data.get("hour_range", [0, 24]), + bool(global_filter_data.get("invert_month", [])), + bool(global_filter_data.get("invert_hour", [])), + ) + + diff --git a/pages/lib/template_graphs.py b/pages/lib/template_graphs.py index 1fb189d..633fc3a 100644 --- a/pages/lib/template_graphs.py +++ b/pages/lib/template_graphs.py @@ -378,10 +378,18 @@ def heatmap_with_filter( var_range = variable.get_range(si_ip) var_color = variable.get_color() + has_global_filter_marker = '_is_filtered' in df.columns + global_filter_mask = None + if has_global_filter_marker: + global_filter_mask = df['_is_filtered'].copy() + df = filter_df_by_month_and_hour( df, time_filter, month, hour, invert_month, invert_hour, var ) + if has_global_filter_marker and global_filter_mask is not None: + df['_is_filtered'] = global_filter_mask + start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( month, hour, invert_month, invert_hour ) @@ -404,8 +412,63 @@ def heatmap_with_filter( # Set maximum and minimum according to data data_max, data_min = get_max_min_value(df[var]) range_z = [data_min, data_max] - fig = go.Figure( - data=go.Heatmap( + fig = go.Figure() + + has_filter_marker = '_is_filtered' in df.columns + + if has_filter_marker and df['_is_filtered'].any(): + filtered_mask = df['_is_filtered'] + if filtered_mask.any(): + original_col = f'_{var}_original' + if original_col in df.columns: + filtered_z = df[original_col].copy() + else: + filtered_z = df[var].copy() + + filtered_z[~filtered_mask] = None + + fig.add_trace(go.Heatmap( + y=df[Variables.HOUR.col_name] - 0.5, + x=df[Variables.UTC_TIME.col_name].dt.date, + z=filtered_z, + colorscale=[[0, 'lightgray'], [1, 'gray']], + zmin=range_z[0], + zmax=range_z[1], + showscale=False, + customdata=np.stack((df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), axis=-1), + hovertemplate=( + "Filtered Data
" + + "Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" + " %{y}:00
" + ), + name="filtered", + )) + + normal_mask = ~filtered_mask + normal_z = df[var].copy() + normal_z[filtered_mask] = None + + fig.add_trace(go.Heatmap( + y=df[Variables.HOUR.col_name] - 0.5, + x=df[Variables.UTC_TIME.col_name].dt.date, + z=normal_z, + colorscale=var_color, + zmin=range_z[0], + zmax=range_z[1], + customdata=np.stack((df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), axis=-1), + hovertemplate=( + "" + + var + + ": %{z:.2f} " + + var_unit + + "
Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" + " %{y}:00
" + ), + name="", + colorbar=dict(title="") if "_categories" in var else dict(title=var_unit), + )) + else: + fig.add_trace(go.Heatmap( y=df[Variables.HOUR.col_name] - 0.5, # Offset by 0.5 to center the hour labels x=df[Variables.UTC_TIME.col_name].dt.date, @@ -426,7 +489,7 @@ def heatmap_with_filter( " %{y}:00
" ), name="", - colorbar=dict(title=var_unit), + colorbar=dict(title="") if "_categories" in var else dict(title=var_unit), ) ) @@ -473,8 +536,63 @@ def heatmap(df, var, global_local, si_ip): # Set maximum and minimum according to data data_max, data_min = get_max_min_value(df[var]) range_z = [data_min, data_max] - fig = go.Figure( - data=go.Heatmap( + fig = go.Figure() + + has_filter_marker = '_is_filtered' in df.columns + + if has_filter_marker and df['_is_filtered'].any(): + filtered_mask = df['_is_filtered'] + if filtered_mask.any(): + original_col = f'_{var}_original' + if original_col in df.columns: + filtered_z = df[original_col].copy() + else: + filtered_z = df[var].copy() + + filtered_z[~filtered_mask] = None + + fig.add_trace(go.Heatmap( + y=df[Variables.HOUR.col_name], + x=df[Variables.UTC_TIME.col_name].dt.date, + z=filtered_z, + colorscale=[[0, 'lightgray'], [1, 'gray']], + zmin=range_z[0], + zmax=range_z[1], + showscale=False, + customdata=np.stack((df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), axis=-1), + hovertemplate=( + "Filtered Data
" + + "Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" + " %{y}:00
" + ), + name="filtered", + )) + + normal_mask = ~filtered_mask + normal_z = df[var].copy() + normal_z[filtered_mask] = None + + fig.add_trace(go.Heatmap( + y=df[Variables.HOUR.col_name], + x=df[Variables.UTC_TIME.col_name].dt.date, + z=normal_z, + colorscale=var_color, + zmin=range_z[0], + zmax=range_z[1], + customdata=np.stack((df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), axis=-1), + hovertemplate=( + "" + + var + + ": %{z:.2f} " + + var_unit + + "
Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" + " %{y}:00
" + ), + name="", + colorbar=dict(title=var_unit), + )) + else: + fig.add_trace(go.Heatmap( y=df[Variables.HOUR.col_name], x=df[Variables.UTC_TIME.col_name].dt.date, z=df[var], @@ -529,35 +647,36 @@ def speed_labels(bins, units): return labels -def wind_rose(df, title, month, hour, labels, si_ip): +def wind_rose(df, title, month, hour, labels, si_ip, skip_time_filter=False): """Return the wind rose figure. Based on: https://gist.github.com/phobson/41b41bdd157a2bcf6e14 """ - start_month = month[0] - end_month = month[1] - start_hour = hour[0] - end_hour = hour[1] - if start_month <= end_month: - df = df.loc[ - (df[Variables.MONTH.col_name] >= start_month) - & (df[Variables.MONTH.col_name] <= end_month) - ] - else: - df = df.loc[ - (df[Variables.MONTH.col_name] <= end_month) - | (df[Variables.MONTH.col_name] >= start_month) - ] - if start_hour <= end_hour: - df = df.loc[ - (df[Variables.HOUR.col_name] > start_hour) - & (df[Variables.HOUR.col_name] <= end_hour) - ] - else: - df = df.loc[ - (df[Variables.HOUR.col_name] <= end_hour) - | (df[Variables.HOUR.col_name] >= start_hour) - ] + if not skip_time_filter: + start_month = month[0] + end_month = month[1] + start_hour = hour[0] + end_hour = hour[1] + if start_month <= end_month: + df = df.loc[ + (df[Variables.MONTH.col_name] >= start_month) + & (df[Variables.MONTH.col_name] <= end_month) + ] + else: + df = df.loc[ + (df[Variables.MONTH.col_name] <= end_month) + | (df[Variables.MONTH.col_name] >= start_month) + ] + if start_hour <= end_hour: + df = df.loc[ + (df[Variables.HOUR.col_name] > start_hour) + & (df[Variables.HOUR.col_name] <= end_hour) + ] + else: + df = df.loc[ + (df[Variables.HOUR.col_name] <= end_hour) + | (df[Variables.HOUR.col_name] >= start_hour) + ] wind_speed_variable = VariableInfo.from_col_name(Variables.WIND_SPEED.col_name) @@ -784,8 +903,11 @@ def thermal_stress_stacked_barchart( linecolor="black", mirror=True, ) + # Get available months from filtered data + available_months = sorted(new_df[Variables.MONTH.col_name].unique()) + fig.update_xaxes( - dict(tickmode="array", tickvals=np.arange(0, 12, 1), ticktext=month_lst), + dict(tickmode="array", tickvals=np.arange(0, len(available_months), 1), ticktext=month_lst), title_text="Day", showline=True, linewidth=1, @@ -831,31 +953,43 @@ def barchart(df, var, time_filter_info, data_filter_info, normalize, si_ip): if len(time_filter_info) == 1: filter_var = str(var) - for i in range(1, 13): - query = ( - f"month=={str(i)} and ({filter_var}>={min_val} and {filter_var}<={max_val})" - ) - a = new_df.query(query)[Variables.DOY.col_name].count() - month_in.append(a) - query = f"month=={str(i)} and ({filter_var}<{min_val})" - b = new_df.query(query)[Variables.DOY.col_name].count() - month_below.append(b) - query = f"month=={str(i)} and {filter_var}>{max_val}" - c = new_df.query(query)[Variables.DOY.col_name].count() - month_above.append(c) + # Always process all 12 months + available_months_set = set(new_df[Variables.MONTH.col_name].unique()) + + for month_num in range(1, 13): + if month_num in available_months_set: + query = ( + f"month=={str(month_num)} and ({filter_var}>={min_val} and {filter_var}<={max_val})" + ) + a = new_df.query(query)[Variables.DOY.col_name].count() + month_in.append(a) + query = f"month=={str(month_num)} and ({filter_var}<{min_val})" + b = new_df.query(query)[Variables.DOY.col_name].count() + month_below.append(b) + query = f"month=={str(month_num)} and {filter_var}>{max_val}" + c = new_df.query(query)[Variables.DOY.col_name].count() + month_above.append(c) + else: + # No data for this month, append zeros + month_in.append(0) + month_below.append(0) + month_above.append(0) go.Figure() + + month_names = month_lst + trace1 = go.Bar( - x=list(range(0, 13)), y=month_in, name="IN range", marker_color=color_in + x=month_names, y=month_in, name="IN range", marker_color=color_in ) trace2 = go.Bar( - x=list(range(0, 13)), + x=month_names, y=month_below, name="BELOW range", marker_color=color_below, ) trace3 = go.Bar( - x=list(range(0, 13)), + x=month_names, y=month_above, name="ABOVE range", marker_color=color_above, diff --git a/pages/lib/utils.py b/pages/lib/utils.py index ea55395..30d5e43 100644 --- a/pages/lib/utils.py +++ b/pages/lib/utils.py @@ -268,14 +268,10 @@ def summary_table_tmp_rh_tab(df, value, si_ip): def determine_month_and_hour_filter(month, hour, invert_month, invert_hour): start_month, end_month = month - if invert_month == [Variables.INVERT.col_name] and ( - start_month != 1 or end_month != 12 - ): + if invert_month and (start_month != 1 or end_month != 12): end_month, start_month = month start_hour, end_hour = hour - if invert_hour == [Variables.INVERT.col_name] and ( - start_hour != 0 or end_hour != 24 - ): + if invert_hour and (start_hour != 0 or end_hour != 24): end_hour, start_hour = hour return start_month, end_month, start_hour, end_hour diff --git a/pages/natural_ventilation.py b/pages/natural_ventilation.py index a2485bb..1749964 100644 --- a/pages/natural_ventilation.py +++ b/pages/natural_ventilation.py @@ -99,7 +99,7 @@ def update_layout(si_ip): def inputs_tab(t_min, t_max, d_set): return dmc.SimpleGrid( - cols=3, + cols=2, spacing="md", children=[ dmc.Stack( @@ -142,58 +142,6 @@ def inputs_tab(t_min, t_max, d_set): ), ] ), - dmc.Stack( - [ - dmc.Button( - "Apply month and hour filter", - color="blue", - id=ElementIds.NV_MONTH_HOUR_FILTER, - variant="link", - ), - dmc.Group( - [ - dmc.Title("Month Range", order=5), - dmc.Stack( - dcc.RangeSlider( - id=ElementIds.NV_MONTH_SLIDER, - min=1, - max=12, - step=1, - value=[1, 12], - marks={1: "1", 12: "12"}, - ), - flex=1, - ), - dcc.Checklist( - options=[{"label": "Invert", "value": "invert"}], - value=[], - id=ElementIds.INVERT_MONTH_NV, - ), - ], - ), - dmc.Group( - [ - dmc.Title("Hour Range", order=5), - dmc.Stack( - dcc.RangeSlider( - id=ElementIds.NV_HOUR_SLIDER, - min=0, - max=24, - step=1, - value=[0, 24], - marks={0: "0", 24: "24"}, - ), - flex=1, - ), - dcc.Checklist( - options=[{"label": "Invert", "value": "invert"}], - value=[], - id=ElementIds.INVERT_HOUR_NV, - ), - ], - ), - ] - ), dmc.Stack( [ dmc.Button( @@ -241,41 +189,33 @@ def inputs_tab(t_min, t_max, d_set): Output(ElementIds.NV_HEATMAP_CHART, "children"), [ Input(ElementIds.ID_NATURAL_VENTILATION_DF_STORE, "modified_timestamp"), - Input(ElementIds.NV_MONTH_HOUR_FILTER, "n_clicks"), Input(ElementIds.NV_DBT_FILTER, "n_clicks"), Input(ElementIds.NV_DPT_FILTER, "n_clicks"), Input(ElementIds.ID_NATURAL_VENTILATION_GLOBAL_LOCAL_RADIO_INPUT, "value"), Input(ElementIds.ENABLE_CONDENSATION, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_NATURAL_VENTILATION_DF_STORE, "data"), - State(ElementIds.NV_MONTH_SLIDER, "value"), - State(ElementIds.NV_HOUR_SLIDER, "value"), State(ElementIds.NV_TDB_MIN_VAL, "value"), State(ElementIds.NV_TDB_MAX_VAL, "value"), State(ElementIds.NV_DPT_MAX_VAL, "value"), State(ElementIds.ID_NATURAL_VENTILATION_META_STORE, "data"), - State(ElementIds.INVERT_MONTH_NV, "value"), - State(ElementIds.INVERT_HOUR_NV, "value"), State(ElementIds.ID_NATURAL_VENTILATION_SI_IP_UNIT_STORE, "data"), ], ) def nv_heatmap( ts, - time_filter, dbt_data_filter, click_dpt_filter, global_local, condensation_enabled, + global_filter_data, df, - month, - hour, min_dbt_val, max_dbt_val, max_dpt_val, meta, - invert_month, - invert_hour, si_ip, ): if df is None: @@ -283,9 +223,22 @@ def nv_heatmap( # enable or disable button apply filter DPT dpt_data_filter = enable_dew_point_data_filter(condensation_enabled) - start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( - month, hour, invert_month, invert_hour - ) + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + df = apply_global_month_hour_filter(df, global_filter_data, Variables.DBT.col_name) + + filter_state = get_global_filter_state(global_filter_data) + month_range = filter_state["month_range"] + hour_range = filter_state["hour_range"] + invert_month_global = filter_state["invert_month"] + invert_hour_global = filter_state["invert_hour"] + + start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( + month_range, hour_range, invert_month_global, invert_hour_global + ) + else: + # Use default values when global filter is not active + start_month, end_month, start_hour, end_hour = 1, 12, 0, 24 var = Variables.DBT.col_name filter_var = Variables.DPT.col_name @@ -311,9 +264,6 @@ def nv_heatmap( ), ) - df = filter_df_by_month_and_hour( - df, time_filter, month, hour, invert_month, invert_hour, var - ) variable = VariableInfo.from_col_name(var) filter = VariableInfo.from_col_name(filter_var) @@ -338,7 +288,8 @@ def nv_heatmap( f" {max_dbt_val} {var_unit}" ) - if time_filter: + # Title will be updated based on global filter state + if global_filter_data and global_filter_data.get("filter_active", False): title += ( f" between the months of {month_lst[start_month - 1]} and " f"{month_lst[end_month - 1]}
and between the hours {start_hour}" @@ -416,52 +367,40 @@ def nv_heatmap( Output(ElementIds.NV_BAR_CHART, "children"), [ Input(ElementIds.ID_NATURAL_VENTILATION_DF_STORE, "modified_timestamp"), - Input(ElementIds.NV_MONTH_HOUR_FILTER, "n_clicks"), Input(ElementIds.NV_DBT_FILTER, "n_clicks"), Input(ElementIds.NV_DPT_FILTER, "n_clicks"), Input(ElementIds.ID_NATURAL_VENTILATION_GLOBAL_LOCAL_RADIO_INPUT, "value"), Input(ElementIds.SWITCHES_INPUT, "checked"), Input(ElementIds.ENABLE_CONDENSATION, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_NATURAL_VENTILATION_DF_STORE, "data"), - State(ElementIds.NV_MONTH_SLIDER, "value"), - State(ElementIds.NV_HOUR_SLIDER, "value"), State(ElementIds.NV_TDB_MIN_VAL, "value"), State(ElementIds.NV_TDB_MAX_VAL, "value"), State(ElementIds.NV_DPT_MAX_VAL, "value"), State(ElementIds.ID_NATURAL_VENTILATION_META_STORE, "data"), - State(ElementIds.INVERT_MONTH_NV, "value"), - State(ElementIds.INVERT_HOUR_NV, "value"), State(ElementIds.ID_NATURAL_VENTILATION_SI_IP_UNIT_STORE, "data"), ], ) def nv_bar_chart( ts, - time_filter, dbt_data_filter, click_dpt_filter, global_local, normalize, condensation_enabled, + global_filter_data, df, - month, - hour, min_dbt_val, max_dbt_val, max_dpt_val, meta, - invert_month, - invert_hour, si_ip, ): # enable or disable button apply filter DPT dpt_data_filter = enable_dew_point_data_filter(condensation_enabled) - start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( - month, hour, invert_month, invert_hour - ) - var = Variables.DBT.col_name filter_var = Variables.DPT.col_name @@ -479,9 +418,22 @@ def nv_bar_chart( df[Variables.NV_ALLOWED.col_name] = 1 - df = filter_df_by_month_and_hour( - df, time_filter, month, hour, invert_month, invert_hour, "nv_allowed" - ) + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + df = apply_global_month_hour_filter(df, global_filter_data, Variables.NV_ALLOWED.col_name) + + filter_state = get_global_filter_state(global_filter_data) + month_range = filter_state["month_range"] + hour_range = filter_state["hour_range"] + invert_month_global = filter_state["invert_month"] + invert_hour_global = filter_state["invert_hour"] + + start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( + month_range, hour_range, invert_month_global, invert_hour_global + ) + else: + # Use default values when global filter is not active + start_month, end_month, start_hour, end_hour = 1, 12, 0, 24 # this should be the total after filtering by time tot_month_hours = ( @@ -558,7 +510,7 @@ def nv_bar_chart( ) fig.update_yaxes(title_text="Percentage (%)", range=[0, 100]) - if time_filter: + if global_filter_data and global_filter_data.get("filter_active", False): title += ( f" between the months of {month_lst[start_month - 1]} and " f"{month_lst[end_month - 1]} and between
the hours {start_hour}" diff --git a/pages/outdoor.py b/pages/outdoor.py index d0483c4..538041c 100644 --- a/pages/outdoor.py +++ b/pages/outdoor.py @@ -44,7 +44,7 @@ def inputs_outdoor_comfort(): dmc.Title("Select a scenario:", order=5), dmc.Stack( dropdown( - id=ElementIds.TAB7_DROPDOWN, + id=ElementIds.OUTDOOR_DROPDOWN, options=outdoor_dropdown_names, value="utci_Sun_Wind", persistence=True, @@ -56,60 +56,6 @@ def inputs_outdoor_comfort(): ], align="flex-start", ), - dmc.Stack( - [ - dmc.Button( - "Apply month and hour filter", - id=ElementIds.MONTH_HOUR_FILTER_OUTDOOR_COMFORT, - variant="filled", - color="blue", - ), - dmc.Group( - [ - dmc.Title("Month Range", order=5), - dmc.Stack( - dcc.RangeSlider( - id=ElementIds.OUTDOOR_COMFORT_MONTH_SLIDER, - min=1, - max=12, - step=1, - value=[1, 12], - marks={1: "1", 12: "12"}, - tooltip={"always_visible": False}, - ), - flex=1, - ), - dcc.Checklist( - id=ElementIds.INVERT_MONTH_OUTDOOR_COMFORT, - options=[{"label": "Invert", "value": "invert"}], - value=[], - ), - ], - ), - dmc.Group( - [ - dmc.Title("Hour Range", order=5), - dmc.Stack( - dcc.RangeSlider( - id=ElementIds.OUTDOOR_COMFORT_HOUR_SLIDER, - min=0, - max=24, - step=1, - value=[0, 24], - marks={0: "0", 24: "24"}, - tooltip={"always_visible": False}, - ), - flex=1, - ), - dcc.Checklist( - id=ElementIds.INVERT_HOUR_OUTDOOR_COMFORT, - options=[{"label": "Invert", "value": "invert"}], - value=[], - ), - ], - ), - ], - ), ], ) @@ -230,40 +176,71 @@ def update_outdoor_comfort_output(_, df): cols_with_the_highest_number_of_zero.append(col) elif count == highest_count: cols_with_the_highest_number_of_zero.append(col) - return f"The Best Weather Condition is: {', '.join(cols_with_the_highest_number_of_zero)}" + + # Convert column names to display names using string replacement + display_names = [] + for col in cols_with_the_highest_number_of_zero: + # Remove utci_ prefix and replace all underscores with spaces + display_name = col.replace("utci_", "UTCI ") + display_name = display_name.replace("_", " ") + display_name = display_name.replace("categories", "Categories") + # Fix specific words that need spaces + display_name = display_name.replace("noSun", "No Sun") + display_name = display_name.replace("noWind", "No Wind") + display_name = display_name.replace("Sun", "Sun") # Keep Sun as is + display_name = display_name.replace("Wind", "Wind") # Keep Wind as is + display_names.append(display_name) + + return f"The Best Weather Condition is: {', '.join(display_names)}" @callback( Output(ElementIds.UTCI_HEATMAP, "children"), [ Input(ElementIds.ID_OUTDOOR_DF_STORE, "modified_timestamp"), - Input(ElementIds.TAB7_DROPDOWN, "value"), + Input(ElementIds.OUTDOOR_DROPDOWN, "value"), Input(ElementIds.ID_OUTDOOR_GLOBAL_LOCAL_RADIO_INPUT, "value"), - Input(ElementIds.MONTH_HOUR_FILTER_OUTDOOR_COMFORT, "n_clicks"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_OUTDOOR_DF_STORE, "data"), State(ElementIds.ID_OUTDOOR_META_STORE, "data"), State(ElementIds.ID_OUTDOOR_SI_IP_UNIT_STORE, "data"), - State(ElementIds.OUTDOOR_COMFORT_MONTH_SLIDER, "value"), - State(ElementIds.OUTDOOR_COMFORT_HOUR_SLIDER, "value"), - State(ElementIds.INVERT_MONTH_OUTDOOR_COMFORT, "value"), - State(ElementIds.INVERT_HOUR_OUTDOOR_COMFORT, "value"), ], ) def update_tab_utci_value( _, var, global_local, - time_filter, + global_filter_data, df, meta, si_ip, - month, - hour, - invert_month, - invert_hour, ): + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + df = apply_global_month_hour_filter(df, global_filter_data, var) + + filter_state = get_global_filter_state(global_filter_data) + month_range = filter_state["month_range"] + hour_range = filter_state["hour_range"] + invert_month_global = filter_state["invert_month"] + invert_hour_global = filter_state["invert_hour"] + + # time_filter is always True for global filter + time_filter = True + month = month_range + hour = hour_range + invert_month = invert_month_global + invert_hour = invert_hour_global + else: + # Use default values when global filter is not active + time_filter = True + month = [1, 12] + hour = [0, 24] + invert_month = [] + invert_hour = [] + custom_inputs = f"{var}" units = generate_units_degree(si_ip) return dcc.Graph( @@ -285,7 +262,7 @@ def update_tab_utci_value( @callback( Output(ElementIds.IMAGE_SELECTION, "children"), - Input(ElementIds.TAB7_DROPDOWN, "value"), + Input(ElementIds.OUTDOOR_DROPDOWN, "value"), ) def change_image_based_on_selection(value): if value == "utci_Sun_Wind": @@ -304,33 +281,49 @@ def change_image_based_on_selection(value): Output(ElementIds.UTCI_CATEGORY_HEATMAP, "children"), [ Input(ElementIds.ID_OUTDOOR_DF_STORE, "modified_timestamp"), - Input(ElementIds.TAB7_DROPDOWN, "value"), + Input(ElementIds.OUTDOOR_DROPDOWN, "value"), Input(ElementIds.ID_OUTDOOR_GLOBAL_LOCAL_RADIO_INPUT, "value"), - Input(ElementIds.MONTH_HOUR_FILTER_OUTDOOR_COMFORT, "n_clicks"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_OUTDOOR_DF_STORE, "data"), State(ElementIds.ID_OUTDOOR_META_STORE, "data"), State(ElementIds.ID_OUTDOOR_SI_IP_UNIT_STORE, "data"), - State(ElementIds.OUTDOOR_COMFORT_MONTH_SLIDER, "value"), - State(ElementIds.OUTDOOR_COMFORT_HOUR_SLIDER, "value"), - State(ElementIds.INVERT_MONTH_OUTDOOR_COMFORT, "value"), - State(ElementIds.INVERT_HOUR_OUTDOOR_COMFORT, "value"), ], ) def update_tab_utci_category( _, var, global_local, - time_filter, + global_filter_data, df, meta, si_ip, - month, - hour, - invert_month, - invert_hour, ): + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + df = apply_global_month_hour_filter(df, global_filter_data, [var, var + "_categories"]) + + filter_state = get_global_filter_state(global_filter_data) + month_range = filter_state["month_range"] + hour_range = filter_state["hour_range"] + invert_month_global = filter_state["invert_month"] + invert_hour_global = filter_state["invert_hour"] + + # time_filter is always True for global filter + time_filter = True + month = month_range + hour = hour_range + invert_month = invert_month_global + invert_hour = invert_hour_global + else: + # time_filter is always True for local filter too + time_filter = True + month = [1, 12] + hour = [0, 24] + invert_month = [] + invert_hour = [] + utci_stress_cat = heatmap_with_filter( df, var + "_categories", @@ -343,7 +336,9 @@ def update_tab_utci_category( invert_hour, "UTCI thermal stress", ) - utci_stress_cat["data"][0]["colorbar"] = dict( + colorbar_index = 1 if len(utci_stress_cat["data"]) > 1 else 0 + + utci_stress_cat["data"][colorbar_index]["colorbar"] = dict( title="Thermal stress", titleside="top", tickmode="array", @@ -375,23 +370,43 @@ def update_tab_utci_category( @callback( Output(ElementIds.UTCI_SUMMARY_CHART, "children"), [ - Input(ElementIds.TAB7_DROPDOWN, "value"), - Input(ElementIds.MONTH_HOUR_FILTER_OUTDOOR_COMFORT, "n_clicks"), + Input(ElementIds.OUTDOOR_DROPDOWN, "value"), Input(ElementIds.OUTDOOR_COMFORT_SWITCHES_INPUT, "checked"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_OUTDOOR_DF_STORE, "data"), - State(ElementIds.OUTDOOR_COMFORT_MONTH_SLIDER, "value"), - State(ElementIds.OUTDOOR_COMFORT_HOUR_SLIDER, "value"), State(ElementIds.ID_OUTDOOR_META_STORE, "data"), - State(ElementIds.INVERT_MONTH_OUTDOOR_COMFORT, "value"), - State(ElementIds.INVERT_HOUR_OUTDOOR_COMFORT, "value"), State(ElementIds.ID_OUTDOOR_SI_IP_UNIT_STORE, "data"), ], ) def update_tab_utci_summary_chart( - var, time_filter, normalize, df, month, hour, meta, invert_month, invert_hour, si_ip + var, normalize, global_filter_data, df, meta, si_ip ): + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + df = apply_global_month_hour_filter(df, global_filter_data, var) + + filter_state = get_global_filter_state(global_filter_data) + month_range = filter_state["month_range"] + hour_range = filter_state["hour_range"] + invert_month_global = filter_state["invert_month"] + invert_hour_global = filter_state["invert_hour"] + + # time_filter is always True for global filter + time_filter = True + month = month_range + hour = hour_range + invert_month = invert_month_global + invert_hour = invert_hour_global + else: + # time_filter is always True for local filter too + time_filter = True + month = [1, 12] + hour = [0, 24] + invert_month = [] + invert_hour = [] + utci_summary_chart = thermal_stress_stacked_barchart( df, var + "_categories", diff --git a/pages/psy-chart.py b/pages/psy-chart.py index b17969a..48a1ae4 100644 --- a/pages/psy-chart.py +++ b/pages/psy-chart.py @@ -57,7 +57,7 @@ def inputs(): return dmc.SimpleGrid( - cols=3, + cols=2, children=[ dmc.Group( [ @@ -75,59 +75,6 @@ def inputs(): ], align="flex-start", ), - dmc.Stack( - [ - dmc.Button( - "Apply month and hour filter", - id=ElementIds.MONTH_HOUR_FILTER, - color="blue", - ), - dmc.Group( - [ - dmc.Title("Month Range", order=5), - dmc.Stack( - dcc.RangeSlider( - id=ElementIds.PSY_MONTH_SLIDER, - min=1, - max=12, - step=1, - value=[1, 12], - marks={1: "1", 12: "12"}, - tooltip={"always_visible": False}, - ), - flex=1, - ), - dcc.Checklist( - id=ElementIds.INVERT_MONTH_PSY, - options=[{"label": "Invert", "value": "invert"}], - value=[], - ), - ], - ), - dmc.Group( - [ - dmc.Title("Hour Range", order=5), - dmc.Stack( - dcc.RangeSlider( - id=ElementIds.PSY_HOUR_SLIDER, - min=0, - max=24, - step=1, - value=[0, 24], - marks={0: "0", 24: "24"}, - tooltip={"always_visible": False}, - ), - flex=1, - ), - dcc.Checklist( - id=ElementIds.INVERT_HOUR_PSY, - options=[{"label": "Invert", "value": "invert"}], - value=[], - ), - ], - ), - ], - ), dmc.Stack( [ dmc.Button( @@ -211,47 +158,53 @@ def layout(): [ Input(ElementIds.ID_PSY_CHART_DF_STORE, "modified_timestamp"), Input(ElementIds.PSY_COLOR_BY_DROPDOWN, "value"), - Input(ElementIds.MONTH_HOUR_FILTER, "n_clicks"), Input(ElementIds.DATA_FILTER, "n_clicks"), Input(ElementIds.ID_PSY_CHART_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_PSY_CHART_DF_STORE, "data"), - State(ElementIds.PSY_MONTH_SLIDER, "value"), - State(ElementIds.PSY_HOUR_SLIDER, "value"), State(ElementIds.PSY_MIN_VAL, "value"), State(ElementIds.PSY_MAX_VAL, "value"), State(ElementIds.PSY_VAR_DROPDOWN, "value"), State(ElementIds.ID_PSY_CHART_META_STORE, "data"), - State(ElementIds.INVERT_MONTH_PSY, "value"), - State(ElementIds.INVERT_HOUR_PSY, "value"), State(ElementIds.ID_PSY_CHART_SI_IP_UNIT_STORE, "data"), ], ) def update_psych_chart( ts, colorby_var, - time_filter, data_filter, global_local, + global_filter_data, df, - month, - hour, min_val, max_val, data_filter_var, meta, - invert_month, - invert_hour, si_ip, ): - start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( - month, hour, invert_month, invert_hour - ) + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + df = apply_global_month_hour_filter(df, global_filter_data) + + filter_state = get_global_filter_state(global_filter_data) + month_range = filter_state["month_range"] + hour_range = filter_state["hour_range"] + invert_month_global = filter_state["invert_month"] + invert_hour_global = filter_state["invert_hour"] + + start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( + month_range, hour_range, invert_month_global, invert_hour_global + ) + else: + # Use default values when global filter is not active + start_month, end_month, start_hour, end_hour = 1, 12, 0, 24 - df = filter_df_by_month_and_hour( - df, time_filter, month, hour, invert_month, invert_hour, df.columns - ) + # Use local filtering when global filter is not active + df = filter_df_by_month_and_hour( + df, True, [1, 12], [0, 24], [], [], df.columns + ) if data_filter: if min_val <= max_val: diff --git a/pages/select.py b/pages/select.py index 498cc67..d2387b9 100644 --- a/pages/select.py +++ b/pages/select.py @@ -125,6 +125,7 @@ def alert(): Output(ElementIds.ALERT, "visible"), Output(ElementIds.ALERT, "children"), Output(ElementIds.ALERT, "color"), + Output(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data", allow_duplicate=True), ], [ Input(ElementIds.MODAL_YES_BUTTON, "n_clicks"), @@ -157,6 +158,13 @@ def submitted_data( True, messages_alert["not_available"], "orange", + { + "month_range": [1, 12], + "hour_range": [0, 24], + "invert_month": [], + "invert_hour": [], + "filter_active": False + }, ) location_info = get_location_info( lines, url_store @@ -167,6 +175,13 @@ def submitted_data( True, messages_alert["success"], "green", + { + "month_range": [1, 12], + "hour_range": [0, 24], + "invert_month": [], + "invert_hour": [], + "filter_active": False + }, ) elif ( @@ -191,6 +206,13 @@ def submitted_data( True, messages_alert["success"], "green", + { + "month_range": [1, 12], + "hour_range": [0, 24], + "invert_month": [], + "invert_hour": [], + "filter_active": False + }, ) else: return ( @@ -199,6 +221,13 @@ def submitted_data( True, messages_alert["invalid_format"], "orange", + { + "month_range": [1, 12], + "hour_range": [0, 24], + "invert_month": [], + "invert_hour": [], + "filter_active": False + }, ) except (ValueError, IndexError, KeyError) as e: print(f"Error parsing EPW file: {e}") @@ -208,6 +237,13 @@ def submitted_data( True, messages_alert["wrong_extension"], "orange", + { + "month_range": [1, 12], + "hour_range": [0, 24], + "invert_month": [], + "invert_hour": [], + "filter_active": False + }, ) raise PreventUpdate @@ -252,7 +288,6 @@ def switch_si_ip(_, si_ip_input, url_store, lines): Output(ElementIds.NAV_EXPLORER, "disabled"), Output(ElementIds.NAV_OUTDOOR, "disabled"), Output(ElementIds.NAV_NATURAL_VENTILATION, "disabled"), - Output(ElementIds.NAV_CHANGELOG, "disabled"), Output(ElementIds.ID_SELECT_BANNER_SUBTITLE, "children"), ], [ @@ -274,7 +309,6 @@ def enable_tabs_when_data_is_loaded(meta, data): True, True, True, - True, # changelog always disabled default, ) else: @@ -288,7 +322,6 @@ def enable_tabs_when_data_is_loaded(meta, data): False, False, False, - True, # changelog always disabled "Current Location: " + meta[Variables.CITY.col_name] + ", " diff --git a/pages/summary.py b/pages/summary.py index e651273..bed9751 100644 --- a/pages/summary.py +++ b/pages/summary.py @@ -51,7 +51,7 @@ def update_layout(si_ip): cooling_setpoint = 64 return dmc.Stack( - id=ElementIds.TAB2_SCE1_CONTAINER, + id=ElementIds.SUMMARY_SCE1_CONTAINER, children=[ dcc.Loading( type="circle", @@ -273,6 +273,7 @@ def update_location_info(ts, df, meta, si_ip): [ Input(ElementIds.ID_SUMMARY_DF_STORE, "modified_timestamp"), Input(ElementIds.SUBMIT_SET_POINTS, "n_clicks"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_SUMMARY_DF_STORE, "data"), @@ -283,7 +284,7 @@ def update_location_info(ts, df, meta, si_ip): ], prevent_initial_call=False, ) -def degree_day_chart(ts, n_clicks, df, meta, hdd_value, cdd_value, si_ip): +def degree_day_chart(ts, n_clicks, global_filter_data, df, meta, hdd_value, cdd_value, si_ip): """Redraw HDD/CDD chart only when Submit is clicked.""" if df is None or meta is None: @@ -292,6 +293,11 @@ def degree_day_chart(ts, n_clicks, df, meta, hdd_value, cdd_value, si_ip): if isinstance(df, (list, tuple, dict)): df = pd.DataFrame(df) + # Apply global filter if active + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data, Variables.DBT.col_name) + hdd_setpoint = hdd_value cdd_setpoint = cdd_value warning_setpoint = cdd_setpoint < hdd_setpoint diff --git a/pages/sun.py b/pages/sun.py index 3999dac..539a910 100644 --- a/pages/sun.py +++ b/pages/sun.py @@ -125,16 +125,16 @@ def explore_daily_heatmap(): children=[ dmc.Title("Select variable: ", order=5), dropdown( - id=ElementIds.TAB_EXPLORE_DROPDOWN, + id=ElementIds.SUN_EXPLORE_DROPDOWN, options=sun_cloud_tab_explore_dropdown_names, value="glob_hor_rad", ), ], ), - dcc.Loading(type="circle", children=dmc.Stack(id=ElementIds.TAB4_DAILY)), + dcc.Loading(type="circle", children=dmc.Stack(id=ElementIds.SUN_DAILY)), dcc.Loading( type="circle", - children=dmc.Stack(id=ElementIds.TAB4_HEATMAP), + children=dmc.Stack(id=ElementIds.SUN_HEATMAP), ), ], ) @@ -187,6 +187,7 @@ def update_static_section(si_ip): ], [ Input(ElementIds.ID_SUN_DF_STORE, "modified_timestamp"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_SUN_DF_STORE, "data"), @@ -194,9 +195,17 @@ def update_static_section(si_ip): State(ElementIds.ID_SUN_SI_IP_UNIT_STORE, "data"), ], ) -def monthly_and_cloud_chart(_, df, meta, si_ip): +def monthly_and_cloud_chart(_, global_filter_data, df, meta, si_ip): """Update the contents of tab four. Passing in the polar selection and the general info (df, meta).""" + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data, + [Variables.GLOB_HOR_RAD.col_name, Variables.DIF_HOR_RAD.col_name, Variables.TOT_SKY_COVER.col_name]) + # Filter out the filtered rows for solar radiation calculations + if '_is_filtered' in df.columns: + df = df[~df['_is_filtered']] + # Sun Radiation monthly = monthly_solar(df, si_ip) monthly = monthly.update_layout(margin=tight_margins) @@ -210,9 +219,7 @@ def monthly_and_cloud_chart(_, df, meta, si_ip): title="", legend=dict(orientation="h", yanchor="bottom", y=1.05, xanchor="right", x=1), ) - cover.update_xaxes( - dict(tickmode="array", tickvals=np.arange(0, 12, 1), ticktext=month_lst) - ) + # Remove the hardcoded x-axis update - let barchart handle it dynamically units = generate_units(si_ip) return dcc.Graph( style={"width": "100%", "height": "520px"}, @@ -234,6 +241,7 @@ def monthly_and_cloud_chart(_, df, meta, si_ip): Input(ElementIds.CUSTOM_SUN_VIEW_DROPDOWN, "value"), Input(ElementIds.CUSTOM_SUN_VAR_DROPDOWN, "value"), Input(ElementIds.ID_SUN_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_SUN_DF_STORE, "data"), @@ -241,8 +249,27 @@ def monthly_and_cloud_chart(_, df, meta, si_ip): State(ElementIds.ID_SUN_SI_IP_UNIT_STORE, "data"), ], ) -def sun_path_chart(_, view, var, global_local, df, meta, si_ip): +def sun_path_chart(_, view, var, global_local, global_filter_data, df, meta, si_ip): """Update the contents of tab four. Passing in the polar selection and the general info (df, meta).""" + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + # For sun path chart, we need to filter all sun position related columns + target_cols = [ + Variables.GLOB_HOR_RAD.col_name, + Variables.DIF_HOR_RAD.col_name, + Variables.APPARENT_ELEVATION.col_name, + Variables.APPARENT_ZENITH.col_name, + Variables.AZIMUTH.col_name, + Variables.ELEVATION.col_name, + Variables.DAY.col_name, + Variables.MONTH_NAMES.col_name, + Variables.HOUR.col_name + ] + # Add the selected variable if it's not "None" + if var != "None": + target_cols.append(var) + df = apply_global_month_hour_filter(df, global_filter_data, target_cols) + custom_inputs = "" if var == "None" else f"{var}" units = "" if var == "None" else generate_units(si_ip) if view == "polar": @@ -264,11 +291,12 @@ def sun_path_chart(_, view, var, global_local, df, meta, si_ip): @callback( - Output(ElementIds.TAB4_DAILY, "children"), + Output(ElementIds.SUN_DAILY, "children"), [ Input(ElementIds.ID_SUN_DF_STORE, "modified_timestamp"), - Input(ElementIds.TAB_EXPLORE_DROPDOWN, "value"), + Input(ElementIds.SUN_EXPLORE_DROPDOWN, "value"), Input(ElementIds.ID_SUN_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_SUN_DF_STORE, "data"), @@ -276,8 +304,12 @@ def sun_path_chart(_, view, var, global_local, df, meta, si_ip): State(ElementIds.ID_SUN_SI_IP_UNIT_STORE, "data"), ], ) -def daily(_, var, global_local, df, meta, si_ip): +def daily(_, var, global_local, global_filter_data, df, meta, si_ip): """Update the contents of tab four section two. Passing in the general info (df, meta).""" + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data, var) + custom_inputs = generate_custom_inputs(var) units = generate_units(si_ip) return dcc.Graph( @@ -288,11 +320,12 @@ def daily(_, var, global_local, df, meta, si_ip): @callback( - Output(ElementIds.TAB4_HEATMAP, "children"), + Output(ElementIds.SUN_HEATMAP, "children"), [ Input(ElementIds.ID_SUN_DF_STORE, "modified_timestamp"), - Input(ElementIds.TAB_EXPLORE_DROPDOWN, "value"), + Input(ElementIds.SUN_EXPLORE_DROPDOWN, "value"), Input(ElementIds.ID_SUN_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_SUN_DF_STORE, "data"), @@ -300,7 +333,11 @@ def daily(_, var, global_local, df, meta, si_ip): State(ElementIds.ID_SUN_SI_IP_UNIT_STORE, "data"), ], ) -def update_heatmap(_, var, global_local, df, meta, si_ip): +def update_heatmap(_, var, global_local, global_filter_data, df, meta, si_ip): + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data, var) + custom_inputs = generate_custom_inputs(var) units = generate_units(si_ip) return dcc.Graph( diff --git a/pages/t_rh.py b/pages/t_rh.py index 932a9df..676facc 100644 --- a/pages/t_rh.py +++ b/pages/t_rh.py @@ -92,6 +92,7 @@ def layout(): Input(ElementIds.ID_T_RH_DF_STORE, "modified_timestamp"), Input(ElementIds.ID_T_RH_GLOBAL_LOCAL_RADIO_INPUT, "value"), Input(ElementIds.ID_T_RH_DROPDOWN, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_T_RH_DF_STORE, "data"), @@ -99,7 +100,14 @@ def layout(): State(ElementIds.ID_T_RH_SI_IP_UNIT_STORE, "data"), ], ) -def update_yearly_chart(_, global_local, dd_value, df, meta, si_ip): +def update_yearly_chart(_, global_local, dd_value, global_filter_data, df, meta, si_ip): + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + target_columns = [Variables.DBT.col_name, Variables.RH.col_name, Variables.ADAPTIVE_CMF_80_LOW.col_name, + Variables.ADAPTIVE_CMF_80_UP.col_name, Variables.ADAPTIVE_CMF_90_LOW.col_name, + Variables.ADAPTIVE_CMF_90_UP.col_name, Variables.ADAPTIVE_CMF_RMT.col_name] + df = apply_global_month_hour_filter(df, global_filter_data, target_columns) + if dd_value == dropdown_names[var_to_plot[0]]: dbt_yearly = yearly_profile(df, Variables.DBT.col_name, global_local, si_ip) dbt_yearly.update_layout(xaxis=dict(rangeslider=dict(visible=True))) @@ -126,6 +134,7 @@ def update_yearly_chart(_, global_local, dd_value, df, meta, si_ip): Input(ElementIds.ID_T_RH_DF_STORE, "modified_timestamp"), Input(ElementIds.ID_T_RH_GLOBAL_LOCAL_RADIO_INPUT, "value"), Input(ElementIds.ID_T_RH_DROPDOWN, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_T_RH_DF_STORE, "data"), @@ -133,7 +142,12 @@ def update_yearly_chart(_, global_local, dd_value, df, meta, si_ip): State(ElementIds.ID_T_RH_SI_IP_UNIT_STORE, "data"), ], ) -def update_daily(_, global_local, dd_value, df, meta, si_ip): +def update_daily(_, global_local, dd_value, global_filter_data, df, meta, si_ip): + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + target_columns = [Variables.DBT.col_name, Variables.RH.col_name] + df = apply_global_month_hour_filter(df, global_filter_data, target_columns) + if dd_value == dropdown_names[var_to_plot[0]]: units = generate_units_degree(si_ip) return dcc.Graph( @@ -184,6 +198,7 @@ def update_daily(_, global_local, dd_value, df, meta, si_ip): Input(ElementIds.ID_T_RH_DF_STORE, "modified_timestamp"), Input(ElementIds.ID_T_RH_GLOBAL_LOCAL_RADIO_INPUT, "value"), Input(ElementIds.ID_T_RH_DROPDOWN, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_T_RH_DF_STORE, "data"), @@ -191,43 +206,40 @@ def update_daily(_, global_local, dd_value, df, meta, si_ip): State(ElementIds.ID_T_RH_SI_IP_UNIT_STORE, "data"), ], ) -def update_heatmap(_, global_local, dd_value, df, meta, si_ip): +def update_heatmap(_, global_local, dd_value, global_filter_data, df, meta, si_ip): """Update heatmap content.""" + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + target_columns = [Variables.DBT.col_name, Variables.RH.col_name] + df = apply_global_month_hour_filter(df, global_filter_data, target_columns) + + base_columns = [Variables.HOUR.col_name, Variables.UTC_TIME.col_name, Variables.MONTH_NAMES.col_name, Variables.DAY.col_name] + if '_is_filtered' in df.columns: + base_columns.append('_is_filtered') + if dd_value == dropdown_names[var_to_plot[0]]: + if f'_{Variables.DBT.col_name}_original' in df.columns: + base_columns.append(f'_{Variables.DBT.col_name}_original') units = generate_units_degree(si_ip) return dcc.Graph( config=generate_chart_name( TabNames.DRY_BULB_TEMPERATURE_HEATMAP, meta, units ), figure=heatmap( - df[ - [ - Variables.DBT.col_name, - Variables.HOUR.col_name, - Variables.UTC_TIME.col_name, - Variables.MONTH_NAMES.col_name, - Variables.DAY.col_name, - ] - ], + df[[Variables.DBT.col_name] + base_columns], Variables.DBT.col_name, global_local, si_ip, ), ) else: + if f'_{Variables.DBT.col_name}_original' in df.columns: + base_columns.append(f'_{Variables.DBT.col_name}_original') units = generate_units(si_ip) return dcc.Graph( config=generate_chart_name(TabNames.RELATIVE_HUMIDITY_HEATMAP, meta, units), figure=heatmap( - df[ - [ - Variables.RH.col_name, - Variables.HOUR.col_name, - Variables.UTC_TIME.col_name, - Variables.MONTH_NAMES.col_name, - Variables.DAY.col_name, - ] - ], + df[[Variables.DBT.col_name] + base_columns], Variables.RH.col_name, global_local, si_ip, @@ -240,14 +252,23 @@ def update_heatmap(_, global_local, dd_value, df, meta, si_ip): [ Input(ElementIds.ID_T_RH_DF_STORE, "modified_timestamp"), Input(ElementIds.ID_T_RH_DROPDOWN, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_T_RH_DF_STORE, "data"), State(ElementIds.ID_T_RH_SI_IP_UNIT_STORE, "data"), ], ) -def update_table(_, dd_value, df, si_ip): +def update_table(_, dd_value, global_filter_data, df, si_ip): """Update the contents of descriptive statistics table.""" + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + target_columns = [Variables.DBT.col_name, Variables.RH.col_name] + df = apply_global_month_hour_filter(df, global_filter_data, target_columns) + # Filter out the filtered rows to avoid empty columns + if '_is_filtered' in df.columns: + df = df[~df['_is_filtered']] + return summary_table_tmp_rh_tab( df[ [ diff --git a/pages/wind.py b/pages/wind.py index e9d0d9f..937509c 100644 --- a/pages/wind.py +++ b/pages/wind.py @@ -144,7 +144,7 @@ def seasonal_wind_rose(): def daily_wind_rose(): """Return the section for the 3 daily wind rose graphs.""" return dmc.Stack( - id=ElementIds.TAB5_DAILY_CONTAINER, + id=ElementIds.WIND_DAILY_CONTAINER, children=[ title_with_link( text="Daily Wind Rose", @@ -201,102 +201,6 @@ def daily_wind_rose(): ) -def custom_wind_rose(): - return dmc.Stack( - children=[ - title_with_tooltip( - text="Customizable Wind Rose", - tooltip_text=None, - id_button=IdButtons.CUSTOM_ROSE_CHART, - ), - dmc.Grid( - gutter="md", - maw=900, - mx="auto", - children=[ - dmc.GridCol( - span=6, - children=dmc.Stack( - children=[ - dmc.Group( - children=[ - dmc.Title( - "Start Month:", - order=5, - w="8rem", - ta="right", - ), - dropdown( - id=ElementIds.TAB5_CUSTOM_START_MONTH, - options={ - j: i + 1 - for i, j in enumerate(month_lst) - }, - value=1, - ), - ], - ), - dmc.Group( - children=[ - dmc.Title( - "Start Hour:", order=5, w="8rem", ta="right" - ), - dropdown( - id=ElementIds.TAB5_CUSTOM_START_HOUR, - options={ - str(i) + ":00": i for i in range(0, 24) - }, - value=0, - ), - ], - ), - ], - ), - ), - dmc.GridCol( - span=6, - children=dmc.Stack( - children=[ - dmc.Group( - children=[ - dmc.Title( - "End Month:", order=5, w="8rem", ta="right" - ), - dropdown( - id=ElementIds.TAB5_CUSTOM_END_MONTH, - options={ - j: i + 1 - for i, j in enumerate(month_lst) - }, - value=12, - ), - ], - ), - dmc.Group( - children=[ - dmc.Title( - "End Hour:", order=5, w="8rem", ta="right" - ), - dropdown( - id=ElementIds.TAB5_CUSTOM_END_HOUR, - options={ - str(i) + ":00": i for i in range(1, 25) - }, - value=24, - ), - ], - ), - ], - ), - ), - ], - ), - dcc.Loading( - type="circle", - children=dmc.Stack(id=ElementIds.CUSTOM_WIND_ROSE, maw=900, mx="auto"), - ), - ], - ) def layout(): @@ -323,22 +227,36 @@ def layout(): ), seasonal_wind_rose(), daily_wind_rose(), - custom_wind_rose(), ], ) @callback( Output(ElementIds.WIND_ROSE, "children"), - Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), + [ + Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), + ], [ State(ElementIds.ID_WIND_DF_STORE, "data"), State(ElementIds.ID_WIND_META_STORE, "data"), State(ElementIds.ID_WIND_SI_IP_UNIT_STORE, "data"), ], ) -def update_annual_wind_rose(_, df, meta, si_ip): - annual = wind_rose(df, "", [1, 12], [1, 24], True, si_ip) +def update_annual_wind_rose(_, global_filter_data, df, meta, si_ip): + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + df = apply_global_month_hour_filter(df, global_filter_data, [Variables.WIND_SPEED.col_name, Variables.WIND_DIR.col_name]) + + months = [1, 12] + hours = [1, 24] + else: + months = [1, 12] + hours = [1, 24] + + skip_filter = global_filter_data and global_filter_data.get("filter_active", False) + annual = wind_rose(df, "", months, hours, True, si_ip, skip_time_filter=skip_filter) + units = generate_units(si_ip) return dcc.Graph( config=generate_chart_name(TabNames.ANNUAL_WIND_ROSE, meta, units), @@ -351,6 +269,7 @@ def update_annual_wind_rose(_, df, meta, si_ip): [ Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), Input(ElementIds.ID_WIND_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_WIND_DF_STORE, "data"), @@ -358,7 +277,11 @@ def update_annual_wind_rose(_, df, meta, si_ip): State(ElementIds.ID_WIND_SI_IP_UNIT_STORE, "data"), ], ) -def update_tab_wind_speed(_, global_local, df, meta, si_ip): +def update_tab_wind_speed(_, global_local, global_filter_data, df, meta, si_ip): + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data, Variables.WIND_SPEED.col_name) + speed = heatmap(df, Variables.WIND_SPEED.col_name, global_local, si_ip) units = generate_units(si_ip) return dcc.Graph( @@ -369,14 +292,22 @@ def update_tab_wind_speed(_, global_local, df, meta, si_ip): @callback( Output(ElementIds.WIND_DIRECTION, "children"), - [Input(ElementIds.ID_WIND_GLOBAL_LOCAL_RADIO_INPUT, "value")], + [ + Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), + Input(ElementIds.ID_WIND_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), + ], [ State(ElementIds.ID_WIND_DF_STORE, "data"), State(ElementIds.ID_WIND_META_STORE, "data"), State(ElementIds.ID_WIND_SI_IP_UNIT_STORE, "data"), ], ) -def update_tab_wind_direction(global_local, df, meta, si_ip): +def update_tab_wind_direction(_, global_local, global_filter_data, df, meta, si_ip): + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data, Variables.WIND_DIR.col_name) + direction = heatmap(df, Variables.WIND_DIR.col_name, global_local, si_ip) units = generate_units(si_ip) return dcc.Graph( @@ -385,63 +316,6 @@ def update_tab_wind_direction(global_local, df, meta, si_ip): ) -@callback( - Output(ElementIds.CUSTOM_WIND_ROSE, "children"), - [ - Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), - Input(ElementIds.TAB5_CUSTOM_START_MONTH, "value"), - Input(ElementIds.TAB5_CUSTOM_START_HOUR, "value"), - Input(ElementIds.TAB5_CUSTOM_END_MONTH, "value"), - Input(ElementIds.TAB5_CUSTOM_END_HOUR, "value"), - ], - [ - State(ElementIds.ID_WIND_DF_STORE, "data"), - State(ElementIds.ID_WIND_META_STORE, "data"), - State(ElementIds.ID_WIND_SI_IP_UNIT_STORE, "data"), - ], -) -def update_custom_wind_rose( - _, start_month, start_hour, end_month, end_hour, df, meta, si_ip -): - start_hour = int(start_hour) - end_hour = int(end_hour) - start_month = int(start_month) - end_month = int(end_month) - - if start_month <= end_month: - df = df.loc[ - (df[Variables.MONTH.col_name] >= start_month) - & (df[Variables.MONTH.col_name] <= end_month) - ] - else: - df = df.loc[ - (df[Variables.MONTH.col_name] <= end_month) - | (df[Variables.MONTH.col_name] >= start_month) - ] - if start_hour <= end_hour: - df = df.loc[ - (df[Variables.HOUR.col_name] >= start_hour) - & (df[Variables.HOUR.col_name] <= end_hour) - ] - else: - df = df.loc[ - (df[Variables.HOUR.col_name] <= end_hour) - | (df[Variables.HOUR.col_name] >= start_hour) - ] - - custom = wind_rose( - df, "", [start_month, end_month], [start_hour, end_hour], True, si_ip - ) - custom_inputs = generate_custom_inputs_time( - start_month, end_month, start_hour, end_hour - ) - units = generate_units(si_ip) - return dcc.Graph( - config=generate_chart_name( - TabNames.CUSTOM_WIND_ROSE, meta, custom_inputs, units - ), - figure=custom, - ) @callback( @@ -571,15 +445,25 @@ def seasonal_chart_caption(month_start, month_end, count, n_calm): Output(ElementIds.NOON_WIND_ROSE_TEXT, "children"), Output(ElementIds.NIGHT_WIND_ROSE_TEXT, "children"), ], - Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), + [ + Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), + ], [ State(ElementIds.ID_WIND_DF_STORE, "data"), State(ElementIds.ID_WIND_META_STORE, "data"), State(ElementIds.ID_WIND_SI_IP_UNIT_STORE, "data"), ], ) -def update_daily_graphs(_, df, meta, si_ip): - months = [1, 12] +def update_daily_graphs(_, global_filter_data, df, meta, si_ip): + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + df = apply_global_month_hour_filter(df, global_filter_data, [Variables.WIND_SPEED.col_name, Variables.WIND_DIR.col_name]) + + months = [1, 12] + else: + months = [1, 12] + morning_times = [6, 13] noon_times = [14, 21] night_times = [22, 5] diff --git a/tests/node/cypress/e2e/spec.cy.js b/tests/node/cypress/e2e/spec.cy.js index 3de4a4a..f0ea1ae 100644 --- a/tests/node/cypress/e2e/spec.cy.js +++ b/tests/node/cypress/e2e/spec.cy.js @@ -84,8 +84,6 @@ describe('Clima', () => { // TODO cy.contains('Daily Wind Rose'); // TODO - cy.contains('Customizable Wind Rose'); - // TODO // Psychrometric Chart click_tab('Psychrometric Chart'); @@ -102,7 +100,7 @@ describe('Clima', () => { // Outdoor Comfort click_tab('Outdoor Comfort'); // TODO - cy.contains('The Best Weather Condition is: utci_noSun_noWind_categories'); + cy.contains('The Best Weather Condition is: UTCI No Sun No Wind Categories'); // TODO cy.contains('UTCI thermal stress chart'); cy.contains('no thermal stress'); From bde8f30b2c2648d44525b9c2d3bdab1f38ffd4b5 Mon Sep 17 00:00:00 2001 From: yluo0664 Date: Mon, 27 Oct 2025 21:15:18 +1100 Subject: [PATCH 02/15] fix: updated the documentation of Contributor, Code Style ,and Pull Request Regulation --- docs/README.md | 6 ++++++ docs/contributing/contributing.md | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/docs/README.md b/docs/README.md index 05ce6e2..b194b92 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,6 +34,12 @@ This ongoing project results from the collaboration and contributions of the peo * [Chun Him Lee](https://www.linkedin.com/in/chun-him-lee-01b553129/): Coding and review * [Tu Minh Phuong Doan](https://www.linkedin.com/in/harry-doan-legopher/): Coding and review * [Yixun Quan](https://www.linkedin.com/in/yixun-quan-929a661a3): Coding and review +* [Yuqing Luo](https://github.com/LeoLuosifen): Coding, code maintenance and review +* Wenshu lyu: Coding, code maintenance and review +* Ziqi Liu: Coding, code maintenance and review +* Tianchi Liu: Coding, code maintenance and review +* Qian Liu: Coding, code maintenance and review +* Feng Wang: Coding, code maintenance and review ## Acknowledgment diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md index 103d52f..3bc8921 100644 --- a/docs/contributing/contributing.md +++ b/docs/contributing/contributing.md @@ -77,6 +77,7 @@ Available [here](code_of_conduct.md) ## Code style +### Code Formatting We use ruff to enforce the code style and code formatting. You can run it with: ```bash @@ -108,6 +109,19 @@ Format your code before committing: black . ``` +### Code Simplicity +Strive to minimize redundancy in your code. +- Keep logic as concise as possible. +- Remove unused variables, imports, and components. +- Prefer reusable utilities to repeated patterns. + +### UI Modifications +For UI-related changes: +* Avoid unnecessary custom CSS. +* [Use DMC (Design Master Components)](https://www.dash-mantine-components.com/) wherever applicable for layout and styling consistency. +* Ensure visual consistency with existing components. + + ## Testing Before submitting a Pull Request, please make sure: @@ -172,6 +186,14 @@ Classification of Common Commit Types: - **Testing:** Describe how you tested your changes and how we can reproduce them. Include test details if necessary. +**Pull Request Review:** + +- After submitting a Pull Request (PR), please @Coderabbit for review. +- Check all improvement suggestions provided by Coderabbit before requesting a final review. + +**Discussion of Solutions:** + When needed, seek feedback from collaborators Toby and Giobetti before making major design or logic decisions. + ## Thanks From 18a0361d8d213620ad052afec25465ce5ae3033a Mon Sep 17 00:00:00 2001 From: yluo0664 Date: Mon, 27 Oct 2025 21:15:59 +1100 Subject: [PATCH 03/15] =?UTF-8?q?fix:=20deleted=20unnecessary=20style=20in?= =?UTF-8?q?=20css=20files=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/banner.css | 8 -------- assets/fonts.css | 5 ----- assets/layout.css | 31 ------------------------------- 3 files changed, 44 deletions(-) delete mode 100644 assets/fonts.css delete mode 100644 assets/layout.css diff --git a/assets/banner.css b/assets/banner.css index edba1bc..ef73f96 100644 --- a/assets/banner.css +++ b/assets/banner.css @@ -1,12 +1,4 @@ /**Banner**/ -#banner { - padding: 1rem; - background-color: #003262; - color: white; - /* position: sticky; */ - z-index: 1; -} - #banner-title { font-family: 'Open Sans', sans-serif; font-weight: 500; diff --git a/assets/fonts.css b/assets/fonts.css deleted file mode 100644 index 61d71fb..0000000 --- a/assets/fonts.css +++ /dev/null @@ -1,5 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap'); - -@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400&display=swap'); - -@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;0,800;1,300;1,400;1,600;1,700;1,800&display=swap'); \ No newline at end of file diff --git a/assets/layout.css b/assets/layout.css deleted file mode 100644 index 42ed6cb..0000000 --- a/assets/layout.css +++ /dev/null @@ -1,31 +0,0 @@ -.container-col { - display: flex; - flex-direction: column; -} - -.container-row { - display: flex; - flex-direction: row; -} - -.justify-center { - justify-content: center; -} - -.align-center{ - align-items: center; -} - -.container-stretch { - align-items: stretch; -} - -.full-width { - width: 100%; -} - -.doc-modal-body img { - box-sizing: border-box; - width: 100%; //or any percentage width you want -} - From a4742e899d0d55f4fe457f284648f4609c8a1870 Mon Sep 17 00:00:00 2001 From: yluo0664 Date: Mon, 27 Oct 2025 21:32:57 +1100 Subject: [PATCH 04/15] fix: formatted the code. --- pages/explorer.py | 30 +++- pages/lib/charts_data_explorer.py | 132 ++++++++------- pages/lib/global_element_ids.py | 2 +- pages/lib/layout.py | 65 ++++--- pages/lib/template_graphs.py | 270 +++++++++++++++++------------- pages/natural_ventilation.py | 21 ++- pages/outdoor.py | 26 ++- pages/psy-chart.py | 10 +- pages/select.py | 10 +- pages/summary.py | 9 +- pages/sun.py | 21 ++- pages/t_rh.py | 39 +++-- pages/wind.py | 38 +++-- 13 files changed, 409 insertions(+), 264 deletions(-) diff --git a/pages/explorer.py b/pages/explorer.py index 6225175..fb39899 100644 --- a/pages/explorer.py +++ b/pages/explorer.py @@ -390,9 +390,15 @@ def update_tab_yearly(_, var, global_local, global_filter_data, df, meta, si_ip) if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter - target_columns = [var, Variables.ADAPTIVE_CMF_80_LOW.col_name, Variables.ADAPTIVE_CMF_80_UP.col_name, - Variables.ADAPTIVE_CMF_90_LOW.col_name, Variables.ADAPTIVE_CMF_90_UP.col_name, - Variables.ADAPTIVE_CMF_RMT.col_name] + + target_columns = [ + var, + Variables.ADAPTIVE_CMF_80_LOW.col_name, + Variables.ADAPTIVE_CMF_80_UP.col_name, + Variables.ADAPTIVE_CMF_90_LOW.col_name, + Variables.ADAPTIVE_CMF_90_UP.col_name, + Variables.ADAPTIVE_CMF_RMT.col_name, + ] df = apply_global_month_hour_filter(df, global_filter_data, target_columns) if df[var].mean() == 99990.0: @@ -431,6 +437,7 @@ def update_tab_daily(_, var, global_local, global_filter_data, df, meta, si_ip): """Update the contents of tab size. Passing in the info from the dropdown and the general info.""" if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data) custom_inputs = generate_custom_inputs(var) @@ -464,6 +471,7 @@ def update_tab_heatmap(_, var, global_local, global_filter_data, df, meta, si_ip """Update the contents of tab size. Passing in the info from the dropdown and the general info.""" if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data) custom_inputs = generate_custom_inputs(var) @@ -518,7 +526,11 @@ def update_heatmap( si_ip, ): if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + from pages.lib.layout import ( + apply_global_month_hour_filter, + get_global_filter_state, + ) + df = apply_global_month_hour_filter(df, global_filter_data, var) filter_state = get_global_filter_state(global_filter_data) @@ -641,12 +653,11 @@ def update_more_charts( if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data) else: # Use local filtering when global filter is not active - df = filter_df_by_month_and_hour( - df, True, [1, 12], [0, 24], [], [], df.columns - ) + df = filter_df_by_month_and_hour(df, True, [1, 12], [0, 24], [], [], df.columns) data_filter_info = [data_filter, data_filter_var, min_val, max_val] if data_filter and (min_val is None or max_val is None): @@ -715,10 +726,11 @@ def update_table( ): if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter + filtered_df = apply_global_month_hour_filter(df, global_filter_data) # Filter out the filtered rows to avoid empty columns - if '_is_filtered' in filtered_df.columns: - filtered_df = filtered_df[~filtered_df['_is_filtered']] + if "_is_filtered" in filtered_df.columns: + filtered_df = filtered_df[~filtered_df["_is_filtered"]] else: # Use default values when global filter is not active filtered_df = df diff --git a/pages/lib/charts_data_explorer.py b/pages/lib/charts_data_explorer.py index cfee251..c63738b 100644 --- a/pages/lib/charts_data_explorer.py +++ b/pages/lib/charts_data_explorer.py @@ -62,12 +62,12 @@ def custom_heatmap(df, global_local, var, time_filter_info, data_filter_info, si fig = go.Figure() - has_filter_marker = '_is_filtered' in df.columns + has_filter_marker = "_is_filtered" in df.columns - if has_filter_marker and df['_is_filtered'].any(): - filtered_mask = df['_is_filtered'] + if has_filter_marker and df["_is_filtered"].any(): + filtered_mask = df["_is_filtered"] if filtered_mask.any(): - original_col = f'_{var}_original' + original_col = f"_{var}_original" if original_col in df.columns: filtered_z = df[original_col].copy() else: @@ -75,40 +75,48 @@ def custom_heatmap(df, global_local, var, time_filter_info, data_filter_info, si filtered_z[~filtered_mask] = None - fig.add_trace(go.Heatmap( - y=df[Variables.HOUR.col_name], - x=df[Variables.DOY.col_name], - z=filtered_z, - colorscale=[[0, 'lightgray'], [1, 'gray']], - zmin=range_z[0], - zmax=range_z[1], - showscale=False, - connectgaps=False, - hoverongaps=False, - customdata=np.stack((df[Variables.MONTH.col_name], df[Variables.DAY.col_name]), axis=-1), - hovertemplate=( + fig.add_trace( + go.Heatmap( + y=df[Variables.HOUR.col_name], + x=df[Variables.DOY.col_name], + z=filtered_z, + colorscale=[[0, "lightgray"], [1, "gray"]], + zmin=range_z[0], + zmax=range_z[1], + showscale=False, + connectgaps=False, + hoverongaps=False, + customdata=np.stack( + (df[Variables.MONTH.col_name], df[Variables.DAY.col_name]), + axis=-1, + ), + hovertemplate=( "Filtered Data
" + "Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" - " %{y}:00
" - ), - name="filtered", - )) + " %{y}:00
" + ), + name="filtered", + ) + ) normal_mask = ~filtered_mask normal_z = df[var].copy() normal_z[filtered_mask] = None - fig.add_trace(go.Heatmap( - y=df[Variables.HOUR.col_name], - x=df[Variables.DOY.col_name], - z=normal_z, - colorscale=var_color, - zmin=range_z[0], - zmax=range_z[1], - connectgaps=False, - hoverongaps=False, - customdata=np.stack((df[Variables.MONTH.col_name], df[Variables.DAY.col_name]), axis=-1), - hovertemplate=( + fig.add_trace( + go.Heatmap( + y=df[Variables.HOUR.col_name], + x=df[Variables.DOY.col_name], + z=normal_z, + colorscale=var_color, + zmin=range_z[0], + zmax=range_z[1], + connectgaps=False, + hoverongaps=False, + customdata=np.stack( + (df[Variables.MONTH.col_name], df[Variables.DAY.col_name]), axis=-1 + ), + hovertemplate=( "" + var + ": %{z:.2f} " @@ -117,38 +125,40 @@ def custom_heatmap(df, global_local, var, time_filter_info, data_filter_info, si + "Month: %{customdata[0]}
" + "Day: %{customdata[1]}
" + "Hour: %{y}:00
" - ), - name="", - colorbar=dict(title=var_unit), - )) + ), + name="", + colorbar=dict(title=var_unit), + ) + ) else: - fig.add_trace(go.Heatmap( - y=df[Variables.HOUR.col_name], - x=df[Variables.DOY.col_name], - z=df[var], - colorscale=var_color, - zmin=range_z[0], - zmax=range_z[1], - connectgaps=False, - hoverongaps=False, - customdata=np.stack( - (df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), - axis=-1, - ), - hovertemplate=( - "" - + var - + ": %{z:.2f} " - + var_unit - + "
" - + "Month: %{customdata[0]}
" - + "Day: %{customdata[1]}
" - + "Hour: %{y}:00
" - ), - name="", - colorbar=dict(title=var_unit), + fig.add_trace( + go.Heatmap( + y=df[Variables.HOUR.col_name], + x=df[Variables.DOY.col_name], + z=df[var], + colorscale=var_color, + zmin=range_z[0], + zmax=range_z[1], + connectgaps=False, + hoverongaps=False, + customdata=np.stack( + (df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), + axis=-1, + ), + hovertemplate=( + "" + + var + + ": %{z:.2f} " + + var_unit + + "
" + + "Month: %{customdata[0]}
" + + "Day: %{customdata[1]}
" + + "Hour: %{y}:00
" + ), + name="", + colorbar=dict(title=var_unit), + ) ) - ) fig.update_layout( template=template, title=title, diff --git a/pages/lib/global_element_ids.py b/pages/lib/global_element_ids.py index c88b3df..67af4da 100644 --- a/pages/lib/global_element_ids.py +++ b/pages/lib/global_element_ids.py @@ -241,4 +241,4 @@ class ElementIds(str, Enum): TOOLS_INVERT_HOUR = "tools-invert-hour" TOOLS_FILTER_SECTION = "tools-filter-section" TOOLS_MONTH_HOUR_SECTION = "tools-month-hour-section" - TOOLS_GLOBAL_FILTER_STORE = "tools-global-filter-store" \ No newline at end of file + TOOLS_GLOBAL_FILTER_STORE = "tools-global-filter-store" diff --git a/pages/lib/layout.py b/pages/lib/layout.py index 7cfc014..bb116dc 100644 --- a/pages/lib/layout.py +++ b/pages/lib/layout.py @@ -38,15 +38,14 @@ def get_icon(cls, page_name): """Get icon for a page name.""" return cls._ICON_MAP.get(page_name, "tabler:circle") + # global filters def create_tools_filter_components(): # Apply month and hour filter apply_month_hour_section = dmc.Stack( id=ElementIds.TOOLS_MONTH_HOUR_SECTION, children=[ - dmc.Divider( - label="Filter function", size="xs", color="blue", mb="xs" - ), + dmc.Divider(label="Filter function", size="xs", color="blue", mb="xs"), dmc.Button( "Apply month and hour filter", id=ElementIds.TOOLS_APPLY_MONTH_HOUR_FILTER, @@ -133,7 +132,11 @@ def create_tools_filter_components(): ], gap="xs", p="xs", - style={"backgroundColor": "#f8f9fa", "borderRadius": "6px", "border": "1px solid #e9ecef"}, + style={ + "backgroundColor": "#f8f9fa", + "borderRadius": "6px", + "border": "1px solid #e9ecef", + }, ) return dmc.Stack( @@ -143,6 +146,7 @@ def create_tools_filter_components(): gap="sm", ) + def create_navbar(): nav_link_styles = { "root": { @@ -431,9 +435,9 @@ def create_stores(): "hour_range": [0, 24], "invert_month": [], "invert_hour": [], - "filter_active": False + "filter_active": False, }, - storage_type="session" + storage_type="session", ), dcc.Interval( id=ElementIds.ID_LAYOUT_INTERVAL_COMPONENT, @@ -561,7 +565,7 @@ def show_alert_after_delay(n_intervals): prevent_initial_call=True, ) def update_global_filter_state( - apply_clicks, month_range, hour_range, invert_month, invert_hour, current_data + apply_clicks, month_range, hour_range, invert_month, invert_hour, current_data ): if apply_clicks is None: apply_clicks = 0 @@ -569,12 +573,14 @@ def update_global_filter_state( if apply_clicks > 0: current_data["filter_active"] = True - current_data.update({ - "month_range": month_range or [1, 12], - "hour_range": hour_range or [0, 24], - "invert_month": ["invert"] if invert_month else [], - "invert_hour": ["invert"] if invert_hour else [], - }) + current_data.update( + { + "month_range": month_range or [1, 12], + "hour_range": hour_range or [0, 24], + "invert_month": ["invert"] if invert_month else [], + "invert_hour": ["invert"] if invert_hour else [], + } + ) return current_data @@ -603,7 +609,7 @@ def apply_global_month_hour_filter(df, filter_store_data, target_columns=None): if not filter_state["filter_active"]: df_copy = df.copy() - df_copy['_is_filtered'] = False + df_copy["_is_filtered"] = False return df_copy month_range = filter_state["month_range"] @@ -624,24 +630,37 @@ def apply_global_month_hour_filter(df, filter_store_data, target_columns=None): month_mask = None if start_month <= end_month: - month_mask = (df_copy[Variables.MONTH.col_name] < start_month) | (df_copy[Variables.MONTH.col_name] > end_month) + month_mask = (df_copy[Variables.MONTH.col_name] < start_month) | ( + df_copy[Variables.MONTH.col_name] > end_month + ) else: - month_mask = (df_copy[Variables.MONTH.col_name] >= end_month) & (df_copy[Variables.MONTH.col_name] <= start_month) + month_mask = (df_copy[Variables.MONTH.col_name] >= end_month) & ( + df_copy[Variables.MONTH.col_name] <= start_month + ) hour_mask = None if start_hour <= end_hour: - hour_mask = (df_copy[Variables.HOUR.col_name] < start_hour) | (df_copy[Variables.HOUR.col_name] > end_hour) + hour_mask = (df_copy[Variables.HOUR.col_name] < start_hour) | ( + df_copy[Variables.HOUR.col_name] > end_hour + ) else: - hour_mask = (df_copy[Variables.HOUR.col_name] >= end_hour) & (df_copy[Variables.HOUR.col_name] <= start_hour) + hour_mask = (df_copy[Variables.HOUR.col_name] >= end_hour) & ( + df_copy[Variables.HOUR.col_name] <= start_hour + ) - df_copy['_is_filtered'] = month_mask | hour_mask + df_copy["_is_filtered"] = month_mask | hour_mask for target_col in target_columns: - df_copy[f'_{target_col}_original'] = df_copy[target_col] + df_copy[f"_{target_col}_original"] = df_copy[target_col] from pages.lib.template_graphs import time_filtering - time_filtering(df_copy, start_month, end_month, Variables.MONTH.col_name, target_col) - time_filtering(df_copy, start_hour, end_hour, Variables.MONTH.col_name, target_col) + + time_filtering( + df_copy, start_month, end_month, Variables.MONTH.col_name, target_col + ) + time_filtering( + df_copy, start_hour, end_hour, Variables.MONTH.col_name, target_col + ) return df_copy @@ -668,5 +687,3 @@ def sync_sliders_with_global_state(global_filter_data): bool(global_filter_data.get("invert_month", [])), bool(global_filter_data.get("invert_hour", [])), ) - - diff --git a/pages/lib/template_graphs.py b/pages/lib/template_graphs.py index 633fc3a..5db67b8 100644 --- a/pages/lib/template_graphs.py +++ b/pages/lib/template_graphs.py @@ -378,17 +378,17 @@ def heatmap_with_filter( var_range = variable.get_range(si_ip) var_color = variable.get_color() - has_global_filter_marker = '_is_filtered' in df.columns + has_global_filter_marker = "_is_filtered" in df.columns global_filter_mask = None if has_global_filter_marker: - global_filter_mask = df['_is_filtered'].copy() + global_filter_mask = df["_is_filtered"].copy() df = filter_df_by_month_and_hour( df, time_filter, month, hour, invert_month, invert_hour, var ) if has_global_filter_marker and global_filter_mask is not None: - df['_is_filtered'] = global_filter_mask + df["_is_filtered"] = global_filter_mask start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter( month, hour, invert_month, invert_hour @@ -414,12 +414,12 @@ def heatmap_with_filter( range_z = [data_min, data_max] fig = go.Figure() - has_filter_marker = '_is_filtered' in df.columns + has_filter_marker = "_is_filtered" in df.columns - if has_filter_marker and df['_is_filtered'].any(): - filtered_mask = df['_is_filtered'] + if has_filter_marker and df["_is_filtered"].any(): + filtered_mask = df["_is_filtered"] if filtered_mask.any(): - original_col = f'_{var}_original' + original_col = f"_{var}_original" if original_col in df.columns: filtered_z = df[original_col].copy() else: @@ -427,71 +427,89 @@ def heatmap_with_filter( filtered_z[~filtered_mask] = None - fig.add_trace(go.Heatmap( - y=df[Variables.HOUR.col_name] - 0.5, - x=df[Variables.UTC_TIME.col_name].dt.date, - z=filtered_z, - colorscale=[[0, 'lightgray'], [1, 'gray']], - zmin=range_z[0], - zmax=range_z[1], - showscale=False, - customdata=np.stack((df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), axis=-1), - hovertemplate=( + fig.add_trace( + go.Heatmap( + y=df[Variables.HOUR.col_name] - 0.5, + x=df[Variables.UTC_TIME.col_name].dt.date, + z=filtered_z, + colorscale=[[0, "lightgray"], [1, "gray"]], + zmin=range_z[0], + zmax=range_z[1], + showscale=False, + customdata=np.stack( + ( + df[Variables.MONTH_NAMES.col_name], + df[Variables.DAY.col_name], + ), + axis=-1, + ), + hovertemplate=( "Filtered Data
" + "Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" - " %{y}:00
" - ), - name="filtered", - )) + " %{y}:00
" + ), + name="filtered", + ) + ) normal_mask = ~filtered_mask normal_z = df[var].copy() normal_z[filtered_mask] = None - fig.add_trace(go.Heatmap( - y=df[Variables.HOUR.col_name] - 0.5, - x=df[Variables.UTC_TIME.col_name].dt.date, - z=normal_z, - colorscale=var_color, - zmin=range_z[0], - zmax=range_z[1], - customdata=np.stack((df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), axis=-1), - hovertemplate=( + fig.add_trace( + go.Heatmap( + y=df[Variables.HOUR.col_name] - 0.5, + x=df[Variables.UTC_TIME.col_name].dt.date, + z=normal_z, + colorscale=var_color, + zmin=range_z[0], + zmax=range_z[1], + customdata=np.stack( + (df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), + axis=-1, + ), + hovertemplate=( "" + var + ": %{z:.2f} " + var_unit + "
Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" - " %{y}:00
" - ), - name="", - colorbar=dict(title="") if "_categories" in var else dict(title=var_unit), - )) + " %{y}:00
" + ), + name="", + colorbar=( + dict(title="") if "_categories" in var else dict(title=var_unit) + ), + ) + ) else: - fig.add_trace(go.Heatmap( - y=df[Variables.HOUR.col_name] - - 0.5, # Offset by 0.5 to center the hour labels - x=df[Variables.UTC_TIME.col_name].dt.date, - z=df[var], - colorscale=var_color, - zmin=range_z[0], - zmax=range_z[1], - customdata=np.stack( - (df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), - axis=-1, - ), - hovertemplate=( - "" - + var - + ": %{z:.2f} " - + var_unit - + "
Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" - " %{y}:00
" - ), - name="", - colorbar=dict(title="") if "_categories" in var else dict(title=var_unit), + fig.add_trace( + go.Heatmap( + y=df[Variables.HOUR.col_name] + - 0.5, # Offset by 0.5 to center the hour labels + x=df[Variables.UTC_TIME.col_name].dt.date, + z=df[var], + colorscale=var_color, + zmin=range_z[0], + zmax=range_z[1], + customdata=np.stack( + (df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), + axis=-1, + ), + hovertemplate=( + "" + + var + + ": %{z:.2f} " + + var_unit + + "
Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" + " %{y}:00
" + ), + name="", + colorbar=( + dict(title="") if "_categories" in var else dict(title=var_unit) + ), + ) ) - ) if var == Variables.WIND_SPEED.col_name: spd_bins = list(WIND_ROSE_BINS) @@ -538,12 +556,12 @@ def heatmap(df, var, global_local, si_ip): range_z = [data_min, data_max] fig = go.Figure() - has_filter_marker = '_is_filtered' in df.columns + has_filter_marker = "_is_filtered" in df.columns - if has_filter_marker and df['_is_filtered'].any(): - filtered_mask = df['_is_filtered'] + if has_filter_marker and df["_is_filtered"].any(): + filtered_mask = df["_is_filtered"] if filtered_mask.any(): - original_col = f'_{var}_original' + original_col = f"_{var}_original" if original_col in df.columns: filtered_z = df[original_col].copy() else: @@ -551,70 +569,84 @@ def heatmap(df, var, global_local, si_ip): filtered_z[~filtered_mask] = None - fig.add_trace(go.Heatmap( - y=df[Variables.HOUR.col_name], - x=df[Variables.UTC_TIME.col_name].dt.date, - z=filtered_z, - colorscale=[[0, 'lightgray'], [1, 'gray']], - zmin=range_z[0], - zmax=range_z[1], - showscale=False, - customdata=np.stack((df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), axis=-1), - hovertemplate=( + fig.add_trace( + go.Heatmap( + y=df[Variables.HOUR.col_name], + x=df[Variables.UTC_TIME.col_name].dt.date, + z=filtered_z, + colorscale=[[0, "lightgray"], [1, "gray"]], + zmin=range_z[0], + zmax=range_z[1], + showscale=False, + customdata=np.stack( + ( + df[Variables.MONTH_NAMES.col_name], + df[Variables.DAY.col_name], + ), + axis=-1, + ), + hovertemplate=( "Filtered Data
" + "Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" - " %{y}:00
" - ), - name="filtered", - )) + " %{y}:00
" + ), + name="filtered", + ) + ) normal_mask = ~filtered_mask normal_z = df[var].copy() normal_z[filtered_mask] = None - fig.add_trace(go.Heatmap( - y=df[Variables.HOUR.col_name], - x=df[Variables.UTC_TIME.col_name].dt.date, - z=normal_z, - colorscale=var_color, - zmin=range_z[0], - zmax=range_z[1], - customdata=np.stack((df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), axis=-1), - hovertemplate=( + fig.add_trace( + go.Heatmap( + y=df[Variables.HOUR.col_name], + x=df[Variables.UTC_TIME.col_name].dt.date, + z=normal_z, + colorscale=var_color, + zmin=range_z[0], + zmax=range_z[1], + customdata=np.stack( + (df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), + axis=-1, + ), + hovertemplate=( "" + var + ": %{z:.2f} " + var_unit + "
Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" - " %{y}:00
" - ), - name="", - colorbar=dict(title=var_unit), - )) + " %{y}:00
" + ), + name="", + colorbar=dict(title=var_unit), + ) + ) else: - fig.add_trace(go.Heatmap( - y=df[Variables.HOUR.col_name], - x=df[Variables.UTC_TIME.col_name].dt.date, - z=df[var], - colorscale=var_color, - zmin=range_z[0], - zmax=range_z[1], - customdata=np.stack( - (df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), - axis=-1, - ), - hovertemplate=( - "" - + var - + ": %{z:.2f} " - + var_unit - + "
Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" - " %{y}:00
" - ), - name="", - colorbar=dict(title=var_unit), + fig.add_trace( + go.Heatmap( + y=df[Variables.HOUR.col_name], + x=df[Variables.UTC_TIME.col_name].dt.date, + z=df[var], + colorscale=var_color, + zmin=range_z[0], + zmax=range_z[1], + customdata=np.stack( + (df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]), + axis=-1, + ), + hovertemplate=( + "" + + var + + ": %{z:.2f} " + + var_unit + + "
Month: %{customdata[0]}
Day: %{customdata[1]}
Hour:" + " %{y}:00
" + ), + name="", + colorbar=dict(title=var_unit), + ) ) - ) if var == Variables.WIND_SPEED.col_name: spd_bins = list(WIND_ROSE_BINS) @@ -907,7 +939,11 @@ def thermal_stress_stacked_barchart( available_months = sorted(new_df[Variables.MONTH.col_name].unique()) fig.update_xaxes( - dict(tickmode="array", tickvals=np.arange(0, len(available_months), 1), ticktext=month_lst), + dict( + tickmode="array", + tickvals=np.arange(0, len(available_months), 1), + ticktext=month_lst, + ), title_text="Day", showline=True, linewidth=1, @@ -958,9 +994,7 @@ def barchart(df, var, time_filter_info, data_filter_info, normalize, si_ip): for month_num in range(1, 13): if month_num in available_months_set: - query = ( - f"month=={str(month_num)} and ({filter_var}>={min_val} and {filter_var}<={max_val})" - ) + query = f"month=={str(month_num)} and ({filter_var}>={min_val} and {filter_var}<={max_val})" a = new_df.query(query)[Variables.DOY.col_name].count() month_in.append(a) query = f"month=={str(month_num)} and ({filter_var}<{min_val})" @@ -979,9 +1013,7 @@ def barchart(df, var, time_filter_info, data_filter_info, normalize, si_ip): month_names = month_lst - trace1 = go.Bar( - x=month_names, y=month_in, name="IN range", marker_color=color_in - ) + trace1 = go.Bar(x=month_names, y=month_in, name="IN range", marker_color=color_in) trace2 = go.Bar( x=month_names, y=month_below, diff --git a/pages/natural_ventilation.py b/pages/natural_ventilation.py index 1749964..e97181e 100644 --- a/pages/natural_ventilation.py +++ b/pages/natural_ventilation.py @@ -224,8 +224,14 @@ def nv_heatmap( dpt_data_filter = enable_dew_point_data_filter(condensation_enabled) if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state - df = apply_global_month_hour_filter(df, global_filter_data, Variables.DBT.col_name) + from pages.lib.layout import ( + apply_global_month_hour_filter, + get_global_filter_state, + ) + + df = apply_global_month_hour_filter( + df, global_filter_data, Variables.DBT.col_name + ) filter_state = get_global_filter_state(global_filter_data) month_range = filter_state["month_range"] @@ -264,7 +270,6 @@ def nv_heatmap( ), ) - variable = VariableInfo.from_col_name(var) filter = VariableInfo.from_col_name(filter_var) @@ -419,8 +424,14 @@ def nv_bar_chart( df[Variables.NV_ALLOWED.col_name] = 1 if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state - df = apply_global_month_hour_filter(df, global_filter_data, Variables.NV_ALLOWED.col_name) + from pages.lib.layout import ( + apply_global_month_hour_filter, + get_global_filter_state, + ) + + df = apply_global_month_hour_filter( + df, global_filter_data, Variables.NV_ALLOWED.col_name + ) filter_state = get_global_filter_state(global_filter_data) month_range = filter_state["month_range"] diff --git a/pages/outdoor.py b/pages/outdoor.py index 538041c..2b96697 100644 --- a/pages/outdoor.py +++ b/pages/outdoor.py @@ -218,7 +218,11 @@ def update_tab_utci_value( si_ip, ): if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + from pages.lib.layout import ( + apply_global_month_hour_filter, + get_global_filter_state, + ) + df = apply_global_month_hour_filter(df, global_filter_data, var) filter_state = get_global_filter_state(global_filter_data) @@ -301,8 +305,14 @@ def update_tab_utci_category( si_ip, ): if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state - df = apply_global_month_hour_filter(df, global_filter_data, [var, var + "_categories"]) + from pages.lib.layout import ( + apply_global_month_hour_filter, + get_global_filter_state, + ) + + df = apply_global_month_hour_filter( + df, global_filter_data, [var, var + "_categories"] + ) filter_state = get_global_filter_state(global_filter_data) month_range = filter_state["month_range"] @@ -380,11 +390,13 @@ def update_tab_utci_category( State(ElementIds.ID_OUTDOOR_SI_IP_UNIT_STORE, "data"), ], ) -def update_tab_utci_summary_chart( - var, normalize, global_filter_data, df, meta, si_ip -): +def update_tab_utci_summary_chart(var, normalize, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + from pages.lib.layout import ( + apply_global_month_hour_filter, + get_global_filter_state, + ) + df = apply_global_month_hour_filter(df, global_filter_data, var) filter_state = get_global_filter_state(global_filter_data) diff --git a/pages/psy-chart.py b/pages/psy-chart.py index 48a1ae4..fa5fdce 100644 --- a/pages/psy-chart.py +++ b/pages/psy-chart.py @@ -185,7 +185,11 @@ def update_psych_chart( si_ip, ): if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state + from pages.lib.layout import ( + apply_global_month_hour_filter, + get_global_filter_state, + ) + df = apply_global_month_hour_filter(df, global_filter_data) filter_state = get_global_filter_state(global_filter_data) @@ -202,9 +206,7 @@ def update_psych_chart( start_month, end_month, start_hour, end_hour = 1, 12, 0, 24 # Use local filtering when global filter is not active - df = filter_df_by_month_and_hour( - df, True, [1, 12], [0, 24], [], [], df.columns - ) + df = filter_df_by_month_and_hour(df, True, [1, 12], [0, 24], [], [], df.columns) if data_filter: if min_val <= max_val: diff --git a/pages/select.py b/pages/select.py index d2387b9..eda1477 100644 --- a/pages/select.py +++ b/pages/select.py @@ -163,7 +163,7 @@ def submitted_data( "hour_range": [0, 24], "invert_month": [], "invert_hour": [], - "filter_active": False + "filter_active": False, }, ) location_info = get_location_info( @@ -180,7 +180,7 @@ def submitted_data( "hour_range": [0, 24], "invert_month": [], "invert_hour": [], - "filter_active": False + "filter_active": False, }, ) @@ -211,7 +211,7 @@ def submitted_data( "hour_range": [0, 24], "invert_month": [], "invert_hour": [], - "filter_active": False + "filter_active": False, }, ) else: @@ -226,7 +226,7 @@ def submitted_data( "hour_range": [0, 24], "invert_month": [], "invert_hour": [], - "filter_active": False + "filter_active": False, }, ) except (ValueError, IndexError, KeyError) as e: @@ -242,7 +242,7 @@ def submitted_data( "hour_range": [0, 24], "invert_month": [], "invert_hour": [], - "filter_active": False + "filter_active": False, }, ) raise PreventUpdate diff --git a/pages/summary.py b/pages/summary.py index bed9751..3a26a28 100644 --- a/pages/summary.py +++ b/pages/summary.py @@ -284,7 +284,9 @@ def update_location_info(ts, df, meta, si_ip): ], prevent_initial_call=False, ) -def degree_day_chart(ts, n_clicks, global_filter_data, df, meta, hdd_value, cdd_value, si_ip): +def degree_day_chart( + ts, n_clicks, global_filter_data, df, meta, hdd_value, cdd_value, si_ip +): """Redraw HDD/CDD chart only when Submit is clicked.""" if df is None or meta is None: @@ -296,7 +298,10 @@ def degree_day_chart(ts, n_clicks, global_filter_data, df, meta, hdd_value, cdd_ # Apply global filter if active if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter - df = apply_global_month_hour_filter(df, global_filter_data, Variables.DBT.col_name) + + df = apply_global_month_hour_filter( + df, global_filter_data, Variables.DBT.col_name + ) hdd_setpoint = hdd_value cdd_setpoint = cdd_value diff --git a/pages/sun.py b/pages/sun.py index 539a910..b01130d 100644 --- a/pages/sun.py +++ b/pages/sun.py @@ -200,11 +200,19 @@ def monthly_and_cloud_chart(_, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter - df = apply_global_month_hour_filter(df, global_filter_data, - [Variables.GLOB_HOR_RAD.col_name, Variables.DIF_HOR_RAD.col_name, Variables.TOT_SKY_COVER.col_name]) + + df = apply_global_month_hour_filter( + df, + global_filter_data, + [ + Variables.GLOB_HOR_RAD.col_name, + Variables.DIF_HOR_RAD.col_name, + Variables.TOT_SKY_COVER.col_name, + ], + ) # Filter out the filtered rows for solar radiation calculations - if '_is_filtered' in df.columns: - df = df[~df['_is_filtered']] + if "_is_filtered" in df.columns: + df = df[~df["_is_filtered"]] # Sun Radiation monthly = monthly_solar(df, si_ip) @@ -253,6 +261,7 @@ def sun_path_chart(_, view, var, global_local, global_filter_data, df, meta, si_ """Update the contents of tab four. Passing in the polar selection and the general info (df, meta).""" if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter + # For sun path chart, we need to filter all sun position related columns target_cols = [ Variables.GLOB_HOR_RAD.col_name, @@ -263,7 +272,7 @@ def sun_path_chart(_, view, var, global_local, global_filter_data, df, meta, si_ Variables.ELEVATION.col_name, Variables.DAY.col_name, Variables.MONTH_NAMES.col_name, - Variables.HOUR.col_name + Variables.HOUR.col_name, ] # Add the selected variable if it's not "None" if var != "None": @@ -308,6 +317,7 @@ def daily(_, var, global_local, global_filter_data, df, meta, si_ip): """Update the contents of tab four section two. Passing in the general info (df, meta).""" if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data, var) custom_inputs = generate_custom_inputs(var) @@ -336,6 +346,7 @@ def daily(_, var, global_local, global_filter_data, df, meta, si_ip): def update_heatmap(_, var, global_local, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter + df = apply_global_month_hour_filter(df, global_filter_data, var) custom_inputs = generate_custom_inputs(var) diff --git a/pages/t_rh.py b/pages/t_rh.py index 676facc..74234ed 100644 --- a/pages/t_rh.py +++ b/pages/t_rh.py @@ -103,9 +103,16 @@ def layout(): def update_yearly_chart(_, global_local, dd_value, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter - target_columns = [Variables.DBT.col_name, Variables.RH.col_name, Variables.ADAPTIVE_CMF_80_LOW.col_name, - Variables.ADAPTIVE_CMF_80_UP.col_name, Variables.ADAPTIVE_CMF_90_LOW.col_name, - Variables.ADAPTIVE_CMF_90_UP.col_name, Variables.ADAPTIVE_CMF_RMT.col_name] + + target_columns = [ + Variables.DBT.col_name, + Variables.RH.col_name, + Variables.ADAPTIVE_CMF_80_LOW.col_name, + Variables.ADAPTIVE_CMF_80_UP.col_name, + Variables.ADAPTIVE_CMF_90_LOW.col_name, + Variables.ADAPTIVE_CMF_90_UP.col_name, + Variables.ADAPTIVE_CMF_RMT.col_name, + ] df = apply_global_month_hour_filter(df, global_filter_data, target_columns) if dd_value == dropdown_names[var_to_plot[0]]: @@ -145,6 +152,7 @@ def update_yearly_chart(_, global_local, dd_value, global_filter_data, df, meta, def update_daily(_, global_local, dd_value, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter + target_columns = [Variables.DBT.col_name, Variables.RH.col_name] df = apply_global_month_hour_filter(df, global_filter_data, target_columns) @@ -210,16 +218,22 @@ def update_heatmap(_, global_local, dd_value, global_filter_data, df, meta, si_i """Update heatmap content.""" if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter + target_columns = [Variables.DBT.col_name, Variables.RH.col_name] df = apply_global_month_hour_filter(df, global_filter_data, target_columns) - base_columns = [Variables.HOUR.col_name, Variables.UTC_TIME.col_name, Variables.MONTH_NAMES.col_name, Variables.DAY.col_name] - if '_is_filtered' in df.columns: - base_columns.append('_is_filtered') + base_columns = [ + Variables.HOUR.col_name, + Variables.UTC_TIME.col_name, + Variables.MONTH_NAMES.col_name, + Variables.DAY.col_name, + ] + if "_is_filtered" in df.columns: + base_columns.append("_is_filtered") if dd_value == dropdown_names[var_to_plot[0]]: - if f'_{Variables.DBT.col_name}_original' in df.columns: - base_columns.append(f'_{Variables.DBT.col_name}_original') + if f"_{Variables.DBT.col_name}_original" in df.columns: + base_columns.append(f"_{Variables.DBT.col_name}_original") units = generate_units_degree(si_ip) return dcc.Graph( config=generate_chart_name( @@ -233,8 +247,8 @@ def update_heatmap(_, global_local, dd_value, global_filter_data, df, meta, si_i ), ) else: - if f'_{Variables.DBT.col_name}_original' in df.columns: - base_columns.append(f'_{Variables.DBT.col_name}_original') + if f"_{Variables.DBT.col_name}_original" in df.columns: + base_columns.append(f"_{Variables.DBT.col_name}_original") units = generate_units(si_ip) return dcc.Graph( config=generate_chart_name(TabNames.RELATIVE_HUMIDITY_HEATMAP, meta, units), @@ -263,11 +277,12 @@ def update_table(_, dd_value, global_filter_data, df, si_ip): """Update the contents of descriptive statistics table.""" if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter + target_columns = [Variables.DBT.col_name, Variables.RH.col_name] df = apply_global_month_hour_filter(df, global_filter_data, target_columns) # Filter out the filtered rows to avoid empty columns - if '_is_filtered' in df.columns: - df = df[~df['_is_filtered']] + if "_is_filtered" in df.columns: + df = df[~df["_is_filtered"]] return summary_table_tmp_rh_tab( df[ diff --git a/pages/wind.py b/pages/wind.py index 937509c..c607898 100644 --- a/pages/wind.py +++ b/pages/wind.py @@ -201,8 +201,6 @@ def daily_wind_rose(): ) - - def layout(): """Contents in the fifth tab 'Wind'.""" return dmc.Stack( @@ -245,8 +243,16 @@ def layout(): ) def update_annual_wind_rose(_, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state - df = apply_global_month_hour_filter(df, global_filter_data, [Variables.WIND_SPEED.col_name, Variables.WIND_DIR.col_name]) + from pages.lib.layout import ( + apply_global_month_hour_filter, + get_global_filter_state, + ) + + df = apply_global_month_hour_filter( + df, + global_filter_data, + [Variables.WIND_SPEED.col_name, Variables.WIND_DIR.col_name], + ) months = [1, 12] hours = [1, 24] @@ -280,7 +286,10 @@ def update_annual_wind_rose(_, global_filter_data, df, meta, si_ip): def update_tab_wind_speed(_, global_local, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter - df = apply_global_month_hour_filter(df, global_filter_data, Variables.WIND_SPEED.col_name) + + df = apply_global_month_hour_filter( + df, global_filter_data, Variables.WIND_SPEED.col_name + ) speed = heatmap(df, Variables.WIND_SPEED.col_name, global_local, si_ip) units = generate_units(si_ip) @@ -306,7 +315,10 @@ def update_tab_wind_speed(_, global_local, global_filter_data, df, meta, si_ip): def update_tab_wind_direction(_, global_local, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import apply_global_month_hour_filter - df = apply_global_month_hour_filter(df, global_filter_data, Variables.WIND_DIR.col_name) + + df = apply_global_month_hour_filter( + df, global_filter_data, Variables.WIND_DIR.col_name + ) direction = heatmap(df, Variables.WIND_DIR.col_name, global_local, si_ip) units = generate_units(si_ip) @@ -316,8 +328,6 @@ def update_tab_wind_direction(_, global_local, global_filter_data, df, meta, si_ ) - - @callback( [ Output(ElementIds.WINTER_WIND_ROSE, "children"), @@ -457,8 +467,16 @@ def seasonal_chart_caption(month_start, month_end, count, n_calm): ) def update_daily_graphs(_, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import apply_global_month_hour_filter, get_global_filter_state - df = apply_global_month_hour_filter(df, global_filter_data, [Variables.WIND_SPEED.col_name, Variables.WIND_DIR.col_name]) + from pages.lib.layout import ( + apply_global_month_hour_filter, + get_global_filter_state, + ) + + df = apply_global_month_hour_filter( + df, + global_filter_data, + [Variables.WIND_SPEED.col_name, Variables.WIND_DIR.col_name], + ) months = [1, 12] else: From 414439b895fd107c323db3833d65a176d04973b2 Mon Sep 17 00:00:00 2001 From: yluo0664 Date: Mon, 27 Oct 2025 21:37:49 +1100 Subject: [PATCH 05/15] fix: formatted the code with removing unused parameters. --- pages/lib/charts_data_explorer.py | 1 - pages/lib/template_graphs.py | 2 -- pages/natural_ventilation.py | 1 - pages/sun.py | 2 -- pages/wind.py | 5 ----- 5 files changed, 11 deletions(-) diff --git a/pages/lib/charts_data_explorer.py b/pages/lib/charts_data_explorer.py index c63738b..de5450d 100644 --- a/pages/lib/charts_data_explorer.py +++ b/pages/lib/charts_data_explorer.py @@ -99,7 +99,6 @@ def custom_heatmap(df, global_local, var, time_filter_info, data_filter_info, si ) ) - normal_mask = ~filtered_mask normal_z = df[var].copy() normal_z[filtered_mask] = None diff --git a/pages/lib/template_graphs.py b/pages/lib/template_graphs.py index 5db67b8..2e56057 100644 --- a/pages/lib/template_graphs.py +++ b/pages/lib/template_graphs.py @@ -452,7 +452,6 @@ def heatmap_with_filter( ) ) - normal_mask = ~filtered_mask normal_z = df[var].copy() normal_z[filtered_mask] = None @@ -594,7 +593,6 @@ def heatmap(df, var, global_local, si_ip): ) ) - normal_mask = ~filtered_mask normal_z = df[var].copy() normal_z[filtered_mask] = None diff --git a/pages/natural_ventilation.py b/pages/natural_ventilation.py index e97181e..bc7bf58 100644 --- a/pages/natural_ventilation.py +++ b/pages/natural_ventilation.py @@ -13,7 +13,6 @@ tight_margins, month_lst, ) -from pages.lib.template_graphs import filter_df_by_month_and_hour from pages.lib.global_variables import Variables, VariableInfo from pages.lib.global_element_ids import ElementIds from pages.lib.global_id_buttons import IdButtons diff --git a/pages/sun.py b/pages/sun.py index b01130d..67f519f 100644 --- a/pages/sun.py +++ b/pages/sun.py @@ -4,7 +4,6 @@ import dash import dash_mantine_components as dmc -import numpy as np from dash import dcc from dash_extensions.enrich import Output, Input, State, callback @@ -22,7 +21,6 @@ sun_cloud_tab_explore_dropdown_names, dropdown_names, tight_margins, - month_lst, ) from pages.lib.template_graphs import heatmap, barchart, daily_profile from pages.lib.utils import ( diff --git a/pages/wind.py b/pages/wind.py index c607898..95d75bc 100644 --- a/pages/wind.py +++ b/pages/wind.py @@ -11,12 +11,9 @@ from pages.lib.global_id_buttons import IdButtons from pages.lib.global_tab_names import TabNames from pages.lib.utils import ( - title_with_tooltip, generate_chart_name, generate_units, - generate_custom_inputs_time, title_with_link, - dropdown, ) @@ -245,7 +242,6 @@ def update_annual_wind_rose(_, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import ( apply_global_month_hour_filter, - get_global_filter_state, ) df = apply_global_month_hour_filter( @@ -469,7 +465,6 @@ def update_daily_graphs(_, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): from pages.lib.layout import ( apply_global_month_hour_filter, - get_global_filter_state, ) df = apply_global_month_hour_filter( From e771549f6f474d36c39a55c7847157e9ee7fba68 Mon Sep 17 00:00:00 2001 From: yluo0664 Date: Tue, 28 Oct 2025 13:04:07 +1100 Subject: [PATCH 06/15] fix: fixed the errors in the documentation. --- docs/contributing/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md index 3bc8921..fe1d963 100644 --- a/docs/contributing/contributing.md +++ b/docs/contributing/contributing.md @@ -118,7 +118,7 @@ Strive to minimize redundancy in your code. ### UI Modifications For UI-related changes: * Avoid unnecessary custom CSS. -* [Use DMC (Design Master Components)](https://www.dash-mantine-components.com/) wherever applicable for layout and styling consistency. +* Use [DMC (Dash Mantine Components)](https://www.dash-mantine-components.com/) wherever applicable for layout and styling consistency. * Ensure visual consistency with existing components. From cffcdf21f3c93dddd66320d6ae60872ed536666b Mon Sep 17 00:00:00 2001 From: yluo0664 Date: Tue, 28 Oct 2025 16:13:07 +1100 Subject: [PATCH 07/15] fix: fixed the errors Temperature page that can not show Relative humidity correctly. --- pages/t_rh.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/t_rh.py b/pages/t_rh.py index 74234ed..5b43a5b 100644 --- a/pages/t_rh.py +++ b/pages/t_rh.py @@ -247,13 +247,13 @@ def update_heatmap(_, global_local, dd_value, global_filter_data, df, meta, si_i ), ) else: - if f"_{Variables.DBT.col_name}_original" in df.columns: - base_columns.append(f"_{Variables.DBT.col_name}_original") + if f"_{Variables.RH.col_name}_original" in df.columns: + base_columns.append(f"_{Variables.RH.col_name}_original") units = generate_units(si_ip) return dcc.Graph( config=generate_chart_name(TabNames.RELATIVE_HUMIDITY_HEATMAP, meta, units), figure=heatmap( - df[[Variables.DBT.col_name] + base_columns], + df[[Variables.RH.col_name] + base_columns], Variables.RH.col_name, global_local, si_ip, From 98e008cf3c43a0ff3c2417818bfe12e8b979046c Mon Sep 17 00:00:00 2001 From: Federico Tartarini Date: Wed, 29 Oct 2025 17:11:17 +1100 Subject: [PATCH 08/15] feat: removed css and unused IDs --- assets/banner.css | 11 ----------- pages/lib/global_element_ids.py | 4 +--- pages/lib/layout.py | 4 +--- 3 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 assets/banner.css diff --git a/assets/banner.css b/assets/banner.css deleted file mode 100644 index ef73f96..0000000 --- a/assets/banner.css +++ /dev/null @@ -1,11 +0,0 @@ -/**Banner**/ -#banner-title { - font-family: 'Open Sans', sans-serif; - font-weight: 500; -} - -#banner-subtitle { - font-family: 'Poppins', sans-serif; - font-weight: 400; - max-height: 25px; -} \ No newline at end of file diff --git a/pages/lib/global_element_ids.py b/pages/lib/global_element_ids.py index 67af4da..335748a 100644 --- a/pages/lib/global_element_ids.py +++ b/pages/lib/global_element_ids.py @@ -165,7 +165,7 @@ class ElementIds(str, Enum): ID_SELECT_DF_STORE = "df-store" ID_SELECT_SI_IP_UNIT_STORE = "si-ip-unit-store" ID_SELECT_SI_IP_RADIO_INPUT = "si-ip-radio-input" - ID_SELECT_BANNER_SUBTITLE = "banner-subtitle" + ID_SELECT_BANNER_SUBTITLE = "banner-selected-location" ID_SELECT_LINES_STORE = "lines-store" TAB_TWO_CONTAINER = "tab-two-container" ID_SUMMARY_SI_IP_RADIO_INPUT = "si-ip-radio-input" @@ -198,8 +198,6 @@ class ElementIds(str, Enum): ID_LAYOUT_ALERT_AUTO = "alert-auto" ID_LAYOUT_INTERVAL_COMPONENT = "interval-component" FOOTER_CONTAINER = "footer-container" - BANNER_TITLE = "banner-title" - ID_LAYOUT_BANNER_SUBTITLE = "banner-subtitle" ID_LAYOUT_GLOBAL_LOCAL_RADIO_INPUT = "global-local-radio-input" ID_LAYOUT_SI_IP_RADIO_INPUT = "si-ip-radio-input" STORE = "store" diff --git a/pages/lib/layout.py b/pages/lib/layout.py index bb116dc..56ab22c 100644 --- a/pages/lib/layout.py +++ b/pages/lib/layout.py @@ -290,16 +290,14 @@ def create_header(): children=[ dmc.Title( "CBE Clima Tool", - id=ElementIds.BANNER_TITLE, order=2, lh=1.1, c="white", ), dmc.Text( "Current Location: N/A", - id=ElementIds.ID_LAYOUT_BANNER_SUBTITLE, + id=ElementIds.ID_SELECT_BANNER_SUBTITLE, size="sm", - opacity=0.85, style={"overflow": "hidden"}, c="white", ), From 4dce09ad80cd92ba083f92f9ff47f470a0d23c1b Mon Sep 17 00:00:00 2001 From: Federico Tartarini Date: Wed, 29 Oct 2025 17:17:17 +1100 Subject: [PATCH 09/15] docs: update contributing guidelines to include pipenv installation and usage --- docs/contributing/contributing.md | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md index fe1d963..5a5c082 100644 --- a/docs/contributing/contributing.md +++ b/docs/contributing/contributing.md @@ -23,6 +23,12 @@ git clone https://github.com/Your Account name/clima.git cd clima ``` +Install the dependencies using pipenv. You will need to have pipenv installed on your machine. If you do not have it yet, please refer to [pipenv installation guide](https://pipenv.pypa.io/en/latest/#install-pipenv-today). + +```bash +pipenv sync --dev +```` + Set up the upstream repository and check the output repositories. ```bash @@ -97,18 +103,6 @@ pipenv run pre-commit run --all-files Hence, you will need to make sure that the code is formatted correctly before committing your changes; otherwise, the commit will fail. More information about pre-commit hooks can be found [here](https://pre-commit.com/). -Install Black: - -```bash -pipenv install black -``` - -Format your code before committing: - -```bash -black . -``` - ### Code Simplicity Strive to minimize redundancy in your code. - Keep logic as concise as possible. @@ -117,11 +111,10 @@ Strive to minimize redundancy in your code. ### UI Modifications For UI-related changes: -* Avoid unnecessary custom CSS. * Use [DMC (Dash Mantine Components)](https://www.dash-mantine-components.com/) wherever applicable for layout and styling consistency. +* Do not use CSS, only inline styles are allowed, but still try to minimize their use. * Ensure visual consistency with existing components. - ## Testing Before submitting a Pull Request, please make sure: @@ -129,15 +122,13 @@ Before submitting a Pull Request, please make sure: - You have installed project dependencies: ```bash -pipenv sync +pipenv sync --dev ``` -From the root directory, run: +- Run tests using pytest: ```bash -cd tests/node - -npx cypress run +pipenv run pytest ``` ## Submitting changes From 1597f908b9691d2c990b8e88cc1ad935466a9a21 Mon Sep 17 00:00:00 2001 From: Federico Tartarini Date: Wed, 29 Oct 2025 17:28:12 +1100 Subject: [PATCH 10/15] refactor(layout): simplify month and hour filter components structure --- pages/lib/layout.py | 130 ++++++++++++++++---------------------------- 1 file changed, 48 insertions(+), 82 deletions(-) diff --git a/pages/lib/layout.py b/pages/lib/layout.py index 56ab22c..2fc139b 100644 --- a/pages/lib/layout.py +++ b/pages/lib/layout.py @@ -42,108 +42,74 @@ def get_icon(cls, page_name): # global filters def create_tools_filter_components(): # Apply month and hour filter - apply_month_hour_section = dmc.Stack( + return dmc.Stack( id=ElementIds.TOOLS_MONTH_HOUR_SECTION, children=[ - dmc.Divider(label="Filter function", size="xs", color="blue", mb="xs"), + dmc.Divider(label="Filter function", size="xs", color="blue"), dmc.Button( "Apply month and hour filter", id=ElementIds.TOOLS_APPLY_MONTH_HOUR_FILTER, color="blue", variant="light", size="xs", - mb="xs", ), - dmc.Stack( + dmc.Text("Month Range:", size="xs", c="dimmed"), + dcc.RangeSlider( + id=ElementIds.TOOLS_MONTH_SLIDER, + min=1, + max=12, + step=1, + value=[1, 12], + marks={1: "1", 12: "12"}, + tooltip={ + "always_visible": False, + "placement": "top", + }, + allowCross=False, + ), + dmc.Group( [ - dmc.Text("Month Range:", size="xs", c="dimmed"), - dmc.Stack( - [ - dcc.RangeSlider( - id=ElementIds.TOOLS_MONTH_SLIDER, - min=1, - max=12, - step=1, - value=[1, 12], - marks={1: "1", 12: "12"}, - tooltip={ - "always_visible": False, - "placement": "top", - }, - allowCross=False, - ), - dmc.Group( - [ - dmc.Switch( - id=ElementIds.TOOLS_INVERT_MONTH, - label="Invert", - checked=False, - size="xs", - color="blue", - style={"fontSize": "0.7rem"}, - ), - ], - justify="flex-end", - ), - ], - gap="xs", + dmc.Switch( + id=ElementIds.TOOLS_INVERT_MONTH, + label="Invert", + checked=False, + size="xs", + color="blue", + style={"fontSize": "0.7rem"}, ), ], - gap="xs", - mb="xs", + justify="flex-end", ), - dmc.Stack( + dmc.Text("Hour Range:", size="xs", c="dimmed"), + dcc.RangeSlider( + id=ElementIds.TOOLS_HOUR_SLIDER, + min=0, + max=24, + step=1, + value=[0, 24], + marks={0: "0", 24: "24"}, + tooltip={ + "always_visible": False, + "placement": "top", + }, + allowCross=False, + ), + dmc.Group( [ - dmc.Text("Hour Range:", size="xs", c="dimmed"), - dmc.Stack( - [ - dcc.RangeSlider( - id=ElementIds.TOOLS_HOUR_SLIDER, - min=0, - max=24, - step=1, - value=[0, 24], - marks={0: "0", 24: "24"}, - tooltip={ - "always_visible": False, - "placement": "top", - }, - allowCross=False, - ), - dmc.Group( - [ - dmc.Switch( - id=ElementIds.TOOLS_INVERT_HOUR, - label="Invert", - checked=False, - size="xs", - color="blue", - style={"fontSize": "0.7rem"}, - ), - ], - justify="flex-end", - ), - ], - gap="xs", + dmc.Switch( + id=ElementIds.TOOLS_INVERT_HOUR, + label="Invert", + checked=False, + size="xs", + color="blue", + style={"fontSize": "0.7rem"}, ), ], - gap="xs", + justify="flex-end", ), ], gap="xs", p="xs", - style={ - "backgroundColor": "#f8f9fa", - "borderRadius": "6px", - "border": "1px solid #e9ecef", - }, - ) - - return dmc.Stack( - children=[ - apply_month_hour_section, - ], - gap="sm", ) From c7a1ef788937d1fa071fbbf3f21de1e9d919b9cd Mon Sep 17 00:00:00 2001 From: Federico Tartarini Date: Wed, 29 Oct 2025 17:43:18 +1100 Subject: [PATCH 11/15] refactor(outdoor): simplify inputs_outdoor_comfort layout and improve structure --- pages/outdoor.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/pages/outdoor.py b/pages/outdoor.py index 2b96697..0ed836b 100644 --- a/pages/outdoor.py +++ b/pages/outdoor.py @@ -36,35 +36,29 @@ def inputs_outdoor_comfort(): - return dmc.SimpleGrid( - cols=2, - children=[ - dmc.Group( - [ - dmc.Title("Select a scenario:", order=5), - dmc.Stack( - dropdown( - id=ElementIds.OUTDOOR_DROPDOWN, - options=outdoor_dropdown_names, - value="utci_Sun_Wind", - persistence=True, - persistence_type="session", - ), - flex=1, - ), - dmc.Paper(id=ElementIds.IMAGE_SELECTION), - ], - align="flex-start", + return dmc.Group( + [ + dmc.Title("Select a scenario:", order=5), + dropdown( + id=ElementIds.OUTDOOR_DROPDOWN, + options=outdoor_dropdown_names, + value="utci_Sun_Wind", + persistence=True, + persistence_type="session", ), + dmc.Paper(id=ElementIds.IMAGE_SELECTION), ], + gap="xs", + justify="center", ) def outdoor_comfort_chart(): return dmc.Stack( children=[ - dmc.Paper( + dmc.Title( id=ElementIds.OUTDOOR_COMFORT_OUTPUT, + order=4 ), title_with_link( text="UTCI heatmap chart", From 6d1d4898adf3fb353de233bdaf499be8fffd6ec4 Mon Sep 17 00:00:00 2001 From: yluo0664 Date: Thu, 30 Oct 2025 18:21:52 +1100 Subject: [PATCH 12/15] fix: fixed the potential issues - optimized duplicated code - removed redundant stack - fixed filter issues --- docs/README.md | 2 +- pages/lib/charts_data_explorer.py | 17 ++--- pages/lib/layout.py | 83 ++++++++--------------- pages/lib/template_graphs.py | 28 ++++---- pages/lib/utils.py | 109 +++++++++++++++++++++++++++--- pages/natural_ventilation.py | 17 ++--- pages/outdoor.py | 89 +++++------------------- pages/select.py | 42 ++---------- pages/summary.py | 37 ++++++++-- 9 files changed, 214 insertions(+), 210 deletions(-) diff --git a/docs/README.md b/docs/README.md index b194b92..84cb022 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,7 +34,7 @@ This ongoing project results from the collaboration and contributions of the peo * [Chun Him Lee](https://www.linkedin.com/in/chun-him-lee-01b553129/): Coding and review * [Tu Minh Phuong Doan](https://www.linkedin.com/in/harry-doan-legopher/): Coding and review * [Yixun Quan](https://www.linkedin.com/in/yixun-quan-929a661a3): Coding and review -* [Yuqing Luo](https://github.com/LeoLuosifen): Coding, code maintenance and review +* Yuqing Luo: Coding, code maintenance and review * Wenshu lyu: Coding, code maintenance and review * Ziqi Liu: Coding, code maintenance and review * Tianchi Liu: Coding, code maintenance and review diff --git a/pages/lib/charts_data_explorer.py b/pages/lib/charts_data_explorer.py index de5450d..0d7f840 100644 --- a/pages/lib/charts_data_explorer.py +++ b/pages/lib/charts_data_explorer.py @@ -68,18 +68,15 @@ def custom_heatmap(df, global_local, var, time_filter_info, data_filter_info, si filtered_mask = df["_is_filtered"] if filtered_mask.any(): original_col = f"_{var}_original" - if original_col in df.columns: - filtered_z = df[original_col].copy() - else: - filtered_z = df[var].copy() - - filtered_z[~filtered_mask] = None + col_to_use = original_col if original_col in df.columns else var + filtered_values = df[col_to_use].copy() + filtered_values[~filtered_mask] = None fig.add_trace( go.Heatmap( y=df[Variables.HOUR.col_name], x=df[Variables.DOY.col_name], - z=filtered_z, + z=filtered_values, colorscale=[[0, "lightgray"], [1, "gray"]], zmin=range_z[0], zmax=range_z[1], @@ -99,14 +96,14 @@ def custom_heatmap(df, global_local, var, time_filter_info, data_filter_info, si ) ) - normal_z = df[var].copy() - normal_z[filtered_mask] = None + base_values = df[var].copy() + base_values[filtered_mask] = None fig.add_trace( go.Heatmap( y=df[Variables.HOUR.col_name], x=df[Variables.DOY.col_name], - z=normal_z, + z=base_values, colorscale=var_color, zmin=range_z[0], zmax=range_z[1], diff --git a/pages/lib/layout.py b/pages/lib/layout.py index 2fc139b..feb62a2 100644 --- a/pages/lib/layout.py +++ b/pages/lib/layout.py @@ -5,7 +5,11 @@ from pages.lib.global_variables import Variables from config import DocLinks, UnitSystem from pages.lib.global_element_ids import ElementIds -from pages.lib.utils import determine_month_and_hour_filter +from pages.lib.utils import ( + determine_month_and_hour_filter, + get_default_global_filter_store_data, + get_global_filter_state, +) class NavBarIcons: @@ -41,7 +45,7 @@ def get_icon(cls, page_name): # global filters def create_tools_filter_components(): - # Apply month and hour filter + # Apply month and hour filter (reduced nesting, same visual layout) return dmc.Stack( id=ElementIds.TOOLS_MONTH_HOUR_SECTION, children=[ @@ -53,6 +57,7 @@ def create_tools_filter_components(): variant="light", size="xs", ), + # Month controls dmc.Text("Month Range:", size="xs", c="dimmed"), dcc.RangeSlider( id=ElementIds.TOOLS_MONTH_SLIDER, @@ -80,6 +85,7 @@ def create_tools_filter_components(): ], justify="flex-end", ), + # Hour controls dmc.Text("Hour Range:", size="xs", c="dimmed"), dcc.RangeSlider( id=ElementIds.TOOLS_HOUR_SLIDER, @@ -394,13 +400,7 @@ def create_stores(): ), dcc.Store( id=ElementIds.TOOLS_GLOBAL_FILTER_STORE, - data={ - "month_range": [1, 12], - "hour_range": [0, 24], - "invert_month": [], - "invert_hour": [], - "filter_active": False, - }, + data=get_default_global_filter_store_data(), storage_type="session", ), dcc.Interval( @@ -531,42 +531,23 @@ def show_alert_after_delay(n_intervals): def update_global_filter_state( apply_clicks, month_range, hour_range, invert_month, invert_hour, current_data ): - if apply_clicks is None: - apply_clicks = 0 - - if apply_clicks > 0: - current_data["filter_active"] = True - - current_data.update( - { - "month_range": month_range or [1, 12], - "hour_range": hour_range or [0, 24], - "invert_month": ["invert"] if invert_month else [], - "invert_hour": ["invert"] if invert_hour else [], - } - ) - - return current_data - - -def get_global_filter_state(filter_store_data): - if not filter_store_data: - return { - "filter_active": False, - "month_range": [1, 12], - "hour_range": [0, 24], - "invert_month": False, - "invert_hour": False, - } - - return { - "filter_active": filter_store_data.get("filter_active", False), - "month_range": filter_store_data.get("month_range", [1, 12]), - "hour_range": filter_store_data.get("hour_range", [0, 24]), - "invert_month": bool(filter_store_data.get("invert_month", [])), - "invert_hour": bool(filter_store_data.get("invert_hour", [])), + if not apply_clicks: + return current_data or get_default_global_filter_store_data() + + # Normalize existing data, then override with inputs + base_state = get_global_filter_state(current_data) + updated_state = { + **base_state, + "filter_active": True, + "month_range": month_range or base_state["month_range"], + "hour_range": hour_range or base_state["hour_range"], + # store as booleans; readers use get_global_filter_state for coercion + "invert_month": bool(invert_month), + "invert_hour": bool(invert_hour), } + return updated_state + def apply_global_month_hour_filter(df, filter_store_data, target_columns=None): filter_state = get_global_filter_state(filter_store_data) @@ -592,7 +573,6 @@ def apply_global_month_hour_filter(df, filter_store_data, target_columns=None): elif isinstance(target_columns, str): target_columns = [target_columns] - month_mask = None if start_month <= end_month: month_mask = (df_copy[Variables.MONTH.col_name] < start_month) | ( df_copy[Variables.MONTH.col_name] > end_month @@ -602,7 +582,6 @@ def apply_global_month_hour_filter(df, filter_store_data, target_columns=None): df_copy[Variables.MONTH.col_name] <= start_month ) - hour_mask = None if start_hour <= end_hour: hour_mask = (df_copy[Variables.HOUR.col_name] < start_hour) | ( df_copy[Variables.HOUR.col_name] > end_hour @@ -623,7 +602,7 @@ def apply_global_month_hour_filter(df, filter_store_data, target_columns=None): df_copy, start_month, end_month, Variables.MONTH.col_name, target_col ) time_filtering( - df_copy, start_hour, end_hour, Variables.MONTH.col_name, target_col + df_copy, start_hour, end_hour, Variables.HOUR.col_name, target_col ) return df_copy @@ -642,12 +621,10 @@ def apply_global_month_hour_filter(df, filter_store_data, target_columns=None): prevent_initial_call=False, ) def sync_sliders_with_global_state(global_filter_data): - if not global_filter_data: - return [1, 12], [0, 24], False, False - + state = get_global_filter_state(global_filter_data) return ( - global_filter_data.get("month_range", [1, 12]), - global_filter_data.get("hour_range", [0, 24]), - bool(global_filter_data.get("invert_month", [])), - bool(global_filter_data.get("invert_hour", [])), + state["month_range"], + state["hour_range"], + state["invert_month"], + state["invert_hour"], ) diff --git a/pages/lib/template_graphs.py b/pages/lib/template_graphs.py index 2e56057..b6ab5b5 100644 --- a/pages/lib/template_graphs.py +++ b/pages/lib/template_graphs.py @@ -421,17 +421,17 @@ def heatmap_with_filter( if filtered_mask.any(): original_col = f"_{var}_original" if original_col in df.columns: - filtered_z = df[original_col].copy() + filtered_values = df[original_col].copy() else: - filtered_z = df[var].copy() + filtered_values = df[var].copy() - filtered_z[~filtered_mask] = None + filtered_values[~filtered_mask] = None fig.add_trace( go.Heatmap( y=df[Variables.HOUR.col_name] - 0.5, x=df[Variables.UTC_TIME.col_name].dt.date, - z=filtered_z, + z=filtered_values, colorscale=[[0, "lightgray"], [1, "gray"]], zmin=range_z[0], zmax=range_z[1], @@ -452,14 +452,14 @@ def heatmap_with_filter( ) ) - normal_z = df[var].copy() - normal_z[filtered_mask] = None + base_values = df[var].copy() + base_values[filtered_mask] = None fig.add_trace( go.Heatmap( y=df[Variables.HOUR.col_name] - 0.5, x=df[Variables.UTC_TIME.col_name].dt.date, - z=normal_z, + z=base_values, colorscale=var_color, zmin=range_z[0], zmax=range_z[1], @@ -562,17 +562,17 @@ def heatmap(df, var, global_local, si_ip): if filtered_mask.any(): original_col = f"_{var}_original" if original_col in df.columns: - filtered_z = df[original_col].copy() + filtered_values = df[original_col].copy() else: - filtered_z = df[var].copy() + filtered_values = df[var].copy() - filtered_z[~filtered_mask] = None + filtered_values[~filtered_mask] = None fig.add_trace( go.Heatmap( y=df[Variables.HOUR.col_name], x=df[Variables.UTC_TIME.col_name].dt.date, - z=filtered_z, + z=filtered_values, colorscale=[[0, "lightgray"], [1, "gray"]], zmin=range_z[0], zmax=range_z[1], @@ -593,14 +593,14 @@ def heatmap(df, var, global_local, si_ip): ) ) - normal_z = df[var].copy() - normal_z[filtered_mask] = None + base_values = df[var].copy() + base_values[filtered_mask] = None fig.add_trace( go.Heatmap( y=df[Variables.HOUR.col_name], x=df[Variables.UTC_TIME.col_name].dt.date, - z=normal_z, + z=base_values, colorscale=var_color, zmin=range_z[0], zmax=range_z[1], diff --git a/pages/lib/utils.py b/pages/lib/utils.py index 30d5e43..5628998 100644 --- a/pages/lib/utils.py +++ b/pages/lib/utils.py @@ -217,13 +217,44 @@ def summary_table_tmp_rh_tab(df, value, si_ip): .describe(percentiles=[0.01, 0.25, 0.5, 0.75, 0.99]) .round(2) ) - df_summary = df_summary.reset_index( - level=Variables.MONTH_NAMES.col_name - ).sort_index() - df_summary = df_summary.drop(["count"], axis=1) - df_summary = df_summary.rename( - columns={Variables.MONTH_NAMES.col_name: Variables.MONTH.col_name} - ) + # Robust reset: when groupby is empty, index level names may be lost (None) + df_summary = df_summary.reset_index() + # Ensure we have a single human-readable month column named 'month' + has_month_num = Variables.MONTH.col_name in df_summary.columns + has_month_name = Variables.MONTH_NAMES.col_name in df_summary.columns + if has_month_num: + df_summary = df_summary.sort_values(by=Variables.MONTH.col_name) + if has_month_name and has_month_num: + # Keep readable names as 'month', drop numeric to avoid duplicate columns + df_summary = df_summary.rename( + columns={Variables.MONTH_NAMES.col_name: Variables.MONTH.col_name} + ) + # After rename there will be two 'month' columns; drop the numeric one by position + # Keep the leftmost 'month' (the renamed names column) + cols = [] + seen = set() + for c in df_summary.columns: + if c == Variables.MONTH.col_name: + if c in seen: + continue + seen.add(c) + cols.append(c) + else: + cols.append(c) + df_summary = df_summary.loc[:, cols] + # Explicitly drop the numeric month column if still present as a duplicate + if df_summary.columns.duplicated().any(): + df_summary = df_summary.loc[:, ~df_summary.columns.duplicated()] + elif has_month_name and not has_month_num: + df_summary = df_summary.rename( + columns={Variables.MONTH_NAMES.col_name: Variables.MONTH.col_name} + ) + # Drop 'count' if present + if "count" in df_summary.columns: + df_summary = df_summary.drop(["count"], axis=1) + # Guarantee unique columns + if df_summary.columns.duplicated().any(): + df_summary = df_summary.loc[:, ~df_summary.columns.duplicated()] df_sum = ( df[value] @@ -235,7 +266,7 @@ def summary_table_tmp_rh_tab(df, value, si_ip): columns={"count": Variables.MONTH.col_name} ) - df_summary = pd.concat([df_summary, df_sum]) + df_summary = pd.concat([df_summary, df_sum], ignore_index=True) unit = ( VariableInfo.from_col_name(value) @@ -288,7 +319,6 @@ def dropdown(options=None, **kwargs): return dcc.Dropdown( options=[{"label": k, "value": v} for k, v in options.items()], clearable=False, - style={"width": "14rem"}, **kwargs, ) @@ -303,6 +333,63 @@ def get_max_min_value(series: pd.Series, base: int = 5) -> tuple[int, int]: Returns: Tuple of (max_value, min_value) adjusted to nearest base step. """ - data_max = base * math.ceil(series.max() / base) - data_min = base * math.floor(series.min() / base) + # Guard against all-NaN series after filtering + non_na = series.dropna() + if non_na.empty: + # Fallback to a symmetric small range to avoid rendering errors + return base, -base + + data_max = base * math.ceil(non_na.max() / base) + data_min = base * math.floor(non_na.min() / base) return data_max, data_min + + +def get_default_global_filter_store_data() -> dict: + """Return default data structure for TOOLS_GLOBAL_FILTER_STORE. + + Centralizes the default so it can be reused across pages without duplication. + """ + return { + "month_range": [1, 12], + "hour_range": [0, 24], + "invert_month": [], + "invert_hour": [], + "filter_active": False, + } + + +def get_global_filter_state(filter_store_data: dict | None) -> dict: + """Normalize filter store data into a consistent, easy-to-use structure. + + Ensures defaults are applied and types are coerced to booleans where appropriate. + """ + default_data = get_default_global_filter_store_data() + data = ( + default_data if not filter_store_data else {**default_data, **filter_store_data} + ) + + return { + "filter_active": bool(data.get("filter_active", False)), + "month_range": data.get("month_range", [1, 12]), + "hour_range": data.get("hour_range", [0, 24]), + # invert flags may be stored as []/['invert'] or booleans; coerce to bool + "invert_month": bool(data.get("invert_month", [])), + "invert_hour": bool(data.get("invert_hour", [])), + } + + +def get_time_filter_from_store( + filter_store_data: dict | None, +) -> tuple[bool, list[int], list[int], bool, bool]: + """Return normalized time filter arguments from the global filter store. + + Returns (time_filter, month, hour, invert_month, invert_hour). + """ + state = get_global_filter_state(filter_store_data) + return ( + True, + state["month_range"], + state["hour_range"], + state["invert_month"], + state["invert_hour"], + ) diff --git a/pages/natural_ventilation.py b/pages/natural_ventilation.py index bc7bf58..b2e2451 100644 --- a/pages/natural_ventilation.py +++ b/pages/natural_ventilation.py @@ -150,19 +150,20 @@ def inputs_tab(t_min, t_max, d_set): variant="link", disabled=True, ), - dcc.Checklist( - options=[ - { - "label": ( + dmc.CheckboxGroup( + id=ElementIds.ENABLE_CONDENSATION, + value=[], + children=[ + dmc.Checkbox( + label=( "Avoid condensation with radiant systems: If the " "outdoor dew point temperature is below the radiant " "system surface temperature, the data point is not plot." ), - "value": 1, - } + value=1, + size="sm", + ) ], - value=[], - id=ElementIds.ENABLE_CONDENSATION, ), dmc.Group( [ diff --git a/pages/outdoor.py b/pages/outdoor.py index 0ed836b..816c5e5 100644 --- a/pages/outdoor.py +++ b/pages/outdoor.py @@ -25,6 +25,7 @@ title_with_link, title_with_tooltip, ) +from pages.lib.utils import get_time_filter_from_store dash.register_page( @@ -56,10 +57,7 @@ def inputs_outdoor_comfort(): def outdoor_comfort_chart(): return dmc.Stack( children=[ - dmc.Title( - id=ElementIds.OUTDOOR_COMFORT_OUTPUT, - order=4 - ), + dmc.Title(id=ElementIds.OUTDOOR_COMFORT_OUTPUT, order=4), title_with_link( text="UTCI heatmap chart", id_button=IdButtons.UTCI_CHARTS_LABEL, @@ -212,32 +210,14 @@ def update_tab_utci_value( si_ip, ): if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import ( - apply_global_month_hour_filter, - get_global_filter_state, - ) + from pages.lib.layout import apply_global_month_hour_filter df = apply_global_month_hour_filter(df, global_filter_data, var) - filter_state = get_global_filter_state(global_filter_data) - month_range = filter_state["month_range"] - hour_range = filter_state["hour_range"] - invert_month_global = filter_state["invert_month"] - invert_hour_global = filter_state["invert_hour"] - - # time_filter is always True for global filter - time_filter = True - month = month_range - hour = hour_range - invert_month = invert_month_global - invert_hour = invert_hour_global - else: - # Use default values when global filter is not active - time_filter = True - month = [1, 12] - hour = [0, 24] - invert_month = [] - invert_hour = [] + # Normalize filter state (handles inactive case with defaults) + time_filter, month, hour, invert_month, invert_hour = get_time_filter_from_store( + global_filter_data if global_filter_data else None + ) custom_inputs = f"{var}" units = generate_units_degree(si_ip) @@ -299,34 +279,15 @@ def update_tab_utci_category( si_ip, ): if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import ( - apply_global_month_hour_filter, - get_global_filter_state, - ) + from pages.lib.layout import apply_global_month_hour_filter df = apply_global_month_hour_filter( df, global_filter_data, [var, var + "_categories"] ) - filter_state = get_global_filter_state(global_filter_data) - month_range = filter_state["month_range"] - hour_range = filter_state["hour_range"] - invert_month_global = filter_state["invert_month"] - invert_hour_global = filter_state["invert_hour"] - - # time_filter is always True for global filter - time_filter = True - month = month_range - hour = hour_range - invert_month = invert_month_global - invert_hour = invert_hour_global - else: - # time_filter is always True for local filter too - time_filter = True - month = [1, 12] - hour = [0, 24] - invert_month = [] - invert_hour = [] + time_filter, month, hour, invert_month, invert_hour = get_time_filter_from_store( + global_filter_data if global_filter_data else None + ) utci_stress_cat = heatmap_with_filter( df, @@ -386,32 +347,14 @@ def update_tab_utci_category( ) def update_tab_utci_summary_chart(var, normalize, global_filter_data, df, meta, si_ip): if global_filter_data and global_filter_data.get("filter_active", False): - from pages.lib.layout import ( - apply_global_month_hour_filter, - get_global_filter_state, - ) + from pages.lib.layout import apply_global_month_hour_filter df = apply_global_month_hour_filter(df, global_filter_data, var) - filter_state = get_global_filter_state(global_filter_data) - month_range = filter_state["month_range"] - hour_range = filter_state["hour_range"] - invert_month_global = filter_state["invert_month"] - invert_hour_global = filter_state["invert_hour"] - - # time_filter is always True for global filter - time_filter = True - month = month_range - hour = hour_range - invert_month = invert_month_global - invert_hour = invert_hour_global - else: - # time_filter is always True for local filter too - time_filter = True - month = [1, 12] - hour = [0, 24] - invert_month = [] - invert_hour = [] + # Unified filter state for both active and inactive cases + time_filter, month, hour, invert_month, invert_hour = get_time_filter_from_store( + global_filter_data if global_filter_data else None + ) utci_summary_chart = thermal_stress_stacked_barchart( df, diff --git a/pages/select.py b/pages/select.py index eda1477..778e4b0 100644 --- a/pages/select.py +++ b/pages/select.py @@ -16,7 +16,7 @@ from pages.lib.global_element_ids import ElementIds from pages.lib.global_tab_names import TabNames from config import PageUrls, PageInfo -from pages.lib.utils import generate_chart_name +from pages.lib.utils import generate_chart_name, get_default_global_filter_store_data dash.register_page( __name__, @@ -158,13 +158,7 @@ def submitted_data( True, messages_alert["not_available"], "orange", - { - "month_range": [1, 12], - "hour_range": [0, 24], - "invert_month": [], - "invert_hour": [], - "filter_active": False, - }, + get_default_global_filter_store_data(), ) location_info = get_location_info( lines, url_store @@ -175,13 +169,7 @@ def submitted_data( True, messages_alert["success"], "green", - { - "month_range": [1, 12], - "hour_range": [0, 24], - "invert_month": [], - "invert_hour": [], - "filter_active": False, - }, + get_default_global_filter_store_data(), ) elif ( @@ -206,13 +194,7 @@ def submitted_data( True, messages_alert["success"], "green", - { - "month_range": [1, 12], - "hour_range": [0, 24], - "invert_month": [], - "invert_hour": [], - "filter_active": False, - }, + get_default_global_filter_store_data(), ) else: return ( @@ -221,13 +203,7 @@ def submitted_data( True, messages_alert["invalid_format"], "orange", - { - "month_range": [1, 12], - "hour_range": [0, 24], - "invert_month": [], - "invert_hour": [], - "filter_active": False, - }, + get_default_global_filter_store_data(), ) except (ValueError, IndexError, KeyError) as e: print(f"Error parsing EPW file: {e}") @@ -237,13 +213,7 @@ def submitted_data( True, messages_alert["wrong_extension"], "orange", - { - "month_range": [1, 12], - "hour_range": [0, 24], - "invert_month": [], - "invert_hour": [], - "filter_active": False, - }, + get_default_global_filter_store_data(), ) raise PreventUpdate diff --git a/pages/summary.py b/pages/summary.py index 3a26a28..82119e3 100644 --- a/pages/summary.py +++ b/pages/summary.py @@ -384,6 +384,7 @@ def degree_day_chart( [ Input(ElementIds.ID_SUMMARY_DF_STORE, "modified_timestamp"), Input(ElementIds.ID_SUMMARY_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_SUMMARY_DF_STORE, "data"), @@ -391,7 +392,14 @@ def degree_day_chart( State(ElementIds.ID_SUMMARY_SI_IP_UNIT_STORE, "data"), ], ) -def update_violin_tdb(ts, global_local, df, meta, si_ip): +def update_violin_tdb(ts, global_local, global_filter_data, df, meta, si_ip): + # Apply global filter if active + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + + df = apply_global_month_hour_filter( + df, global_filter_data, Variables.DBT.col_name + ) units = generate_units_degree(si_ip) return dcc.Graph( id=ElementIds.TDB_PROFILE_GRAPH, @@ -405,6 +413,7 @@ def update_violin_tdb(ts, global_local, df, meta, si_ip): [ Input(ElementIds.ID_SUMMARY_DF_STORE, "modified_timestamp"), Input(ElementIds.ID_SUMMARY_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_SUMMARY_DF_STORE, "data"), @@ -412,8 +421,14 @@ def update_violin_tdb(ts, global_local, df, meta, si_ip): State(ElementIds.ID_SUMMARY_SI_IP_UNIT_STORE, "data"), ], ) -def update_tab_wind(ts, global_local, df, meta, si_ip): +def update_tab_wind(ts, global_local, global_filter_data, df, meta, si_ip): """Update the contents of tab two. Passing in the general info (df, meta).""" + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + + df = apply_global_month_hour_filter( + df, global_filter_data, Variables.WIND_SPEED.col_name + ) units = generate_units(si_ip) return dcc.Graph( id=ElementIds.WIND_PROFILE_GRAPH, @@ -427,6 +442,7 @@ def update_tab_wind(ts, global_local, df, meta, si_ip): [ Input(ElementIds.ID_SUMMARY_DF_STORE, "modified_timestamp"), Input(ElementIds.ID_SUMMARY_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_SUMMARY_DF_STORE, "data"), @@ -434,8 +450,14 @@ def update_tab_wind(ts, global_local, df, meta, si_ip): State(ElementIds.ID_SUMMARY_SI_IP_UNIT_STORE, "data"), ], ) -def update_tab_rh(ts, global_local, df, meta, si_ip): +def update_tab_rh(ts, global_local, global_filter_data, df, meta, si_ip): """Update the contents of tab two. Passing in the general info (df, meta).""" + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + + df = apply_global_month_hour_filter( + df, global_filter_data, Variables.RH.col_name + ) units = generate_units(si_ip) return dcc.Graph( id=ElementIds.RH_PROFILE_GRAPH, @@ -449,6 +471,7 @@ def update_tab_rh(ts, global_local, df, meta, si_ip): [ Input(ElementIds.ID_SUMMARY_DF_STORE, "modified_timestamp"), Input(ElementIds.ID_SUMMARY_GLOBAL_LOCAL_RADIO_INPUT, "value"), + Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"), ], [ State(ElementIds.ID_SUMMARY_DF_STORE, "data"), @@ -456,8 +479,14 @@ def update_tab_rh(ts, global_local, df, meta, si_ip): State(ElementIds.ID_SUMMARY_SI_IP_UNIT_STORE, "data"), ], ) -def update_tab_gh_rad(ts, global_local, df, meta, si_ip): +def update_tab_gh_rad(ts, global_local, global_filter_data, df, meta, si_ip): """Update the contents of tab two. Passing in the general info (df, meta).""" + if global_filter_data and global_filter_data.get("filter_active", False): + from pages.lib.layout import apply_global_month_hour_filter + + df = apply_global_month_hour_filter( + df, global_filter_data, Variables.GLOB_HOR_RAD.col_name + ) units = generate_units(si_ip) return dcc.Graph( id=ElementIds.GH_RAD_PROFILE_GRAPH, From 7afef7a8cf22afe0a3f6558a9d146cc318298d0e Mon Sep 17 00:00:00 2001 From: yluo0664 Date: Fri, 31 Oct 2025 09:16:54 +1100 Subject: [PATCH 13/15] fix: fixed the Stack issues, replaced SimpleGrid with Gird --- pages/explorer.py | 199 +++++++++++++++-------------------- pages/lib/utils.py | 1 + pages/natural_ventilation.py | 124 +++++++++++----------- pages/psy-chart.py | 77 ++++++-------- 4 files changed, 182 insertions(+), 219 deletions(-) diff --git a/pages/explorer.py b/pages/explorer.py index fb39899..63c30ff 100644 --- a/pages/explorer.py +++ b/pages/explorer.py @@ -111,16 +111,6 @@ def section_one(): tooltip_text="count, mean, std, min, max, and percentiles", id_button=IdButtons.TABLE_EXPLORE, ), - dmc.Center( - children=dmc.Box( - w="33%", - children=dmc.Stack( - children=[ - # Month and hour filter moved to global sidebar - ], - ), - ) - ), # Results table dmc.Paper(id=ElementIds.TABLE_DATA_EXPLORER, p="sm"), ] @@ -136,71 +126,64 @@ def section_two_inputs(): tooltip_text=None, id_button=IdButtons.CUSTOM_HEATMAP_CHART_LABEL, ), - dmc.SimpleGrid( - cols=2, - spacing="md", + dmc.Grid( children=[ - dmc.Group( - [ - dmc.Title("Variable:", order=5), - dmc.Stack( + dmc.GridCol( + dmc.Group( + [ + dmc.Title("Variable:", order=5), dropdown( id=ElementIds.SEC2_VAR_DROPDOWN, options=explore_dropdown_names, value=Variables.RH.col_name, ), - flex=1, - ), - ], - align="flex-start", + ], + align="flex-start", + ), + span=4, ), - dmc.Stack( - [ - dmc.Button( - "Apply filter", - id=ElementIds.SEC2_DATA_FILTER_INPUT, - color="blue", - ), - dmc.Group( - [ - dmc.Title("Filter Variable:", order=5), - dmc.Stack( + dmc.GridCol( + dmc.Stack( + children=[ + dmc.Button( + "Apply filter", + id=ElementIds.SEC2_DATA_FILTER_INPUT, + color="blue", + w="50%", + ), + dmc.Group( + [ + dmc.Title("Filter Variable:", order=5), dropdown( id=ElementIds.SEC2_DATA_FILTER_VAR, options=explore_dropdown_names, value=Variables.RH.col_name, ), - flex=1, - ), - ], - ), - dmc.Group( - [ - dmc.Title("Min Value:", order=5), - dmc.Stack( + ], + ), + dmc.Group( + [ + dmc.Title("Min Value:", order=5), dmc.NumberInput( id=ElementIds.SEC2_MIN_VAL, placeholder="Enter a number for the min val", value=0, ), - flex=1, - ), - ], - ), - dmc.Group( - [ - dmc.Title("Max Value:", order=5), - dmc.Stack( + ], + ), + dmc.Group( + [ + dmc.Title("Max Value:", order=5), dmc.NumberInput( id=ElementIds.SEC2_MAX_VAL, placeholder="Enter a number for the max val", value=100, ), - flex=1, - ), - ], - ), - ], + ], + ), + ], + ), + span=8, ), ], ), @@ -246,99 +229,87 @@ def section_two(): def section_three_inputs(): - return dmc.SimpleGrid( - cols=2, + return dmc.Grid( children=[ - dmc.Stack( - [ - dmc.Group( - [ - dmc.Title("X Variable:", order=5), - dmc.Stack( + dmc.GridCol( + dmc.Stack( + [ + dmc.Group( + [ + dmc.Title("X Variable:", order=5), dropdown( id=ElementIds.EXPLORER_SEC3_VAR_X_DROPDOWN, options=explore_dropdown_names, value="DBT", ), - flex=1, - ), - ], - ), - dmc.Group( - [ - dmc.Title("Y Variable:", order=5), - dmc.Stack( + ], + ), + dmc.Group( + [ + dmc.Title("Y Variable:", order=5), dropdown( id=ElementIds.EXPLORER_SEC3_VAR_Y_DROPDOWN, options=explore_dropdown_names, value=Variables.RH.col_name, ), - flex=1, - ), - ], - ), - dmc.Group( - [ - dmc.Title("Color By:", order=5), - dmc.Stack( + ], + ), + dmc.Group( + [ + dmc.Title("Color By:", order=5), dropdown( id=ElementIds.EXPLORER_SEC3_COLORBY_DROPDOWN, options=explore_dropdown_names, value="glob_hor_rad", ), - flex=1, - ), - ], - ), - ], + ], + ), + ], + ), + span=4, ), - dmc.Stack( - [ - dmc.Button( - "Apply filter", - id=ElementIds.EXPLORER_SEC3_DATA_FILTER_INPUT, - color="blue", - ), - dmc.Group( - [ - dmc.Title("Filter Variable:", order=5), - dmc.Stack( + dmc.GridCol( + dmc.Stack( + [ + dmc.Button( + "Apply filter", + id=ElementIds.EXPLORER_SEC3_DATA_FILTER_INPUT, + color="blue", + w="45%", + ), + dmc.Group( + [ + dmc.Title("Filter Variable:", order=5), dropdown( id=ElementIds.EXPLORER_SEC3_FILTER_VAR_DROPDOWN, options=explore_dropdown_names, value=Variables.RH.col_name, ), - flex=1, - ), - ], - ), - dmc.Group( - [ - dmc.Title("Min Value:", order=5), - dmc.Stack( + ], + ), + dmc.Group( + [ + dmc.Title("Min Value:", order=5), dmc.NumberInput( id=ElementIds.EXPLORER_SEC3_MIN_VAL, placeholder="Enter a number for the min val", value=0, ), - flex=1, - ), - ], - ), - dmc.Group( - [ - dmc.Title("Max Value:", order=5), - dmc.Stack( + ], + ), + dmc.Group( + [ + dmc.Title("Max Value:", order=5), dmc.NumberInput( id=ElementIds.EXPLORER_SEC3_MAX_VAL, placeholder="Enter a number for the max val", value=100, ), - flex=1, - ), - ], - ), - ], + ], + ), + ], + ), + span=8, ), ], ) diff --git a/pages/lib/utils.py b/pages/lib/utils.py index 5628998..5bd8cf1 100644 --- a/pages/lib/utils.py +++ b/pages/lib/utils.py @@ -319,6 +319,7 @@ def dropdown(options=None, **kwargs): return dcc.Dropdown( options=[{"label": k, "value": v} for k, v in options.items()], clearable=False, + style={"width": "14rem"}, **kwargs, ) diff --git a/pages/natural_ventilation.py b/pages/natural_ventilation.py index b2e2451..ac985ad 100644 --- a/pages/natural_ventilation.py +++ b/pages/natural_ventilation.py @@ -97,89 +97,87 @@ def update_layout(si_ip): def inputs_tab(t_min, t_max, d_set): - return dmc.SimpleGrid( - cols=2, - spacing="md", + return dmc.Grid( children=[ - dmc.Stack( - [ - dmc.Button( - "Apply filter", - color="blue", - id=ElementIds.NV_DBT_FILTER, - variant="link", - n_clicks=1, - ), - dmc.Title("Outdoor dry-bulb air temperature range", order=5), - dmc.Group( - [ - dmc.Title("Min Value:", order=5), - dmc.Stack( + dmc.GridCol( + dmc.Stack( + [ + dmc.Button( + "Apply filter", + color="blue", + id=ElementIds.NV_DBT_FILTER, + variant="link", + n_clicks=1, + w="80%", + ), + dmc.Title("Outdoor dry-bulb air temperature range", order=5), + dmc.Group( + [ + dmc.Title("Min Value:", order=5), dmc.NumberInput( id=ElementIds.NV_TDB_MIN_VAL, placeholder="Enter a number for the min val", step=1, value=t_min, ), - flex=1, - ), - ], - ), - dmc.Group( - [ - dmc.Title("Max Value:", order=5), - dmc.Stack( + ], + ), + dmc.Group( + [ + dmc.Title("Max Value:", order=5), dmc.NumberInput( id=ElementIds.NV_TDB_MAX_VAL, placeholder="Enter a number for the max val", value=t_max, step=1, ), - flex=1, - ), - ], - ), - ] + ], + ), + ] + ), + span=4, ), - dmc.Stack( - [ - dmc.Button( - "Apply filter", - color="blue", - id=ElementIds.NV_DPT_FILTER, - variant="link", - disabled=True, - ), - dmc.CheckboxGroup( - id=ElementIds.ENABLE_CONDENSATION, - value=[], - children=[ - dmc.Checkbox( - label=( - "Avoid condensation with radiant systems: If the " - "outdoor dew point temperature is below the radiant " - "system surface temperature, the data point is not plot." - ), - value=1, - size="sm", - ) - ], - ), - dmc.Group( - [ - dmc.Title("Surface temperature:", order=5), - dmc.Stack( + dmc.GridCol( + dmc.Stack( + [ + dmc.Button( + "Apply filter", + color="blue", + id=ElementIds.NV_DPT_FILTER, + variant="link", + disabled=True, + w="70%", + ), + dmc.CheckboxGroup( + id=ElementIds.ENABLE_CONDENSATION, + value=[], + children=[ + dmc.Checkbox( + label=( + "Avoid condensation with radiant systems: If the " + "outdoor dew point temperature is below the radiant " + "system surface temperature, the data point is not plot." + ), + value=1, + size="sm", + w="70%", + ) + ], + ), + dmc.Group( + [ + dmc.Title("Surface temperature:", order=5), dmc.NumberInput( id=ElementIds.NV_DPT_MAX_VAL, placeholder="Enter a number for the max val", value=d_set, step=1, ), - flex=1, - ), - ], - ), - ] + ], + ), + ] + ), + span=8, ), ], ) diff --git a/pages/psy-chart.py b/pages/psy-chart.py index fa5fdce..aceddb1 100644 --- a/pages/psy-chart.py +++ b/pages/psy-chart.py @@ -56,13 +56,12 @@ def inputs(): - return dmc.SimpleGrid( - cols=2, + return dmc.Grid( children=[ - dmc.Group( - [ - dmc.Title("Color By:", order=5), - dmc.Stack( + dmc.GridCol( + dmc.Group( + [ + dmc.Title("Color By:", order=5), dropdown( id=ElementIds.PSY_COLOR_BY_DROPDOWN, options=psy_dropdown_names, @@ -70,60 +69,54 @@ def inputs(): persistence=True, persistence_type="session", ), - flex=1, - ), - ], - align="flex-start", + ], + ), + span=4, ), - dmc.Stack( - [ - dmc.Button( - "Apply filter", - id=ElementIds.DATA_FILTER, - color="blue", - ), - dmc.Group( - [ - dmc.Title("Filter Variable:", order=5), - dmc.Stack( + dmc.GridCol( + dmc.Stack( + [ + dmc.Button( + "Apply filter", + id=ElementIds.DATA_FILTER, + color="blue", + w="50%", + ), + dmc.Group( + [ + dmc.Title("Filter Variable:", order=5), dropdown( id=ElementIds.PSY_VAR_DROPDOWN, options=dropdown_names, value=Variables.RH.col_name, ), - flex=1, - ), - ], - ), - dmc.Group( - [ - dmc.Title("Min Value:", order=5), - dmc.Stack( + ], + ), + dmc.Group( + [ + dmc.Title("Min Value:", order=5), dmc.NumberInput( id=ElementIds.PSY_MIN_VAL, placeholder="Enter a number for the min val", value=0, step=1, ), - flex=1, - ), - ], - ), - dmc.Group( - [ - dmc.Title("Max Value:", order=5), - dmc.Stack( + ], + ), + dmc.Group( + [ + dmc.Title("Max Value:", order=5), dmc.NumberInput( id=ElementIds.PSY_MAX_VAL, placeholder="Enter a number for the max val", value=100, step=1, ), - flex=1, - ), - ], - ), - ], + ], + ), + ], + ), + span=8, ), ], ) From 5d21c9b6fa2734498b74dcfef2535beeecd23b7d Mon Sep 17 00:00:00 2001 From: federico Tartarini Date: Tue, 4 Nov 2025 10:01:21 +1100 Subject: [PATCH 14/15] refactor: improve layout and structure of input components in natural ventilation and psy-chart --- pages/natural_ventilation.py | 55 ++++++++++++++++++------------------ pages/psy-chart.py | 39 +++++++++++++------------ 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/pages/natural_ventilation.py b/pages/natural_ventilation.py index ac985ad..79a0e09 100644 --- a/pages/natural_ventilation.py +++ b/pages/natural_ventilation.py @@ -98,18 +98,11 @@ def update_layout(si_ip): def inputs_tab(t_min, t_max, d_set): return dmc.Grid( + justify="center", children=[ dmc.GridCol( dmc.Stack( [ - dmc.Button( - "Apply filter", - color="blue", - id=ElementIds.NV_DBT_FILTER, - variant="link", - n_clicks=1, - w="80%", - ), dmc.Title("Outdoor dry-bulb air temperature range", order=5), dmc.Group( [ @@ -133,20 +126,31 @@ def inputs_tab(t_min, t_max, d_set): ), ], ), + dmc.Button( + "Apply filter", + color="blue", + id=ElementIds.NV_DBT_FILTER, + variant="link", + n_clicks=1, + w="80%", + ), ] ), - span=4, + span={"base": 12, "md": 4}, ), dmc.GridCol( dmc.Stack( [ - dmc.Button( - "Apply filter", - color="blue", - id=ElementIds.NV_DPT_FILTER, - variant="link", - disabled=True, - w="70%", + dmc.Group( + [ + dmc.Title("Surface temperature:", order=5), + dmc.NumberInput( + id=ElementIds.NV_DPT_MAX_VAL, + placeholder="Enter a number for the max val", + value=d_set, + step=1, + ), + ], ), dmc.CheckboxGroup( id=ElementIds.ENABLE_CONDENSATION, @@ -164,20 +168,17 @@ def inputs_tab(t_min, t_max, d_set): ) ], ), - dmc.Group( - [ - dmc.Title("Surface temperature:", order=5), - dmc.NumberInput( - id=ElementIds.NV_DPT_MAX_VAL, - placeholder="Enter a number for the max val", - value=d_set, - step=1, - ), - ], + dmc.Button( + "Apply filter", + color="blue", + id=ElementIds.NV_DPT_FILTER, + variant="link", + disabled=True, + w="70%", ), ] ), - span=8, + span={"base": 12, "md": 5}, ), ], ) diff --git a/pages/psy-chart.py b/pages/psy-chart.py index aceddb1..3712f20 100644 --- a/pages/psy-chart.py +++ b/pages/psy-chart.py @@ -57,31 +57,25 @@ def inputs(): return dmc.Grid( + justify='center', children=[ dmc.GridCol( - dmc.Group( - [ - dmc.Title("Color By:", order=5), - dropdown( - id=ElementIds.PSY_COLOR_BY_DROPDOWN, - options=psy_dropdown_names, - value="Frequency", - persistence=True, - persistence_type="session", - ), - ], - ), - span=4, + [ + dmc.Title("Color By:", order=5), + dropdown( + id=ElementIds.PSY_COLOR_BY_DROPDOWN, + options=psy_dropdown_names, + value="Frequency", + persistence=True, + persistence_type="session", + ), + ], + span={"base": 12, "md": 4}, ), dmc.GridCol( dmc.Stack( [ - dmc.Button( - "Apply filter", - id=ElementIds.DATA_FILTER, - color="blue", - w="50%", - ), + dmc.Group( [ dmc.Title("Filter Variable:", order=5), @@ -113,10 +107,15 @@ def inputs(): step=1, ), ], + ),dmc.Button( + "Apply filter", + id=ElementIds.DATA_FILTER, + color="blue", + w="50%", ), ], ), - span=8, + span={"base": 12, "md": 4}, ), ], ) From 9f5db0e9eac62b9e7be84bcfa91789abf40ffb42 Mon Sep 17 00:00:00 2001 From: federico Tartarini Date: Tue, 4 Nov 2025 11:31:02 +1100 Subject: [PATCH 15/15] refactor: replace dcc.Loading with dmc.Skeleton for improved loading indicators --- pages/explorer.py | 12 ++++++-- pages/lib/global_element_ids.py | 1 - pages/lib/layout.py | 12 ++++---- pages/natural_ventilation.py | 19 +++++++++---- pages/outdoor.py | 35 +++++++++-------------- pages/psy-chart.py | 21 ++++++-------- pages/select.py | 3 +- pages/summary.py | 29 +++++++++++++------ pages/sun.py | 49 ++++++++++++++++++--------------- pages/t_rh.py | 21 +++++++++----- pages/wind.py | 15 ++++++---- 11 files changed, 120 insertions(+), 97 deletions(-) diff --git a/pages/explorer.py b/pages/explorer.py index 63c30ff..b23017b 100644 --- a/pages/explorer.py +++ b/pages/explorer.py @@ -93,19 +93,25 @@ def section_one(): id_button=IdButtons.EXPLORE_YEARLY_CHART_LABEL, doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, ), - dcc.Loading(type="circle", children=dmc.Paper(id=ElementIds.YEARLY_EXPLORE)), + dmc.Skeleton( + visible=False, h=450, children=dmc.Paper(id=ElementIds.YEARLY_EXPLORE) + ), title_with_link( text="Daily chart", id_button=IdButtons.EXPLORE_DAILY_CHART_LABEL, doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, ), - dcc.Loading(type="circle", children=dmc.Paper(id=ElementIds.QUERY_DAILY)), + dmc.Skeleton( + visible=False, h=450, children=dmc.Paper(id=ElementIds.QUERY_DAILY) + ), title_with_link( text="Heatmap chart", id_button=IdButtons.EXPLORE_HEATMAP_CHART_LABEL, doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, ), - dcc.Loading(type="circle", children=dmc.Paper(id=ElementIds.QUERY_HEATMAP)), + dmc.Skeleton( + visible=False, h=450, children=dmc.Paper(id=ElementIds.QUERY_HEATMAP) + ), title_with_tooltip( text="Descriptive statistics", tooltip_text="count, mean, std, min, max, and percentiles", diff --git a/pages/lib/global_element_ids.py b/pages/lib/global_element_ids.py index 335748a..69d7001 100644 --- a/pages/lib/global_element_ids.py +++ b/pages/lib/global_element_ids.py @@ -150,7 +150,6 @@ class ElementIds(str, Enum): ID_WIND_META_STORE = "meta-store" ID_WIND_SI_IP_UNIT_STORE = "si-ip-unit-store" ID_WIND_GLOBAL_LOCAL_RADIO_INPUT = "global-local-radio-input" - LOADING_ONE = "loading-1" UPLOAD_DATA = "upload-data" UPLOAD_DATA_BUTTON = "upload-data-button" TAB_ONE_MAP = "tab-one-map" diff --git a/pages/lib/layout.py b/pages/lib/layout.py index feb62a2..07b5cf5 100644 --- a/pages/lib/layout.py +++ b/pages/lib/layout.py @@ -426,13 +426,11 @@ def create_collapsible_layout(): ), # including main and footer dmc.AppShellMain( - dmc.ScrollArea( - children=[ - create_stores(), - dash.page_container, - create_footer(), - ], - ), + children=[ + create_stores(), + dash.page_container, + create_footer(), + ], pos="relative", style={ "zIndex": 1, diff --git a/pages/natural_ventilation.py b/pages/natural_ventilation.py index 79a0e09..b1ab38f 100644 --- a/pages/natural_ventilation.py +++ b/pages/natural_ventilation.py @@ -37,7 +37,14 @@ def layout(): - return dmc.Stack(p="md", id=ElementIds.MAIN_NV_SECTION) + return dmc.Stack( + p="md", + children=dmc.Skeleton( # needed to avoid empty layout on load + visible=True, + height="100vh", + ), + id=ElementIds.MAIN_NV_SECTION, + ) @callback( @@ -61,8 +68,9 @@ def update_layout(si_ip): doc_link=DocLinks.NATURAL_VENTILATION, ), inputs_tab(tdb_set_min, tdb_set_max, dpt_set), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Paper( id=ElementIds.NV_HEATMAP_CHART, ), @@ -87,8 +95,9 @@ def update_layout(si_ip): ), ], ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Paper( id=ElementIds.NV_BAR_CHART, ), diff --git a/pages/outdoor.py b/pages/outdoor.py index 816c5e5..8f2ec39 100644 --- a/pages/outdoor.py +++ b/pages/outdoor.py @@ -63,8 +63,9 @@ def outdoor_comfort_chart(): id_button=IdButtons.UTCI_CHARTS_LABEL, doc_link=DocLinks.UTCI_CHART, ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Paper( id=ElementIds.UTCI_HEATMAP, ), @@ -74,8 +75,9 @@ def outdoor_comfort_chart(): id_button=IdButtons.UTCI_CHARTS_LABEL, doc_link=DocLinks.UTCI_CHART, ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Paper( id=ElementIds.UTCI_CATEGORY_HEATMAP, ), @@ -98,11 +100,11 @@ def outdoor_comfort_chart(): ), ], ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Paper( id=ElementIds.UTCI_SUMMARY_CHART, - # p="sm", ), ), ], @@ -113,27 +115,16 @@ def layout(): return dmc.Stack( p="md", children=[ - dcc.Loading( - type="circle", - children=dmc.Stack( - children=[ - inputs_outdoor_comfort(), - outdoor_comfort_chart(), - ], - ), - ), + inputs_outdoor_comfort(), + outdoor_comfort_chart(), ], ) @callback( Output(ElementIds.OUTDOOR_COMFORT_OUTPUT, "children"), - [ - Input(ElementIds.ID_OUTDOOR_DF_STORE, "modified_timestamp"), - ], - [ - State(ElementIds.ID_OUTDOOR_DF_STORE, "data"), - ], + Input(ElementIds.ID_OUTDOOR_DF_STORE, "modified_timestamp"), + State(ElementIds.ID_OUTDOOR_DF_STORE, "data"), ) def update_outdoor_comfort_output(_, df): """ diff --git a/pages/psy-chart.py b/pages/psy-chart.py index 3712f20..7f94a25 100644 --- a/pages/psy-chart.py +++ b/pages/psy-chart.py @@ -57,7 +57,7 @@ def inputs(): return dmc.Grid( - justify='center', + justify="center", children=[ dmc.GridCol( [ @@ -75,7 +75,6 @@ def inputs(): dmc.GridCol( dmc.Stack( [ - dmc.Group( [ dmc.Title("Filter Variable:", order=5), @@ -107,7 +106,8 @@ def inputs(): step=1, ), ], - ),dmc.Button( + ), + dmc.Button( "Apply filter", id=ElementIds.DATA_FILTER, color="blue", @@ -130,16 +130,11 @@ def layout(): id_button=IdButtons.PSYCHROMETRIC_CHART_CHART, doc_link=DocLinks.PSYCHROMETRIC_CHART, ), - dcc.Loading( - type="circle", - children=dmc.Stack( - children=[ - inputs(), - dmc.Paper( - id=ElementIds.PSYCH_CHART, - ), - ], - ), + inputs(), + dmc.Skeleton( + visible=False, + h=450, + id=ElementIds.PSYCH_CHART, ), ], ) diff --git a/pages/select.py b/pages/select.py index 778e4b0..2b61da8 100644 --- a/pages/select.py +++ b/pages/select.py @@ -41,8 +41,7 @@ def layout(): p="md", children=[ dcc.Loading( - id=ElementIds.LOADING_ONE, - type="circle", + custom_spinner=dmc.Skeleton(visible=True, h="100%"), fullscreen=True, children=alert(), ), diff --git a/pages/summary.py b/pages/summary.py index 82119e3..84d2064 100644 --- a/pages/summary.py +++ b/pages/summary.py @@ -35,7 +35,14 @@ def layout(): """Contents in the second tab 'Climate Summary'.""" - return dmc.Stack(id=ElementIds.TAB_TWO_CONTAINER, p="md") + return dmc.Stack( + id=ElementIds.TAB_TWO_CONTAINER, + p="md", + children=dmc.Skeleton( # needed to avoid empty layout on load + visible=True, + height="100vh", + ), + ) @callback( @@ -53,15 +60,18 @@ def update_layout(si_ip): return dmc.Stack( id=ElementIds.SUMMARY_SCE1_CONTAINER, children=[ - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, children=dmc.Stack( id=ElementIds.LOCATION_INFO, + children=[dmc.Text("info")] + * 10, # placeholder text for height calc gap=0, ), ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=300, children=dmc.Stack(id=ElementIds.WORLD_MAP), ), title_with_tooltip( @@ -69,8 +79,8 @@ def update_layout(si_ip): id_button=IdButtons.DOWNLOAD_BUTTON_LABEL, tooltip_text="Use the following buttons to download either the Clima sourcefile or the EPW file", ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, children=dmc.Group( children=[ dmc.Button( @@ -127,8 +137,9 @@ def update_layout(si_ip): ), ], ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Stack(id=ElementIds.DEGREE_DAYS_CHART_WRAPPER), ), title_with_link( diff --git a/pages/sun.py b/pages/sun.py index 67f519f..650fe97 100644 --- a/pages/sun.py +++ b/pages/sun.py @@ -57,7 +57,14 @@ def layout(): return dmc.Stack( p="md", id=ElementIds.TAB_FOUR_CONTAINER, - children=[sun_path(), static_section(), explore_daily_heatmap()], + children=[ + sun_path(), + dmc.Stack( + id=ElementIds.STATIC_SECTION, + w="100%", + ), + explore_daily_heatmap(), + ], ) @@ -97,10 +104,11 @@ def sun_path(): ), ], ), - dmc.Center( - dcc.Loading( - type="circle", - children=dmc.Stack(id=ElementIds.CUSTOM_SUNPATH, w="100%"), + dmc.Skeleton( + visible=False, + h=450, + children=dmc.Center( + id=ElementIds.CUSTOM_SUNPATH, ), ), ], @@ -129,25 +137,20 @@ def explore_daily_heatmap(): ), ], ), - dcc.Loading(type="circle", children=dmc.Stack(id=ElementIds.SUN_DAILY)), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=520, + children=dmc.Stack(id=ElementIds.SUN_DAILY), + ), + dmc.Skeleton( + visible=False, + h=520, children=dmc.Stack(id=ElementIds.SUN_HEATMAP), ), ], ) -def static_section(): - return dmc.Stack( - id=ElementIds.STATIC_SECTION, - w="100%", - children=[ - # ... - ], - ) - - @callback( Output(ElementIds.STATIC_SECTION, "children"), [Input(ElementIds.ID_SUN_SI_IP_RADIO_INPUT, "value")], @@ -162,8 +165,9 @@ def update_static_section(si_ip): id_button=IdButtons.MONTHLY_CHART_LABEL, doc_link=DocLinks.SOLAR_RADIATION, ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=520, children=dmc.Stack(id=ElementIds.MONTHLY_SOLAR), ), title_with_link( @@ -171,8 +175,9 @@ def update_static_section(si_ip): id_button=IdButtons.CLOUD_CHART_LABEL, doc_link=DocLinks.CLOUD_COVER, ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=520, children=dmc.Stack(id=ElementIds.CLOUD_COVER), ), ] diff --git a/pages/t_rh.py b/pages/t_rh.py index 5b43a5b..24f4dc6 100644 --- a/pages/t_rh.py +++ b/pages/t_rh.py @@ -51,8 +51,9 @@ def layout(): id_button=IdButtons.YEARLY_CHART_LABEL, doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Stack(id=ElementIds.YEARLY_CHART), ), # Daily chart @@ -61,8 +62,9 @@ def layout(): id_button=IdButtons.DAILY_CHART_LABEL, doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Stack(id=ElementIds.DAILY), ), # Heatmap chart @@ -71,8 +73,9 @@ def layout(): id_button=IdButtons.HEATMAP_CHART_LABEL, doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Stack(id=ElementIds.HEATMAP), ), # Descriptive statistics @@ -81,7 +84,11 @@ def layout(): tooltip_text="count, mean, std, min, max, and percentiles", id_button=IdButtons.TABLE_TMP_RH, ), - dmc.Stack(id=ElementIds.TABLE_TMP_HUM), + dmc.Skeleton( + visible=False, + h=450, + children=dmc.Stack(id=ElementIds.TABLE_TMP_HUM), + ), ], ) diff --git a/pages/wind.py b/pages/wind.py index 95d75bc..67f33fb 100644 --- a/pages/wind.py +++ b/pages/wind.py @@ -208,16 +208,19 @@ def layout(): id_button=IdButtons.WIND_ROSE_LABEL, doc_link=DocLinks.WIND_ROSE, ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Stack(id=ElementIds.WIND_ROSE), ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Stack(id=ElementIds.WIND_SPEED), ), - dcc.Loading( - type="circle", + dmc.Skeleton( + visible=False, + h=450, children=dmc.Stack(id=ElementIds.WIND_DIRECTION), ), seasonal_wind_rose(),