diff --git a/assets/banner.css b/assets/banner.css
deleted file mode 100644
index edba1bc..0000000
--- a/assets/banner.css
+++ /dev/null
@@ -1,19 +0,0 @@
-/**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;
-}
-
-#banner-subtitle {
- font-family: 'Poppins', sans-serif;
- font-weight: 400;
- max-height: 25px;
-}
\ No newline at end of file
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
-}
-
diff --git a/docs/README.md b/docs/README.md
index 05ce6e2..84cb022 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: 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..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
@@ -77,6 +83,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
@@ -96,17 +103,17 @@ 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:
+### 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.
-```bash
-pipenv install black
-```
-
-Format your code before committing:
-
-```bash
-black .
-```
+### UI Modifications
+For UI-related changes:
+* 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
@@ -115,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
@@ -172,6 +177,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
diff --git a/pages/explorer.py b/pages/explorer.py
index 2614d10..b23017b 100644
--- a/pages/explorer.py
+++ b/pages/explorer.py
@@ -93,90 +93,30 @@ 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",
id_button=IdButtons.TABLE_EXPLORE,
),
- dmc.Center(
- children=dmc.Box(
- 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=[],
- ),
- ],
- ),
- ],
- ),
- )
- ),
# Results table
dmc.Paper(id=ElementIds.TABLE_DATA_EXPLORER, p="sm"),
]
@@ -192,134 +132,64 @@ def section_two_inputs():
tooltip_text=None,
id_button=IdButtons.CUSTOM_HEATMAP_CHART_LABEL,
),
- dmc.SimpleGrid(
- cols=3,
- 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",
- ),
- 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=[],
- ),
- ],
- ),
- ],
+ ],
+ 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,
),
],
),
@@ -330,7 +200,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(
@@ -365,152 +235,87 @@ def section_two():
def section_three_inputs():
- return dmc.SimpleGrid(
- cols=3,
+ 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.TAB6_SEC3_VAR_X_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.TAB6_SEC3_VAR_Y_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.TAB6_SEC3_COLORBY_DROPDOWN,
+ id=ElementIds.EXPLORER_SEC3_COLORBY_DROPDOWN,
options=explore_dropdown_names,
value="glob_hor_rad",
),
- flex=1,
- ),
- ],
- ),
- ],
- ),
- 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=[],
- ),
- ],
- ),
- ],
+ ],
+ ),
+ ],
+ ),
+ span=4,
),
- dmc.Stack(
- [
- dmc.Button(
- "Apply filter",
- id=ElementIds.TAB6_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.TAB6_SEC3_FILTER_VAR_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.TAB6_SEC3_MIN_VAL,
+ 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.TAB6_SEC3_MAX_VAL,
+ id=ElementIds.EXPLORER_SEC3_MAX_VAL,
placeholder="Enter a number for the max val",
value=100,
),
- flex=1,
- ),
- ],
- ),
- ],
+ ],
+ ),
+ ],
+ ),
+ span=8,
),
],
)
@@ -549,6 +354,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 +362,22 @@ 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 +402,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 +410,13 @@ 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 +435,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 +443,14 @@ 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 +473,60 @@ 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,
+ )
- start_month, end_month, start_hour, end_hour = determine_month_and_hour_filter(
- month, hour, invert_month, invert_hour
- )
+ 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
+
+ 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 +592,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 +613,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 +628,13 @@ 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 +687,31 @@ 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..0d7f840 100644
--- a/pages/lib/charts_data_explorer.py
+++ b/pages/lib/charts_data_explorer.py
@@ -60,34 +60,101 @@ 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(
- 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 = 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"
+ 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_values,
+ 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",
+ )
+ )
+
+ 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=base_values,
+ 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],
+ 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/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..69d7001 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"
@@ -155,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"
@@ -170,11 +164,11 @@ 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"
- 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"
@@ -203,8 +197,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"
@@ -233,3 +225,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"
diff --git a/pages/lib/layout.py b/pages/lib/layout.py
index 90c0f49..07b5cf5 100644
--- a/pages/lib/layout.py
+++ b/pages/lib/layout.py
@@ -5,6 +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,
+ get_default_global_filter_store_data,
+ get_global_filter_state,
+)
class NavBarIcons:
@@ -38,6 +43,82 @@ def get_icon(cls, page_name):
return cls._ICON_MAP.get(page_name, "tabler:circle")
+# global filters
+def create_tools_filter_components():
+ # Apply month and hour filter (reduced nesting, same visual layout)
+ return dmc.Stack(
+ id=ElementIds.TOOLS_MONTH_HOUR_SECTION,
+ children=[
+ 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",
+ ),
+ # Month controls
+ 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.Switch(
+ id=ElementIds.TOOLS_INVERT_MONTH,
+ label="Invert",
+ checked=False,
+ size="xs",
+ color="blue",
+ style={"fontSize": "0.7rem"},
+ ),
+ ],
+ justify="flex-end",
+ ),
+ # Hour controls
+ 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.Switch(
+ id=ElementIds.TOOLS_INVERT_HOUR,
+ label="Invert",
+ checked=False,
+ size="xs",
+ color="blue",
+ style={"fontSize": "0.7rem"},
+ ),
+ ],
+ justify="flex-end",
+ ),
+ ],
+ gap="xs",
+ p="xs",
+ )
+
+
def create_navbar():
nav_link_styles = {
"root": {
@@ -69,7 +150,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 +219,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,
@@ -179,16 +262,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",
),
@@ -317,6 +398,11 @@ def create_stores():
dcc.Store(
id=ElementIds.TOOLS_MENU_EXPANDED, data=False, storage_type="session"
),
+ dcc.Store(
+ id=ElementIds.TOOLS_GLOBAL_FILTER_STORE,
+ data=get_default_global_filter_store_data(),
+ storage_type="session",
+ ),
dcc.Interval(
id=ElementIds.ID_LAYOUT_INTERVAL_COMPONENT,
interval=12 * 1000,
@@ -340,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,
@@ -406,7 +490,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 +499,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 +510,119 @@ 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 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)
+
+ 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]
+
+ 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
+ )
+
+ 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.HOUR.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):
+ state = get_global_filter_state(global_filter_data)
+ return (
+ 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 1fb189d..b6ab5b5 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,31 +412,103 @@ 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(
- 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=var_unit),
+ 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_values = df[original_col].copy()
+ else:
+ filtered_values = df[var].copy()
+
+ 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_values,
+ 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",
+ )
+ )
+
+ 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=base_values,
+ 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,
+ 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)
@@ -473,30 +553,98 @@ 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(
- 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 = 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_values = df[original_col].copy()
+ else:
+ filtered_values = df[var].copy()
+
+ 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_values,
+ 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",
+ )
+ )
+
+ 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=base_values,
+ 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],
+ 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)
@@ -529,35 +677,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 +933,15 @@ 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 +987,39 @@ 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()
- trace1 = go.Bar(
- x=list(range(0, 13)), y=month_in, name="IN range", marker_color=color_in
- )
+
+ month_names = month_lst
+
+ trace1 = go.Bar(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..5bd8cf1 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)
@@ -268,14 +299,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
@@ -307,6 +334,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 a2485bb..b1ab38f 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
@@ -38,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(
@@ -62,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,
),
@@ -88,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,
),
@@ -98,140 +106,88 @@ def update_layout(si_ip):
def inputs_tab(t_min, t_max, d_set):
- return dmc.SimpleGrid(
- cols=3,
- spacing="md",
+ return dmc.Grid(
+ justify="center",
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.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,
- ),
- ],
- ),
- ]
- ),
- 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.Button(
+ "Apply filter",
+ color="blue",
+ id=ElementIds.NV_DBT_FILTER,
+ variant="link",
+ n_clicks=1,
+ w="80%",
+ ),
+ ]
+ ),
+ span={"base": 12, "md": 4},
),
- dmc.Stack(
- [
- dmc.Button(
- "Apply filter",
- color="blue",
- id=ElementIds.NV_DPT_FILTER,
- variant="link",
- disabled=True,
- ),
- dcc.Checklist(
- options=[
- {
- "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=[],
- id=ElementIds.ENABLE_CONDENSATION,
- ),
- dmc.Group(
- [
- dmc.Title("Surface temperature:", order=5),
- dmc.Stack(
+ dmc.GridCol(
+ dmc.Stack(
+ [
+ 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,
- ),
- ],
- ),
- ]
+ ],
+ ),
+ 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.Button(
+ "Apply filter",
+ color="blue",
+ id=ElementIds.NV_DPT_FILTER,
+ variant="link",
+ disabled=True,
+ w="70%",
+ ),
+ ]
+ ),
+ span={"base": 12, "md": 5},
),
],
)
@@ -241,41 +197,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 +231,28 @@ 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,10 +278,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 +301,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 +380,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 +431,28 @@ 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 +529,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..8f2ec39 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(
@@ -36,97 +37,35 @@
def inputs_outdoor_comfort():
- return dmc.SimpleGrid(
- cols=2,
- children=[
- dmc.Group(
- [
- dmc.Title("Select a scenario:", order=5),
- dmc.Stack(
- dropdown(
- id=ElementIds.TAB7_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",
- ),
- 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=[],
- ),
- ],
- ),
- ],
+ 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(
- id=ElementIds.OUTDOOR_COMFORT_OUTPUT,
- ),
+ dmc.Title(id=ElementIds.OUTDOOR_COMFORT_OUTPUT, order=4),
title_with_link(
text="UTCI heatmap 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,
),
@@ -136,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,
),
@@ -160,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",
),
),
],
@@ -175,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):
"""
@@ -230,40 +159,57 @@ 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
+
+ df = apply_global_month_hour_filter(df, global_filter_data, var)
+
+ # 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)
return dcc.Graph(
@@ -285,7 +231,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 +250,36 @@ 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
+
+ df = apply_global_month_hour_filter(
+ df, global_filter_data, [var, var + "_categories"]
+ )
+
+ 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,
var + "_categories",
@@ -343,7 +292,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 +326,27 @@ 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
-):
+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
+
+ df = apply_global_month_hour_filter(df, global_filter_data, var)
+
+ # 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,
var + "_categories",
diff --git a/pages/psy-chart.py b/pages/psy-chart.py
index b17969a..7f94a25 100644
--- a/pages/psy-chart.py
+++ b/pages/psy-chart.py
@@ -56,127 +56,66 @@
def inputs():
- return dmc.SimpleGrid(
- cols=3,
+ return dmc.Grid(
+ justify="center",
children=[
- dmc.Group(
+ dmc.GridCol(
[
dmc.Title("Color By:", order=5),
- dmc.Stack(
- dropdown(
- id=ElementIds.PSY_COLOR_BY_DROPDOWN,
- options=psy_dropdown_names,
- value="Frequency",
- persistence=True,
- persistence_type="session",
- ),
- flex=1,
+ dropdown(
+ id=ElementIds.PSY_COLOR_BY_DROPDOWN,
+ options=psy_dropdown_names,
+ value="Frequency",
+ persistence=True,
+ persistence_type="session",
),
],
- align="flex-start",
+ span={"base": 12, "md": 4},
),
- 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(
- "Apply filter",
- id=ElementIds.DATA_FILTER,
- color="blue",
- ),
- dmc.Group(
- [
- dmc.Title("Filter Variable:", order=5),
- dmc.Stack(
+ dmc.GridCol(
+ dmc.Stack(
+ [
+ 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,
- ),
- ],
- ),
- ],
+ ],
+ ),
+ dmc.Button(
+ "Apply filter",
+ id=ElementIds.DATA_FILTER,
+ color="blue",
+ w="50%",
+ ),
+ ],
+ ),
+ span={"base": 12, "md": 4},
),
],
)
@@ -191,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,
),
],
)
@@ -211,47 +145,55 @@ 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 = filter_df_by_month_and_hour(
- df, time_filter, month, hour, invert_month, invert_hour, df.columns
- )
+ 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
+
+ # 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..2b61da8 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__,
@@ -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(),
),
@@ -125,6 +124,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 +157,7 @@ def submitted_data(
True,
messages_alert["not_available"],
"orange",
+ get_default_global_filter_store_data(),
)
location_info = get_location_info(
lines, url_store
@@ -167,6 +168,7 @@ def submitted_data(
True,
messages_alert["success"],
"green",
+ get_default_global_filter_store_data(),
)
elif (
@@ -191,6 +193,7 @@ def submitted_data(
True,
messages_alert["success"],
"green",
+ get_default_global_filter_store_data(),
)
else:
return (
@@ -199,6 +202,7 @@ def submitted_data(
True,
messages_alert["invalid_format"],
"orange",
+ get_default_global_filter_store_data(),
)
except (ValueError, IndexError, KeyError) as e:
print(f"Error parsing EPW file: {e}")
@@ -208,6 +212,7 @@ def submitted_data(
True,
messages_alert["wrong_extension"],
"orange",
+ get_default_global_filter_store_data(),
)
raise PreventUpdate
@@ -252,7 +257,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 +278,6 @@ def enable_tabs_when_data_is_loaded(meta, data):
True,
True,
True,
- True, # changelog always disabled
default,
)
else:
@@ -288,7 +291,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..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(
@@ -51,17 +58,20 @@ 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",
+ 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(
@@ -273,6 +284,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 +295,9 @@ 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 +306,14 @@ 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
@@ -373,6 +395,7 @@ def degree_day_chart(ts, n_clicks, df, meta, hdd_value, cdd_value, 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"),
@@ -380,7 +403,14 @@ def degree_day_chart(ts, n_clicks, df, meta, hdd_value, cdd_value, si_ip):
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,
@@ -394,6 +424,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"),
@@ -401,8 +432,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,
@@ -416,6 +453,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"),
@@ -423,8 +461,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,
@@ -438,6 +482,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"),
@@ -445,8 +490,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,
diff --git a/pages/sun.py b/pages/sun.py
index 3999dac..650fe97 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 (
@@ -59,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(),
+ ],
)
@@ -99,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,
),
),
],
@@ -125,27 +131,22 @@ 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.TAB4_HEATMAP),
+ 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=[
- # ...
],
)
@@ -164,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(
@@ -173,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),
),
]
@@ -187,6 +190,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 +198,25 @@ 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 +230,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 +252,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 +260,28 @@ 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 +303,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 +316,13 @@ 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 +333,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 +346,12 @@ 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..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),
+ ),
],
)
@@ -92,6 +99,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 +107,21 @@ 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 +148,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 +156,13 @@ 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 +213,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 +221,46 @@ 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.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.RH.col_name,
- Variables.HOUR.col_name,
- Variables.UTC_TIME.col_name,
- Variables.MONTH_NAMES.col_name,
- Variables.DAY.col_name,
- ]
- ],
+ df[[Variables.RH.col_name] + base_columns],
Variables.RH.col_name,
global_local,
si_ip,
@@ -240,14 +273,24 @@ 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..67f33fb 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,
)
@@ -144,7 +141,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,104 +198,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():
"""Contents in the fifth tab 'Wind'."""
return dmc.Stack(
@@ -309,36 +208,60 @@ 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(),
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,
+ )
+
+ 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 +274,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 +282,14 @@ 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,30 +300,10 @@ 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")],
- [
- 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):
- direction = heatmap(df, Variables.WIND_DIR.col_name, global_local, si_ip)
- units = generate_units(si_ip)
- return dcc.Graph(
- config=generate_chart_name(TabNames.WIND_DIRECTION, meta, units),
- figure=direction,
- )
-
-
-@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"),
+ Input(ElementIds.ID_WIND_GLOBAL_LOCAL_RADIO_INPUT, "value"),
+ Input(ElementIds.TOOLS_GLOBAL_FILTER_STORE, "data"),
],
[
State(ElementIds.ID_WIND_DF_STORE, "data"),
@@ -400,47 +311,19 @@ def update_tab_wind_direction(global_local, df, meta, si_ip):
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)
- ]
+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
- 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
- )
+ 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(
- config=generate_chart_name(
- TabNames.CUSTOM_WIND_ROSE, meta, custom_inputs, units
- ),
- figure=custom,
+ config=generate_chart_name(TabNames.WIND_DIRECTION, meta, units),
+ figure=direction,
)
@@ -571,15 +454,32 @@ 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,
+ )
+
+ 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');