diff --git a/Pipfile b/Pipfile index 9128883..0c01f3d 100644 --- a/Pipfile +++ b/Pipfile @@ -4,12 +4,12 @@ verify_ssl = true name = "pypi" [packages] -dash = "==2.15" +dash = "==3.2" pvlib = "==0.9.1" pythermalcomfort = "==2.9.1" dash-bootstrap-components = "==1.2.0" dash-extensions = "==1.0.7" -dash-mantine-components = "==0.12.1" +dash-mantine-components = "==2.2.1" requests = "==2.32.4" plotly = "==5.18.0" pandas = "==2.2.0" diff --git a/Pipfile.lock b/Pipfile.lock index 15c208f..8e475b6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7214a1158f64483648ecff36a57b61b67020408e0f16770b77d7165a9d6f47e0" + "sha256": "4f09fe1d5fd82b15503fd3b0c1606e5e6139df983488fdc78c72555daf01e167" }, "pipfile-spec": 6, "requires": { @@ -133,22 +133,14 @@ "markers": "python_version >= '3.10'", "version": "==8.2.1" }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==0.4.6" - }, "dash": { "hashes": [ - "sha256:d38891337fc855d5673f75e5346354daa063c4ff45a8a6a21f25e858fcae41c2", - "sha256:df1882bbf613e4ca4372281c8facbeb68e97d76720336b051bf84c75d2de8588" + "sha256:4c1819588d83bed2cbcf5807daa5c2380c8c85789a6935a733f018f04ad8a6a2", + "sha256:93300b9b99498f8b8ed267e61c455b4ee1282c7e4d4b518600eec87ce6ddea55" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==2.15.0" + "markers": "python_version >= '3.8'", + "version": "==3.2.0" }, "dash-bootstrap-components": { "hashes": [ @@ -159,13 +151,6 @@ "markers": "python_version >= '3.6' and python_version < '4'", "version": "==1.2.0" }, - "dash-core-components": { - "hashes": [ - "sha256:52b8e8cce13b18d0802ee3acbc5e888cb1248a04968f962d63d070400af2e346", - "sha256:c6733874af975e552f95a1398a16c2ee7df14ce43fa60bb3718a3c6e0b63ffee" - ], - "version": "==2.0.0" - }, "dash-extensions": { "hashes": [ "sha256:46c4ec7c7d3b42db63032005f7d258435bc0092d0fec6f6ce000be68befeb102", @@ -175,13 +160,6 @@ "markers": "python_version >= '3.8' and python_version < '4'", "version": "==1.0.7" }, - "dash-html-components": { - "hashes": [ - "sha256:8703a601080f02619a6390998e0b3da4a5daabe97a1fd7a9cebc09d015f26e50", - "sha256:b42cc903713c9706af03b3f2548bda4be7307a7cf89b7d6eae3da872717d1b63" - ], - "version": "==2.0.0" - }, "dash-iconify": { "hashes": [ "sha256:564774be6b11b0ac3a8999b7137c3d17a1d351d69b673aa313c7228eacc9d143", @@ -192,18 +170,11 @@ }, "dash-mantine-components": { "hashes": [ - "sha256:2630bca31cb96d96fb2c4f986e639b9f92d6319aba8cba02f76da6c0d8f5ca48", - "sha256:c3dcbfd89813a1539654b8d016eb953dc5f67aafe1a77d45b5ec9faa6f25d3e7" + "sha256:a54acdb8b3e7a80251e9d68947a84b0a1d16285f5ca33363dbc29f287a13ebaa", + "sha256:cd3aefd3191be365db0435067196fe037e3803c996794c174f0f0409c24efac4" ], "index": "pypi", - "version": "==0.12.1" - }, - "dash-table": { - "hashes": [ - "sha256:18624d693d4c8ef2ddec99a6f167593437a7ea0bf153aa20f318c170c5bc7308", - "sha256:19036fa352bb1c11baf38068ec62d172f0515f73ca3276c79dee49b95ddc16c9" - ], - "version": "==5.0.0" + "version": "==2.2.1" }, "dataclass-wizard": { "hashes": [ @@ -635,7 +606,7 @@ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.17.0" }, "tenacity": { @@ -648,11 +619,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", - "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76" + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" ], "markers": "python_version >= '3.9'", - "version": "==4.14.1" + "version": "==4.15.0" }, "tzdata": { "hashes": [ @@ -672,11 +643,11 @@ }, "werkzeug": { "hashes": [ - "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17", - "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d" + "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", + "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" ], - "markers": "python_version >= '3.8'", - "version": "==3.0.6" + "markers": "python_version >= '3.9'", + "version": "==3.1.3" }, "zipp": { "hashes": [ diff --git a/assets/construction.css b/assets/construction.css deleted file mode 100644 index 930d108..0000000 --- a/assets/construction.css +++ /dev/null @@ -1,5 +0,0 @@ -#construction-container { - height: 80vh; - align-items: center; - margin-top: 20px; -} \ No newline at end of file diff --git a/assets/footer.css b/assets/footer.css deleted file mode 100644 index b334dcd..0000000 --- a/assets/footer.css +++ /dev/null @@ -1,18 +0,0 @@ -#footer-container { - padding: 1rem; - margin:0; - color: white; - background-color: #003262; -} - -a { - color: white; - text-decoration: underline; -} - -a:hover { - color: lightgrey; -} - - - diff --git a/assets/layout.css b/assets/layout.css index e9089db..42ed6cb 100644 --- a/assets/layout.css +++ b/assets/layout.css @@ -29,4 +29,3 @@ width: 100%; //or any percentage width you want } - diff --git a/assets/tabs.css b/assets/tabs.css deleted file mode 100644 index 15590df..0000000 --- a/assets/tabs.css +++ /dev/null @@ -1,288 +0,0 @@ -*[data-dash-is-loading="true"]{ - visibility: hidden; -} -*[data-dash-is-loading="true"]::before{ - content: ""; - display: inline-block; - color: magenta; - visibility: visible; - height: 10rem -} - -/* Tabs */ -#tabs { - margin: 0; -} - -#tabs-content { - padding: 1rem; -} - -#tabs-parent { - padding: 0 1rem; - background-color: #003262 !important; -} - -#store-container { - padding: 0; -} - -#loading-container { - top: 0; -} - -.custom-tabs-container { - width: 85%; -} - -.custom-tabs { - background-color: #f9f9f9; - padding: 0 24px; - border-bottom: 1px solid #d6d6d6; -} - -.custom-tab { - border-color: rgb(238, 236, 236); - border-top-left-radius: 3px; - border-top-right-radius: 3px; - border-top: 3px solid transparent !important; - border-left: 1px solid lightgrey !important; - border-right: 1px solid lightgrey !important; - border-bottom: 1px solid #d6d6d6; - background-color: #f6f8f8; - padding: 12px !important; - font-family: "system-ui"; - display: flex !important; - align-items: center; - justify-content: center; -} - -.custom-tab:has(.active) { - color:#586069; - background-color: white; - box-shadow: 1px 1px 0 white; - border-left: 1px solid lightgrey !important; - border-right: 1px solid lightgrey !important; - border-top: 6px solid #abd2ff !important; - border-bottom: 1px solid transparent; -} - -.nav-pills { - display: flex; - flex-wrap: wrap; -} - -@media (max-width: 900px) { - .nav-pills { - flex-direction: column; - } -} - -.nav-pills .nav-link { - padding: 0; - color: #586069; - font-family: "system-ui"; - background-color: transparent; -} - -.nav-pills .nav-link.disabled { - color: #c8c6c6; -} - -.nav-pills .nav-link.active { - color: black; - background-color: white; -} - -/* Tab One */ -#tab-one-container { - height: 100%; - padding-top: 0; -} - -#tab-one-form-container { - justify-content: space-between; -} - -#store-container { - height: 100%; -} - -#tab-one-map { - margin-top: 28px; - /*height: 100vh;*/ - width: 100%; -} - -#map-credits { - padding-top: 10px; -} - - -/* Tab Two */ -#tab-two-container { - justify-content: space-between; - width: 100%; -} - -p { - margin-bottom: 0; -} - -#world-map { - height: 65%; - width: 100%; -} - -.violin-container { - height: 100%; - width: 100%; - display: flex; - align-items: stretch; -} - -.loading-violin-container { - width: 100%; - height: 100%; -} - -#temp-profile-graph, #humidity-profile-graph, #solar-radiation-graph, #wind-speed-graph { - height: 50%; - /*width: 100%;*/ -} - -/* Tab Temperature RH*/ -.dropdown-t-rh { - width: 15rem; -} - -/* Tab Four */ -#tab-four-container { - width: 100%; - justify-content: center; - align-items: center; -} - -/*#tab-four-custom-sun-container {*/ -/* justify-content: space-evenly;*/ -/* align-items: stretch;*/ -/* align-content: center;*/ -/*}*/ - -/* Tab Five */ -#tab-five-container { - justify-content: center; - align-items: stretch; -} - -#tab5-daily-container { - margin-top: 60px; -} - -#slider-container { - width: 50%; - margin: 24px; -} - -.seasonal-graph { - width: 100%; - height: 100%; -} - -.daily-wind-graph { - width: 80%; - height: 80%; -} - -#daily-wind-rose-container { - display: flex; - flex-direction: row; - max-width: 100%; -} - -#daily-wind-rose-outer-container { - align-items: stretch; -} - -#slider-container * { - justify-content: center; -} - -.each-slider { - padding: 32px 0 0 0; - width: 60%; -} - -#hour-slider, #month-slider { - width: 60%; -} - -#wind-speed, #wind-direction { - width: 100%; -} - -#custom-windrose-container { - margin-top: 80px; -} - -#tab5-custom-dropdown-container { - margin-top: 30px; -} - -/* Tab Six */ - -#first-var-dropdown { - width: 15%; -} - -.month-hour-slider { - width: 25%; -} - -.var-dropdown { - width: 35%; -} - -#min-val, #max-val { - width: 15%; -} - -.row-center { - align-items: center; - justify-content: center; -} - -.text-next-to-input { - margin-bottom: 0; - margin-right: 1rem; - text-align: right; -} - -#sec1-var-dropdown { - width: 25%; -} - -.three-inputs-container { - justify-content: space-evenly; -} - -.one-of-three-container { - width: 30%; - align-self: flex-start; - align-items: stretch; -} - -#tab6-sec2-container { - align-items: stretch; -} - -.survey-alert { - color: white; - background-color: #0c2772; - opacity: 0.98; - font-family: "system-ui"; - font-size: 15px; - border-radius: 0.25rem; - border: 0.5px solid lightgrey; - z-index: 1000; -} \ No newline at end of file diff --git a/main.py b/main.py index e5c2366..cdf1b94 100644 --- a/main.py +++ b/main.py @@ -1,38 +1,23 @@ -import dash_bootstrap_components as dbc -from dash import html, dcc -from dash_extensions.enrich import Output, Input, callback +from dash import dcc +import dash_mantine_components as dmc from app import app -from pages.lib.layout import banner, footer, build_tabs +from pages.lib.layout import create_collapsible_layout from config import AppConfig from pages.lib.global_element_ids import ElementIds server = app.server app.title = AppConfig.TITLE -app.layout = dbc.Container( - fluid=True, - style={"padding": "0"}, +app.layout = dmc.MantineProvider( children=[ - dcc.Location(id="url", refresh=False), # connected to callback below - banner(), - html.Div(id="page-content", children=build_tabs()), - footer(), + dcc.Location(id=ElementIds.MAIN_URL, refresh=False), + create_collapsible_layout(), ], ) - -# callback for survey alert (dbc.Toast) -@callback( - Output(ElementIds.ID_MAIN_ALERT_AUTO, "is_open"), - Input(ElementIds.ID_MAIN_INTERVAL_COMPONENT, "n_intervals"), -) -def display_alert(n): - return n == 1 - - if __name__ == "__main__": - app.run_server( + app.run( debug=AppConfig.DEBUG, host=AppConfig.HOST, port=AppConfig.PORT, diff --git a/pages/explorer.py b/pages/explorer.py index dde7df5..f40b4e8 100644 --- a/pages/explorer.py +++ b/pages/explorer.py @@ -1,6 +1,6 @@ import dash -from dash import dcc, html -import dash_bootstrap_components as dbc +from dash import dcc +import dash_mantine_components as dmc from dash_extensions.enrich import Output, Input, State, callback from dash.exceptions import PreventUpdate @@ -22,8 +22,6 @@ sun_cloud_tab_dropdown_names, more_variables_dropdown, sun_cloud_tab_explore_dropdown_names, - container_row_center_full, - container_col_center_one_of_three, ) from pages.lib.template_graphs import ( heatmap, @@ -62,12 +60,21 @@ explore_dropdown_names.pop("None", None) +def layout(): + """Return the contents of tab six.""" + return dmc.Stack( + p="md", + children=[*section_one(), section_two(), section_three()], + ) + + def section_one_inputs(): """Return the inputs from section one.""" - return html.Div( - className="container-row full-width row-center", + return dmc.Group( + mt="md", + justify="center", children=[ - html.H4(className="text-next-to-input", children=["Select a variable: "]), + dmc.Title("Select a variable:", order=5), dropdown( id=ElementIds.SEC1_VAR_DROPDOWN, options=explore_dropdown_names, @@ -79,187 +86,147 @@ def section_one_inputs(): def section_one(): """Return the graphs for section one""" - return html.Div( - className="container-col full-width", - children=[ - section_one_inputs(), - html.Div( - children=title_with_link( - text="Yearly chart", - id_button=IdButtons.EXPLORE_YEARLY_CHART_LABEL, - doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, - ), - ), - dcc.Loading( - type="circle", - children=html.Div(id=ElementIds.YEARLY_EXPLORE, className="full-width"), - ), - html.Div( - children=title_with_link( - text="Daily chart", - id_button=IdButtons.EXPLORE_DAILY_CHART_LABEL, - doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, - ), - ), - dcc.Loading( - html.Div(className="full-width", id=ElementIds.QUERY_DAILY), - type="circle", - ), - html.Div( - children=title_with_link( - text="Heatmap chart", - id_button=IdButtons.EXPLORE_HEATMAP_CHART_LABEL, - doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, - ), - ), - dcc.Loading( - html.Div(className="full-width", id=ElementIds.QUERY_HEATMAP), - type="circle", - ), - html.Div( - children=title_with_tooltip( - text="Descriptive statistics", - tooltip_text="count, mean, std, min, max, and percentiles", - id_button=IdButtons.TABLE_EXPLORE, - ), - ), - html.Div( - className="container-row justify-content-center", - children=[ - html.Div( - className=container_col_center_one_of_three, - children=[ - dbc.Button( - "Apply month and hour filter", - color="primary", - id=ElementIds.SEC1_TIME_FILTER_INPUT, - className="mb-2", - n_clicks=0, - ), - html.Div( - className=( - "container-row full-width justify-center mt-2" - ), - children=[ - html.H6("Month Range", style={"flex": "20%"}), - html.Div( - 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, - ), - style={"flex": "50%"}, - ), - dcc.Checklist( - options=[ - {"label": "Invert", "value": "invert"}, - ], - value=[], - id=ElementIds.INVERT_MONTH_EXPLORE_DESCRIPTIVE, - labelStyle={"flex": "30%"}, - ), - ], - ), - html.Div( - className="container-row justify-center", - children=[ - html.H6("Hour Range", style={"flex": "20%"}), - html.Div( - 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, - ), - style={"flex": "50%"}, + return [ + section_one_inputs(), + title_with_link( + text="Yearly chart", + id_button=IdButtons.EXPLORE_YEARLY_CHART_LABEL, + doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, + ), + dcc.Loading( + type="circle", children=dmc.Paper(id=ElementIds.YEARLY_EXPLORE, p="sm") + ), + 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, p="sm") + ), + 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, p="sm") + ), + 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( - options=[ - {"label": "Invert", "value": "invert"}, - ], - value=[], - id=ElementIds.INVERT_HOUR_EXPLORE_DESCRIPTIVE, - labelStyle={"flex": "30%"}, + ), + 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, ), - ], - ), - ], - ), - ], - ), - html.Div( - id=ElementIds.TABLE_DATA_EXPLORER, - ), - ], - ) + ), + 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"), + ] def section_two_inputs(): """Return all the input forms from section two.""" - return html.Div( + return dmc.Stack( + p="md", children=[ - html.Div( - children=title_with_tooltip( - text="Customizable heatmap", - tooltip_text=None, - id_button=IdButtons.CUSTOM_HEATMAP_CHART_LABEL, - ), + title_with_tooltip( + text="Customizable heatmap", + tooltip_text=None, + id_button=IdButtons.CUSTOM_HEATMAP_CHART_LABEL, ), - html.Div( - className="container-row full-width three-inputs-container", + dmc.SimpleGrid( + cols=3, + spacing="md", children=[ - html.Div( - className=container_col_center_one_of_three, - children=[ - html.Div( - className=container_row_center_full, - children=[ - html.H6( - children=["Variable:"], - style={"flex": "30%"}, - ), - dropdown( - id=ElementIds.SEC2_VAR_DROPDOWN, - options=explore_dropdown_names, - value=ColNames.RH, - style={"flex": "70%"}, - ), - ], + dmc.Group( + [ + dmc.Title("Variable:", order=5), + dmc.Stack( + dropdown( + id=ElementIds.SEC2_VAR_DROPDOWN, + options=explore_dropdown_names, + value=ColNames.RH, + ), + flex=1, ), ], + align="flex-start", ), - html.Div( - className=container_col_center_one_of_three, - children=[ - dbc.Button( + dmc.Stack( + [ + dmc.Button( "Apply month and hour filter", - color="primary", id=ElementIds.SEC2_TIME_FILTER_INPUT, - className="mb-2", - n_clicks=0, + color="blue", ), - html.Div( - className=( - "container-row full-width justify-center mt-2" - ), - children=[ - html.H6("Month Range", style={"flex": "20%"}), - html.Div( + dmc.Group( + [ + dmc.Title("Month Range", order=5), + dmc.Stack( dcc.RangeSlider( id=ElementIds.SEC2_MONTH_SLIDER, min=1, @@ -269,27 +236,24 @@ def section_two_inputs(): marks={1: "1", 12: "12"}, tooltip={ "always_visible": False, - "placement": "top", + "placement": "topLeft", }, - allowCross=False, ), - style={"flex": "50%"}, + flex=1, ), dcc.Checklist( + id=ElementIds.INVERT_MONTH_EXPLORE_HEATMAP, options=[ - {"label": "Invert", "value": "invert"}, + {"label": "Invert", "value": "invert"} ], value=[], - id=ElementIds.INVERT_MONTH_EXPLORE_HEATMAP, - labelStyle={"flex": "30%"}, ), ], ), - html.Div( - className="container-row justify-center", - children=[ - html.H6("Hour Range", style={"flex": "20%"}), - html.Div( + dmc.Group( + [ + dmc.Title("Hour Range", order=5), + dmc.Stack( dcc.RangeSlider( id=ElementIds.SEC2_HOUR_SLIDER, min=0, @@ -301,76 +265,63 @@ def section_two_inputs(): "always_visible": False, "placement": "topLeft", }, - allowCross=False, ), - style={"flex": "50%"}, + flex=1, ), dcc.Checklist( + id=ElementIds.INVERT_HOUR_EXPLORE_HEATMAP, options=[ - {"label": "Invert", "value": "invert"}, + {"label": "Invert", "value": "invert"} ], value=[], - id=ElementIds.INVERT_HOUR_EXPLORE_HEATMAP, - labelStyle={"flex": "30%"}, ), ], ), ], ), - html.Div( - className=container_col_center_one_of_three, - children=[ - dbc.Button( + dmc.Stack( + [ + dmc.Button( "Apply filter", - color="primary", id=ElementIds.SEC2_DATA_FILTER_INPUT, - className="mb-2", - n_clicks=0, + color="blue", ), - html.Div( - className=container_row_center_full, - children=[ - html.H6( - children=["Filter Variable:"], - style={"flex": "30%"}, - ), - dropdown( - id=ElementIds.SEC2_DATA_FILTER_VAR, - options=explore_dropdown_names, - value=ColNames.RH, - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Filter Variable:", order=5), + dmc.Stack( + dropdown( + id=ElementIds.SEC2_DATA_FILTER_VAR, + options=explore_dropdown_names, + value=ColNames.RH, + ), + flex=1, ), ], ), - html.Div( - className=container_row_center_full, - children=[ - html.H6( - children=["Min Value:"], style={"flex": "30%"} - ), - dbc.Input( - id=ElementIds.SEC2_MIN_VAL, - placeholder="Enter a number for the min val", - type="number", - value=0, - step=1, - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Min Value:", order=5), + dmc.Stack( + dmc.NumberInput( + id=ElementIds.SEC2_MIN_VAL, + placeholder="Enter a number for the min val", + value=0, + ), + flex=1, ), ], ), - html.Div( - className=container_row_center_full, - children=[ - html.H6( - children=["Max Value:"], style={"flex": "30%"} - ), - dbc.Input( - id=ElementIds.SEC2_MAX_VAL, - placeholder="Enter a number for the max val", - type="number", - value=100, - step=1, - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Max Value:", order=5), + dmc.Stack( + dmc.NumberInput( + id=ElementIds.SEC2_MAX_VAL, + placeholder="Enter a number for the max val", + value=100, + ), + flex=1, ), ], ), @@ -384,205 +335,184 @@ def section_two_inputs(): def section_two(): """Return the two graphs in section two.""" - return html.Div( + return dmc.Stack( id=ElementIds.TAB6_SEC2_CONTAINER, - className="container-col justify-center full-width", children=[ section_two_inputs(), dcc.Loading( type="circle", - children=html.Div(className="full-width", id=ElementIds.CUSTOM_HEATMAP), + children=dmc.Paper( + id=ElementIds.CUSTOM_HEATMAP, + p="sm", + ), ), - dbc.Checklist( - options=[ - {"label": "Normalize", "value": "normal"}, + dmc.Group( + children=[ + dmc.CheckboxGroup( + id=ElementIds.NORMALIZE, + value=[], + children=[ + dmc.Checkbox(label="Normalize", value="normal"), + ], + ), ], - value=[], - id=ElementIds.NORMALIZE, ), dcc.Loading( type="circle", - children=[ - dcc.Graph( - className="full-width", + children=dmc.Paper( + children=dcc.Graph( id=ElementIds.CUSTOM_SUMMARY, config=fig_config, ), - ], + ), ), ], ) def section_three_inputs(): - """""" - return html.Div( - className="container-row full-width three-inputs-container", + return dmc.SimpleGrid( + cols=3, children=[ - html.Div( - className=container_col_center_one_of_three, - children=[ - html.Div( - className=container_row_center_full, - children=[ - html.H6(style={"flex": "30%"}, children=["X Variable:"]), - dropdown( - id=ElementIds.TAB6_SEC3_VAR_X_DROPDOWN, - options=explore_dropdown_names, - value="DBT", - style={"flex": "70%"}, + dmc.Stack( + [ + dmc.Group( + [ + dmc.Title("X Variable:", order=5), + dmc.Stack( + dropdown( + id=ElementIds.TAB6_SEC3_VAR_X_DROPDOWN, + options=explore_dropdown_names, + value="DBT", + ), + flex=1, ), ], ), - html.Div( - className=container_row_center_full, - children=[ - html.H6(style={"flex": "30%"}, children=["Y Variable:"]), - dropdown( - id=ElementIds.TAB6_SEC3_VAR_Y_DROPDOWN, - options=explore_dropdown_names, - value=ColNames.RH, - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Y Variable:", order=5), + dmc.Stack( + dropdown( + id=ElementIds.TAB6_SEC3_VAR_Y_DROPDOWN, + options=explore_dropdown_names, + value=ColNames.RH, + ), + flex=1, ), ], ), - html.Div( - className=container_row_center_full, - children=[ - html.H6(style={"flex": "30%"}, children=["Color By:"]), - dropdown( - id=ElementIds.TAB6_SEC3_COLORBY_DROPDOWN, - options=explore_dropdown_names, - value="glob_hor_rad", - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Color By:", order=5), + dmc.Stack( + dropdown( + id=ElementIds.TAB6_SEC3_COLORBY_DROPDOWN, + options=explore_dropdown_names, + value="glob_hor_rad", + ), + flex=1, ), ], ), ], ), - html.Div( - className=container_col_center_one_of_three, - children=[ - dbc.Button( + dmc.Stack( + [ + dmc.Button( "Apply month and hour filter", - color="primary", id=ElementIds.TAB6_SEC3_TIME_FILTER_INPUT, - className="mb-2", - n_clicks=0, + color="blue", ), - html.Div( - className="container-row full-width justify-center", - children=[ - html.H6("Month Range", style={"flex": "20%"}), - html.Div( + dmc.Group( + [ + dmc.Title("Month Range", order=5), + dmc.Stack( dcc.RangeSlider( id=ElementIds.TAB6_SEC3_QUERY_MONTH_SLIDER, min=1, max=12, - step=1, value=[1, 12], marks={1: "1", 12: "12"}, - tooltip={ - "always_visible": False, - "placement": "top", - }, - allowCross=False, ), - style={"flex": "50%"}, + flex=1, ), dcc.Checklist( - options=[ - {"label": "Invert", "value": "invert"}, - ], - value=[], id=ElementIds.INVERT_MONTH_EXPLORE_MORE_CHARTS, - labelStyle={"flex": "30%"}, + options=[{"label": "Invert", "value": "invert"}], + value=[], ), ], ), - html.Div( - className="container-row full-width justify-center", - children=[ - html.H6("Hour Range", style={"flex": "20%"}), - html.Div( + dmc.Group( + [ + dmc.Title("Hour Range", order=5), + dmc.Stack( dcc.RangeSlider( id=ElementIds.TAB6_SEC3_QUERY_HOUR_SLIDER, min=0, max=24, - step=1, value=[0, 24], marks={0: "0", 24: "24"}, tooltip={ "always_visible": False, - "placement": "top", + "placement": "topLeft", }, - allowCross=False, ), - style={"flex": "50%"}, + flex=1, ), dcc.Checklist( - options=[ - {"label": "Invert", "value": "invert"}, - ], - value=[], id=ElementIds.INVERT_HOUR_EXPLORE_MORE_CHARTS, - labelStyle={"flex": "30%"}, + options=[{"label": "Invert", "value": "invert"}], + value=[], ), ], ), ], ), - html.Div( - className=container_col_center_one_of_three, - children=[ - dbc.Button( + dmc.Stack( + [ + dmc.Button( "Apply filter", - color="primary", id=ElementIds.TAB6_SEC3_DATA_FILTER_INPUT, - className="mb-2", - n_clicks=0, + color="blue", ), - html.Div( - className=container_row_center_full, - children=[ - html.H6( - children=["Filter Variable:"], style={"flex": "30%"} - ), - dropdown( - id=ElementIds.TAB6_SEC3_FILTER_VAR_DROPDOWN, - options=explore_dropdown_names, - value=ColNames.RH, - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Filter Variable:", order=5), + dmc.Stack( + dropdown( + id=ElementIds.TAB6_SEC3_FILTER_VAR_DROPDOWN, + options=explore_dropdown_names, + value=ColNames.RH, + ), + flex=1, ), ], ), - html.Div( - className=container_row_center_full, - children=[ - html.H6(children=["Min Value:"], style={"flex": "30%"}), - dbc.Input( - className="num-input", - id=ElementIds.TAB6_SEC3_MIN_VAL, - placeholder="Enter a number for the min val", - type="number", - step=1, - value=0, - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Min Value:", order=5), + dmc.Stack( + dmc.NumberInput( + id=ElementIds.TAB6_SEC3_MIN_VAL, + placeholder="Enter a number for the min val", + value=0, + ), + flex=1, ), ], ), - html.Div( - className=container_row_center_full, - children=[ - html.H6(children=["Max Value:"], style={"flex": "30%"}), - dbc.Input( - className="num-input", - id=ElementIds.TAB6_SEC3_MAX_VAL, - placeholder="Enter a number for the max val", - type="number", - value=100, - step=1, - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Max Value:", order=5), + dmc.Stack( + dmc.NumberInput( + id=ElementIds.TAB6_SEC3_MAX_VAL, + placeholder="Enter a number for the max val", + value=100, + ), + flex=1, ), ], ), @@ -594,37 +524,32 @@ def section_three_inputs(): def section_three(): """Return the two graphs in section three.""" - return html.Div( - className="container-col full-width", + return dmc.Stack( children=[ - html.Div( - children=title_with_tooltip( - text="More charts", - tooltip_text=None, - id_button=IdButtons.MORE_CHARTS_LABEL, - ), + title_with_tooltip( + text="More charts", + tooltip_text=None, + id_button=IdButtons.MORE_CHARTS_LABEL, ), section_three_inputs(), dcc.Loading( - html.Div(id=ElementIds.THREE_VAR), type="circle", + children=dmc.Paper( + id=ElementIds.THREE_VAR, + p="sm", + ), ), dcc.Loading( - html.Div(id=ElementIds.TWO_VAR), type="circle", + children=dmc.Paper( + id=ElementIds.TWO_VAR, + p="sm", + ), ), ], ) -def layout(): - """Return the contents of tab six.""" - return html.Div( - className="justify-center", - children=[section_one(), section_two(), section_three()], - ) - - @callback( Output(ElementIds.YEARLY_EXPLORE, "children"), # Section One @@ -643,7 +568,7 @@ def update_tab_yearly(_, var, global_local, df, meta, si_ip): """Update the contents of tab size. Passing in the info from the dropdown and the general info.""" if df[var].mean() == 99990.0: - return dbc.Alert( + return dmc.Alert( """The selected variable is not available, the Clima tool could not generate the yearly plot""", color="warning", @@ -781,7 +706,7 @@ def update_heatmap( if not heat_map: return ( - dbc.Alert( + dmc.Alert( "No data is available in this location under these conditions. Please " "either change the month and hour filters, or select a wider range for " "the filter variable", @@ -902,7 +827,7 @@ def update_more_charts( if not three: custom_inputs = f"{var_x}-{var_y}" units = generate_units(si_ip) - return dbc.Alert( + return dmc.Alert( "No data is available in this location under these conditions. Please " "either change the month and hour filters, or select a wider range for " "the filter variable", diff --git a/pages/lib/global_element_ids.py b/pages/lib/global_element_ids.py index 644dcd3..63709a8 100644 --- a/pages/lib/global_element_ids.py +++ b/pages/lib/global_element_ids.py @@ -155,7 +155,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" - SEASONAL_WIND_ROSE_DOC = "seasonal-wind-rose-doc" LOADING_ONE = "loading-1" UPLOAD_DATA = "upload-data" UPLOAD_DATA_BUTTON = "upload-data-button" @@ -201,13 +200,9 @@ class ElementIds(str, Enum): WIND_PROFILE_GRAPH = "wind-profile-graph" TDB_PROFILE_GRAPH = "tdb-profile-graph" RH_PROFILE_GRAPH = "rh-profile-graph" - ALERT_CONTAINER = "alert-container" ID_LAYOUT_ALERT_AUTO = "alert-auto" - ID_MAIN_ALERT_AUTO = "alert-auto" ID_LAYOUT_INTERVAL_COMPONENT = "interval-component" - ID_MAIN_INTERVAL_COMPONENT = "interval-component" FOOTER_CONTAINER = "footer-container" - BANNER = "banner" BANNER_TITLE = "banner-title" ID_LAYOUT_BANNER_SUBTITLE = "banner-subtitle" ID_LAYOUT_GLOBAL_LOCAL_RADIO_INPUT = "global-local-radio-input" @@ -218,8 +213,23 @@ class ElementIds(str, Enum): ID_LAYOUT_URL_STORE = "url-store" ID_LAYOUT_SI_IP_UNIT_STORE = "si-ip-unit-store" ID_LAYOUT_LINES_STORE = "lines-store" - TABS_CONTAINER = "tabs-container" - TABS_PARENT = "tabs-parent" - TABS = "tabs" - STORE_CONTAINER = "store-container" - TABS_CONTENT = "tabs-content" + BURGER_BUTTON = "burger-button" + NAVBAR = "navbar" + NAV_GROUP_MAIN = "nav-group-main" + NAV_GROUP_CONTROLS = "nav-group-controls" + NAV_DOC_LINK = "nav-doc-link" + SELECT_URL = "url" + MAIN_URL = "url" + NAV = "nav-" + NAV_SUMMARY = "nav-summary" + NAV_T_RH = "nav-t-rh" + NAV_SUN = "nav-sun" + NAV_WIND = "nav-wind" + NAV_PSY_CHART = "nav-psy-chart" + NAV_EXPLORER = "nav-explorer" + NAV_OUTDOOR = "nav-outdoor" + NAV_NATURAL_VENTILATION = "nav-natural-ventilation" + NAV_CHANGELOG = "nav-changelog" + APP_SHELL = "appshell" + NAVBAR_CONTAINER = "navbar-container" + TOOLS_MENU_EXPANDED = "tools-menu-expanded" diff --git a/pages/lib/layout.py b/pages/lib/layout.py index f6cff01..71b1119 100644 --- a/pages/lib/layout.py +++ b/pages/lib/layout.py @@ -1,6 +1,5 @@ -import dash_bootstrap_components as dbc import dash -from dash import dcc, html +from dash import dcc, Input, Output, State, callback import dash_mantine_components as dmc from dash_iconify import DashIconify from pages.lib.global_column_names import ColNames @@ -8,222 +7,306 @@ from pages.lib.global_element_ids import ElementIds -def alert(): - """Alert for survey.""" - return html.Div( - id=ElementIds.ALERT_CONTAINER, +class NavBarIcons: + _ICON_MAP = { + "Select Weather File": "tabler:upload", + "Climate Summary": "tabler:chart-bar", + "Temperature and Humidity": "tabler:temperature", + "Sun and Clouds": "tabler:sun", + "Wind": "tabler:wind", + "Psychrometric Chart": "tabler:chart-dots", + "Natural Ventilation": "tabler:windmill", + "Outdoor Comfort": "tabler:thermometer", + "Data Explorer": "tabler:database", + "Changelog": "tabler:history", + } + + SELECT_WEATHER_FILE = _ICON_MAP["Select Weather File"] + CLIMATE_SUMMARY = _ICON_MAP["Climate Summary"] + TEMPERATURE_AND_HUMIDITY = _ICON_MAP["Temperature and Humidity"] + SUN_AND_CLOUDS = _ICON_MAP["Sun and Clouds"] + WIND = _ICON_MAP["Wind"] + PSYCHROMETRIC_CHART = _ICON_MAP["Psychrometric Chart"] + NATURAL_VENTILATION = _ICON_MAP["Natural Ventilation"] + OUTDOOR_COMFORT = _ICON_MAP["Outdoor Comfort"] + DATA_EXPLORER = _ICON_MAP["Data Explorer"] + CHANGELOG = _ICON_MAP["Changelog"] + + @classmethod + def get_icon(cls, page_name): + """Get icon for a page name.""" + return cls._ICON_MAP.get(page_name, "tabler:circle") + + +def create_navbar(): + nav_link_styles = { + "root": { + "borderRadius": "0.375rem", + "transition": "all 0.2s ease", + "&:hover": {"backgroundColor": "#e3f2fd"}, + "&[data-active='true']": { + "backgroundColor": "#1976d2", + "color": "white", + "fontWeight": 600, + }, + "&[data-active='true']:hover": { + "backgroundColor": "#1565c0", + "color": "white", + }, + } + } + + # Secondary Menu + sub_links = [ + dmc.NavLink( + label=page[ColNames.NAME], + leftSection=DashIconify( + icon=NavBarIcons.get_icon(page[ColNames.NAME]), width=20 + ), + href=page[ColNames.PATH], + id=f"nav-{page[ColNames.PATH].replace('/', '')}", + active=False, + styles=nav_link_styles, + ) + for page in dash.page_registry.values() + if page[ColNames.NAME] not in ["404"] + ] + + parent_group = dmc.NavLink( + label="Pages Menu", + children=sub_links, + id=ElementIds.NAV_GROUP_MAIN, + variant="light", + childrenOffset=0, + opened=True, + ) + + segmented_control_styles = { + "control": {"flex": 1, "minWidth": 0}, + } + + controls_stack = dmc.Stack( + gap="xs", + py="xs", children=[ - dbc.Toast( - [ - "If you have a moment, help us improve Clima and take a ", - html.A( - "quick user survey", - href="https://forms.gle/k289zP3R92jdu14M7", - className="alert-link", - target="_blank", - ), - "! ☀️", - ], - id=ElementIds.ID_LAYOUT_ALERT_AUTO, - header="CBE Clima User Survey", - icon="info", - is_open=False, - dismissable=True, - className="survey-alert", - style={"position": "fixed", "top": 25, "right": 10, "width": 400}, + dmc.Tooltip( + label=dmc.Stack( + gap="xs", + children=[ + dmc.Text( + "You can choose value ranges between Global and Local" + ), + ], + ), + position="right", + withArrow=True, + children=dmc.SegmentedControl( + id=ElementIds.ID_LAYOUT_GLOBAL_LOCAL_RADIO_INPUT, + value="local", + color="blue", + data=[ + {"label": "Global", "value": "global"}, + {"label": "Local", "value": "local"}, + ], + w=220, + size="sm", + styles=segmented_control_styles, + ), ), - dcc.Interval( - id=ElementIds.ID_LAYOUT_INTERVAL_COMPONENT, - interval=12 * 1000, - n_intervals=0, + dmc.Tooltip( + label=dmc.Stack( + gap="xs", + children=[ + dmc.Text("You can choose units between SI and IP"), + ], + ), + position="right", + withArrow=True, + children=dmc.SegmentedControl( + id=ElementIds.ID_LAYOUT_SI_IP_RADIO_INPUT, + value=UnitSystem.SI, + color="blue", + data=[ + {"label": "SI", "value": UnitSystem.SI}, + {"label": "IP", "value": UnitSystem.IP}, + ], + w=220, + size="sm", + styles=segmented_control_styles, + ), ), ], ) + # Tools + controls_group = dmc.NavLink( + label="Tools Menu", + children=[controls_stack], + id=ElementIds.NAV_GROUP_CONTROLS, + variant="light", + childrenOffset=0, + ) -def footer(): - """Build the footer at the bottom of the page.""" - return dbc.Row( - align="center", - justify="between", - id=ElementIds.FOOTER_CONTAINER, - children=[ - dbc.Col( + # Documentation + doc_link = dmc.NavLink( + label="Documentation", + href=DocLinks.MAIN.value, + target="_blank", + id=ElementIds.NAV_DOC_LINK, + variant="light", + ) + + return dmc.ScrollArea( + children=[parent_group, controls_group, doc_link], + ) + + +def create_header(): + return dmc.Group( + [ + dmc.Burger( + id=ElementIds.BURGER_BUTTON, + size="sm", + opened=True, + color="blue", + ), + dmc.Anchor( + href="/", + children=dmc.Image(src="assets/img/cbe-logo-small.png", h=40, flex=0), + ), + dmc.Stack( + gap="xs", children=[ - dbc.Row( - html.A( - children=[ - html.Img( - src="assets/img/cbe-logo.png", - ) - ], - href="https://cbe.berkeley.edu/", - ) + 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, + size="sm", + opacity=0.85, + style={"overflow": "hidden"}, + c="white", ), ], - width=12, - md=4, - style={"padding": "15px"}, + p="xs", ), - dbc.Col( - children=[ - dbc.Row( - [ - dcc.Markdown( - """ - Please cite us: - Betti, G., Tartarini, F., Nguyen, C, Schiavon, S. CBE Clima Tool: - A free and open-source web application for climate analysis tailored to sustainable building design. - Build. Simul. (2023). [https://doi.org/10.1007/s12273-023-1090-5](https://doi.org/10.1007/s12273-023-1090-5). - """ - ), - dmc.Group( - [ - dmc.Anchor( - "Version: 0.9.0", - href="https://center-for-the-built-environment.gitbook.io/clima/version/changelog", - underline=True, - c="white", - target="_blank", - ), - dmc.Anchor( - "Contributors", - href="https://cbe-berkeley.gitbook.io/clima/#contributions", - underline=True, - c="white", - target="_blank", - ), - dmc.Anchor( - "Report issues on GitHub", - href="https://github.com/CenterForTheBuiltEnvironment/clima/issues", - underline=True, - c="white", - target="_blank", - ), - dmc.Anchor( - "Contact us", - href="https://github.com/CenterForTheBuiltEnvironment/clima/discussions", - underline=True, - c="white", - target="_blank", - ), - dmc.Anchor( - "Documentation", - href="https://center-for-the-built-environment.gitbook.io/clima/", - underline=True, - c="white", - target="_blank", - ), - dmc.Anchor( - "License", - href="https://center-for-the-built-environment.gitbook.io/clima/#license", - underline=True, - c="white", - target="_blank", - ), - ], - spacing="sm", - style={"marginTop": "1rem"}, - ), - ], - style={"marginTop": "1rem"}, + dmc.Alert( + [ + "If you have a moment, help us improve Clima and take a ", + dmc.Anchor( + "quick user survey", + href="https://forms.gle/k289zP3R92jdu14M7", + target="_blank", + c="white", + underline="always", ), + "! ☀️", ], - width=12, - md=8, + id=ElementIds.ID_LAYOUT_ALERT_AUTO, + title="CBE Clima User Survey", + icon=dmc.ThemeIcon( + DashIconify(icon="tabler:info-circle", color="white"), + ), + color="blue", + variant="filled", + withCloseButton=True, + w=400, + pos="fixed", + top="1em", + right="1em", + style={"zIndex": 1002, "display": "none"}, ), ], + pl="md", ) -def banner(): - """Build the banner at the top of the page.""" - return html.Div( - id=ElementIds.BANNER, - children=[ - dmc.Group( - position="apart", - align="center", +def create_footer(): + white_anchor_style = { + "underline": "always", + "c": "white", + "target": "_blank", + } + + footer_links = [ + ( + "Version: 0.9.0", + "https://center-for-the-built-environment.gitbook.io/clima/version/changelog", + ), + ("Contributors", "https://cbe-berkeley.gitbook.io/clima/#contributions"), + ( + "Report issues on GitHub", + "https://github.com/CenterForTheBuiltEnvironment/clima/issues", + ), + ( + "Contact us", + "https://github.com/CenterForTheBuiltEnvironment/clima/discussions", + ), + ("Documentation", "https://center-for-the-built-environment.gitbook.io/clima/"), + ( + "License", + "https://center-for-the-built-environment.gitbook.io/clima/#license", + ), + ] + + return dmc.Group( + [ + dmc.Anchor( + href="https://cbe.berkeley.edu/", + children=dmc.Image( + src="assets/img/cbe-logo.png", + alt="CBE Logo", + h=40, + w="auto", + fit="contain", + ), + ), + dmc.Stack( + gap="xs", children=[ - dmc.Group( - align="center", - children=[ - html.A( - href="/", - children=[ - dmc.Image( - src="assets/img/cbe-logo-small.png", - height=40, - width="auto", - ) - ], - ), - dmc.Stack( - spacing=0, - children=[ - dmc.Title( - "CBE Clima Tool", - order=1, - id=ElementIds.BANNER_TITLE, - style={"fontSize": "2rem"}, - ), - dmc.Text( - "Current Location:", - id=ElementIds.ID_LAYOUT_BANNER_SUBTITLE, - size="sm", - ), - ], - ), - ], + dcc.Markdown( + """ + Please cite us: Betti, G., Tartarini, F., Nguyen, C, Schiavon, S. CBE Clima Tool: A free and open-source web application for climate analysis tailored to sustainable building design. Build. Simul. (2023). [https://doi.org/10.1007/s12273-023-1090-5](https://doi.org/10.1007/s12273-023-1090-5). + """, + style={ + "fontSize": "1rem", + "lineHeight": 1.3, + "fontWeight": 400, + "color": "white", + "textAlign": "left", + }, ), dmc.Group( - align="center", - children=[ - html.A( - dmc.Button( - "Documentation", - leftIcon=DashIconify(icon="bi:book-half", width=20), - variant="filled", - color="#5c7cfa", - ), - href=DocLinks.MAIN.value, - target="_blank", - style={"textDecoration": "none"}, - ), - dmc.SegmentedControl( - id=ElementIds.ID_LAYOUT_GLOBAL_LOCAL_RADIO_INPUT, - value="local", - radius="md", - data=[ - {"label": "Global Value Ranges", "value": "global"}, - {"label": "Local Value Ranges", "value": "local"}, - ], - ), - dmc.SegmentedControl( - id=ElementIds.ID_LAYOUT_SI_IP_RADIO_INPUT, - value=UnitSystem.SI, - radius="md", - data=[ - { - "label": UnitSystem.SI.upper(), - "value": UnitSystem.SI, - }, - { - "label": UnitSystem.IP.upper(), - "value": UnitSystem.IP, - }, - ], - ), + [ + dmc.Anchor(text, href=url, **white_anchor_style) + for text, url in footer_links ], + gap="sm", + wrap="wrap", + justify="flex-start", ), ], - ) + flex=1, + align="flex-start", + ml="xl", + ), ], + id=ElementIds.FOOTER_CONTAINER, + p="sm", + c="white", + bg="#003262", + justify="flex-start", + align="center", ) -def store(): - return html.Div( +def create_stores(): + return dmc.Box( id=ElementIds.STORE, children=[ dcc.Store(id=ElementIds.ID_LAYOUT_DF_STORE, storage_type="session"), @@ -231,53 +314,115 @@ def store(): dcc.Store(id=ElementIds.ID_LAYOUT_URL_STORE, storage_type="session"), dcc.Store(id=ElementIds.ID_LAYOUT_SI_IP_UNIT_STORE, storage_type="session"), dcc.Store(id=ElementIds.ID_LAYOUT_LINES_STORE, storage_type="session"), + dcc.Store( + id=ElementIds.TOOLS_MENU_EXPANDED, data=False, storage_type="session" + ), + dcc.Interval( + id=ElementIds.ID_LAYOUT_INTERVAL_COMPONENT, + interval=12 * 1000, + n_intervals=0, + ), ], ) -def build_tabs(): - return html.Div( - id=ElementIds.TABS_CONTAINER, - children=[ - html.Div( - id=ElementIds.TABS_PARENT, - className="custom-tabs", - children=[ - dbc.Nav( - [ - dbc.NavItem( - dbc.NavLink( - page[ColNames.NAME], - id=page[ColNames.PATH], - href=page[ColNames.PATH], - active="exact", - className="nav-link", - disabled=True, - ), - className="custom-tab", - ) - for page in dash.page_registry.values() - if page[ColNames.NAME] not in ["404", "changelog"] - ], - id=ElementIds.TABS, - class_name="tab-container", - pills=True, - justified=True, - ) - ], +def create_collapsible_layout(): + return dmc.AppShell( + [ + dmc.AppShellHeader( + create_header(), + bg="#003262", ), - html.Div( - id=ElementIds.STORE_CONTAINER, - children=[ - store(), - html.Div( - id=ElementIds.TABS_CONTENT, - children=[ - alert(), # alert can be removed after survey is done - dash.page_container, - ], - ), - ], + dmc.AppShellNavbar( + id=ElementIds.NAVBAR, + children=create_navbar(), + bg="#f8f9fa", + ), + # including main and footer + dmc.AppShellMain( + dmc.ScrollArea( + children=[ + create_stores(), + dash.page_container, + create_footer(), + ], + ), + pos="relative", + style={ + "zIndex": 1, + "@media (max-width: 48rem)": { + "left": "0", + }, + }, ), ], + header={"height": 80}, + navbar={ + "width": 230, + "breakpoint": "sm", + "collapsed": {"mobile": True, "desktop": False}, + "id": ElementIds.NAVBAR_CONTAINER, + }, + id=ElementIds.APP_SHELL, ) + + +@callback( + [ + Output(ElementIds.APP_SHELL, "navbar"), + Output(ElementIds.TOOLS_MENU_EXPANDED, "data"), + ], + [ + Input(ElementIds.BURGER_BUTTON, "opened"), + Input(ElementIds.NAV_GROUP_CONTROLS, "opened"), + Input(ElementIds.NAV_GROUP_MAIN, "opened"), + ], + [ + State(ElementIds.APP_SHELL, "navbar"), + State(ElementIds.TOOLS_MENU_EXPANDED, "data"), + ], +) +def toggle_navbar_and_width( + burger_opened, tools_opened, pages_opened, navbar, tools_expanded +): + navbar["collapsed"] = {"mobile": not burger_opened, "desktop": not burger_opened} + + WIDTHS = {"default": 230, "pages": 230, "tools": 230} + + if tools_opened is not None: + tools_expanded = tools_opened + navbar["width"] = ( + WIDTHS["tools"] + if tools_opened + else (WIDTHS["pages"] if pages_opened else WIDTHS["default"]) + ) + elif pages_opened is not None: + navbar["width"] = WIDTHS["pages"] if pages_opened else WIDTHS["default"] + + return navbar, tools_expanded + + +@callback( + [ + Output(f"nav-{page[ColNames.PATH].replace('/', '')}", "active") + for page in dash.page_registry.values() + if page[ColNames.NAME] not in ["404"] + ], + Input(ElementIds.MAIN_URL, "pathname"), + prevent_initial_call=True, +) +def update_nav_active_state(pathname): + return [ + pathname == page[ColNames.PATH] + for page in dash.page_registry.values() + if page[ColNames.NAME] not in ["404"] + ] + + +@callback( + Output(ElementIds.ID_LAYOUT_ALERT_AUTO, "style"), + Input(ElementIds.ID_LAYOUT_INTERVAL_COMPONENT, "n_intervals"), + prevent_initial_call=True, +) +def show_alert_after_delay(n_intervals): + return {"display": "block" if n_intervals == 1 else "none"} diff --git a/pages/lib/template_graphs.py b/pages/lib/template_graphs.py index f8e3641..4916381 100644 --- a/pages/lib/template_graphs.py +++ b/pages/lib/template_graphs.py @@ -71,6 +71,7 @@ def violin(df, var, global_local, si_ip): title=title, title_x=0.5, dragmode=False, + height=400, ) fig.update_xaxes(showline=True, linewidth=1, linecolor="black", mirror=True) fig.update_yaxes( @@ -642,7 +643,7 @@ def thermal_stress_stacked_barchart( style={"text-align": "center", "marginTop": "2rem"}, ), ) - isNormalized = True if len(normalize) != 0 else False + isNormalized = True if normalize else False if isNormalized: new_df = ( df.groupby(ColNames.MONTH)[var] @@ -674,7 +675,7 @@ def thermal_stress_stacked_barchart( "
Month: %{x}
Category: " + categories[i] + "
Count: %{y}
" - if len(normalize) == 0 + if not normalize else "
Month: %{x}
Category: " + categories[i] + "
Proportion: %{y:.1f}%
" diff --git a/pages/lib/utils.py b/pages/lib/utils.py index 539ca23..c95c908 100644 --- a/pages/lib/utils.py +++ b/pages/lib/utils.py @@ -3,9 +3,9 @@ import time import math -import dash_bootstrap_components as dbc import pandas as pd from dash import html, dash_table, dcc +import dash_mantine_components as dmc from config import UnitSystem from pages.lib.global_scheme import fig_config, mapping_dictionary, month_lst @@ -147,38 +147,34 @@ def generate_custom_inputs_psy( def title_with_tooltip(text, tooltip_text, id_button): - display_tooltip = "none" if tooltip_text: - display_tooltip = "block" - - return html.Div( - className="container-row", - style={"padding": "1rem", "marginTop": "1rem"}, - children=[ - html.H5(text, style={"marginRight": "0.5rem"}), - html.Div( - [ - html.Sup( - html.Img( + return dmc.Group( + children=[ + dmc.Title(text, order=3), + dmc.Tooltip( + label=tooltip_text, + position="right", + withArrow=True, + children=[ + dmc.Image( id=id_button, - src="../assets/icons/help.png", + src="/assets/icons/help.png", alt="help", - style={ - "width": "1rem", - "height": "1rem", - }, - ), - ), - dbc.Tooltip( - tooltip_text, - target=id_button, - placement="right", - ), - ], - style={"display": display_tooltip}, - ), - ], - ) + w=16, + h=16, + ) + ], + ), + ], + ) + else: + return dmc.Group( + mt="md", + px="md", + children=[ + dmc.Title(text, order=3), + ], + ) def title_with_link( @@ -187,33 +183,25 @@ def title_with_link( id_button=None, doc_link: str = "", ): - return html.Div( - className="container-row", - style={"padding": "1rem", "marginTop": "1rem"}, + return dmc.Group( children=[ - html.H5(text, style={"marginRight": "0.5rem"}), - html.Div( - [ - html.Sup( - html.A( - html.Img( - id=id_button, - src="../assets/icons/book.png", - alt="book", - style={ - "width": "1rem", - "height": "1rem", - }, - ), - href=doc_link, - target="_blank", + dmc.Title(text, order=3), + dmc.Tooltip( + label=tooltip_text, + position="right", + withArrow=True, + children=[ + html.A( + dmc.Image( + id=id_button, + src="/assets/icons/book.png", + alt="book", + w=16, + h=16, ), - ), - dbc.Tooltip( - tooltip_text, - target=id_button, - placement="right", - ), + href=doc_link, + target="_blank", + ) ], ), ], @@ -247,9 +235,11 @@ def summary_table_tmp_rh_tab(df, value, si_ip): ) return dash_table.DataTable( columns=[ - {"name": i, "id": i} - if i == ColNames.MONTH - else {"name": f"{i} ({unit})", "id": i} + ( + {"name": i, "id": i} + if i == ColNames.MONTH + else {"name": f"{i} ({unit})", "id": i} + ) for i in df_summary.columns ], style_table={"overflowX": "auto"}, @@ -288,6 +278,7 @@ def dropdown(options=None, **kwargs): return dcc.Dropdown( options=[{"label": k, "value": v} for k, v in options.items()], clearable=False, + style={"width": "14rem"}, **kwargs, ) diff --git a/pages/natural_ventilation.py b/pages/natural_ventilation.py index ee002e2..4cd2cc9 100644 --- a/pages/natural_ventilation.py +++ b/pages/natural_ventilation.py @@ -1,6 +1,6 @@ import dash -from dash import dcc, html -import dash_bootstrap_components as dbc +from dash import dcc +import dash_mantine_components as dmc from dash_extensions.enrich import Output, Input, State, callback import numpy as np @@ -12,8 +12,6 @@ mapping_dictionary, tight_margins, month_lst, - container_row_center_full, - container_col_center_one_of_three, ) from pages.lib.utils import get_max_min_value from pages.lib.template_graphs import filter_df_by_month_and_hour @@ -41,13 +39,7 @@ def layout(): - return html.Div( - className="container-col", - id=ElementIds.MAIN_NV_SECTION, - children=[ - # - ], - ) + return dmc.Stack(p="md", id=ElementIds.MAIN_NV_SECTION) @callback( @@ -65,119 +57,104 @@ def update_layout(si_ip): dpt_set = 16 return [ - html.Div( - children=title_with_link( - text="Natural Ventilation Potential", - id_button=IdButtons.NATURAL_VENTILATION_LABEL, - doc_link=DocLinks.NATURAL_VENTILATION, - ), + title_with_link( + text="Natural Ventilation Potential", + id_button=IdButtons.NATURAL_VENTILATION_LABEL, + doc_link=DocLinks.NATURAL_VENTILATION, ), inputs_tab(tdb_set_min, tdb_set_max, dpt_set), dcc.Loading( - html.Div( + type="circle", + children=dmc.Paper( id=ElementIds.NV_HEATMAP_CHART, - style={"marginTop": "1rem"}, ), - type="circle", ), - html.Div( - className="container-row align-center justify-center", + dmc.Group( + justify="center", children=[ - dbc.Checklist( - options=[ - {"label": "", "value": 1}, - ], - value=[1], + dmc.Switch( id=ElementIds.SWITCHES_INPUT, - switch=True, - style={ - "padding": "1rem", - "marginTop": "1rem", - "marginRight": "-2rem", - }, + label="", + checked=True, + color="blue", + style={"padding": "1rem", "marginRight": "-2rem"}, ), - html.Div( - children=title_with_tooltip( - text="Normalize data", - tooltip_text=( - "If normalized is enabled it calculates the % " - "time otherwise it calculates the total number of hours" - ), - id_button=IdButtons.NV_NORMALIZE, + title_with_tooltip( + text="Normalize data", + tooltip_text=( + "If normalized is enabled it calculates the % " + "time otherwise it calculates the total number of hours" ), + id_button=IdButtons.NV_NORMALIZE, ), ], ), dcc.Loading( - html.Div( + type="circle", + children=dmc.Paper( id=ElementIds.NV_BAR_CHART, - style={"marginTop": "1rem"}, ), - type="circle", ), ] def inputs_tab(t_min, t_max, d_set): - return html.Div( - className="container-row full-width three-inputs-container", + return dmc.SimpleGrid( + cols=3, + spacing="md", children=[ - html.Div( - className=container_col_center_one_of_three, - children=[ - dbc.Button( + dmc.Stack( + [ + dmc.Button( "Apply filter", - color="primary", + color="blue", id=ElementIds.NV_DBT_FILTER, - className="mb-2", + variant="link", n_clicks=1, ), - html.H6("Outdoor dry-bulb air temperature range"), - html.Div( - className=container_row_center_full, - children=[ - html.H6(children=["Min Value:"], style={"flex": "30%"}), - dbc.Input( - id=ElementIds.NV_TDB_MIN_VAL, - placeholder="Enter a number for the min val", - type="number", - step=1, - value=t_min, - style={"flex": "70%"}, + dmc.Title("Outdoor dry-bulb air temperature range", order=5), + dmc.Group( + [ + dmc.Title("Min Value:", order=5), + dmc.Stack( + dmc.NumberInput( + id=ElementIds.NV_TDB_MIN_VAL, + placeholder="Enter a number for the min val", + step=1, + value=t_min, + ), + flex=1, ), ], ), - html.Div( - className=container_row_center_full, - children=[ - html.H6(children=["Max Value:"], style={"flex": "30%"}), - dbc.Input( - id=ElementIds.NV_TDB_MAX_VAL, - placeholder="Enter a number for the max val", - type="number", - value=t_max, - step=1, - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Max Value:", order=5), + dmc.Stack( + dmc.NumberInput( + id=ElementIds.NV_TDB_MAX_VAL, + placeholder="Enter a number for the max val", + value=t_max, + step=1, + ), + flex=1, ), ], ), - ], + ] ), - html.Div( - className=container_col_center_one_of_three, - children=[ - dbc.Button( + dmc.Stack( + [ + dmc.Button( "Apply month and hour filter", - color="primary", + color="blue", id=ElementIds.NV_MONTH_HOUR_FILTER, - className="mb-2", - n_clicks=0, + variant="link", ), - html.Div( - className="container-row full-width justify-center mt-2", - children=[ - html.H6("Month Range", style={"flex": "20%"}), - html.Div( + dmc.Group( + [ + dmc.Title("Month Range", order=5), + dmc.Stack( dcc.RangeSlider( id=ElementIds.NV_MONTH_SLIDER, min=1, @@ -185,29 +162,20 @@ def inputs_tab(t_min, t_max, d_set): step=1, value=[1, 12], marks={1: "1", 12: "12"}, - tooltip={ - "always_visible": False, - "placement": "top", - }, - allowCross=False, ), - style={"flex": "50%"}, + flex=1, ), dcc.Checklist( - options=[ - {"label": "Invert", "value": "invert"}, - ], + options=[{"label": "Invert", "value": "invert"}], value=[], id=ElementIds.INVERT_MONTH_NV, - labelStyle={"flex": "30%"}, ), ], ), - html.Div( - className="container-row align-center justify-center", - children=[ - html.H6("Hour Range", style={"flex": "20%"}), - html.Div( + dmc.Group( + [ + dmc.Title("Hour Range", order=5), + dmc.Stack( dcc.RangeSlider( id=ElementIds.NV_HOUR_SLIDER, min=0, @@ -215,70 +183,56 @@ def inputs_tab(t_min, t_max, d_set): step=1, value=[0, 24], marks={0: "0", 24: "24"}, - tooltip={ - "always_visible": False, - "placement": "topLeft", - }, - allowCross=False, ), - style={"flex": "50%"}, + flex=1, ), dcc.Checklist( - options=[ - {"label": "Invert", "value": "invert"}, - ], + options=[{"label": "Invert", "value": "invert"}], value=[], id=ElementIds.INVERT_HOUR_NV, - labelStyle={"flex": "30%"}, ), ], ), - ], + ] ), - html.Div( - className=container_col_center_one_of_three, - children=[ - dbc.Button( + dmc.Stack( + [ + dmc.Button( "Apply filter", - color="primary", + color="blue", id=ElementIds.NV_DPT_FILTER, - className="mb-2", - n_clicks=0, + variant="link", disabled=True, ), - dbc.Checklist( + 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." + "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, ), - html.Div( - className=container_row_center_full, - children=[ - html.H6( - children=["Surface temperature:"], - style={"marginRight": "1rem"}, - ), - dbc.Input( - id=ElementIds.NV_DPT_MAX_VAL, - placeholder="Enter a number for the max val", - type="number", - value=d_set, - step=1, - style={"flex": "1"}, + dmc.Group( + [ + dmc.Title("Surface temperature:", order=5), + dmc.Stack( + dmc.NumberInput( + id=ElementIds.NV_DPT_MAX_VAL, + placeholder="Enter a number for the max val", + value=d_set, + step=1, + ), + flex=1, ), ], ), - ], + ] ), ], ) @@ -343,12 +297,15 @@ def nv_heatmap( if df.dropna(subset=[ColNames.MONTH]).shape[0] == 0: return ( - dbc.Alert( - "Natural ventilation is not available in this location under these" - " conditions. Please either select a different outdoor dry-bulb air" - " temperature range, change the month and hour filter, or increase" - " thedew-point temperature.", - color="danger", + dmc.Alert( + title="Notice", + color="red", + children=( + "Natural ventilation is not available in this location under these " + "conditions. Please either select a different outdoor dry-bulb air " + "temperature range, change the month and hour filter, or increase " + "the dew-point temperature." + ), style={"text-align": "center", "marginTop": "2rem"}, ), ) @@ -457,7 +414,7 @@ def nv_heatmap( Input(ElementIds.NV_MONTH_HOUR_FILTER, "n_clicks"), Input(ElementIds.NV_DBT_FILTER, "n_clicks"), Input(ElementIds.NV_DPT_FILTER, "n_clicks"), - Input(ElementIds.SWITCHES_INPUT, "value"), + Input(ElementIds.SWITCHES_INPUT, "checked"), Input(ElementIds.ENABLE_CONDENSATION, "value"), ], [ @@ -538,7 +495,7 @@ def nv_bar_chart( per_time_nv_allowed = np.round(100 * (n_hours_nv_allowed / tot_month_hours)) - if len(normalize) == 0: + if not normalize: fig = go.Figure( go.Bar( x=df[ColNames.MONTH_NAMES].unique(), diff --git a/pages/not_found_404.py b/pages/not_found_404.py index ff3b389..9fc307e 100644 --- a/pages/not_found_404.py +++ b/pages/not_found_404.py @@ -13,7 +13,7 @@ layout = [ dmc.Title("I could not find the page you are currently looking for", order=4), - dmc.Text("Use the button below to return to the home page.", className="mb-2"), + dmc.Text("Use the button below to return to the home page.", mb="sm"), Lottie( options=dict( loop=True, @@ -27,7 +27,7 @@ dmc.Button( "Home page", fullWidth=True, - leftIcon=DashIconify(icon="material-symbols:home-outline-rounded"), + leftSection=DashIconify(icon="material-symbols:home-outline-rounded"), ), href=PageUrls.SELECT.value, ), diff --git a/pages/outdoor.py b/pages/outdoor.py index 0e4885a..a7d9ba4 100644 --- a/pages/outdoor.py +++ b/pages/outdoor.py @@ -1,6 +1,6 @@ import dash from dash import dcc, html -import dash_bootstrap_components as dbc +import dash_mantine_components as dmc from dash_extensions.enrich import Output, Input, State, callback import numpy as np @@ -36,52 +36,38 @@ def inputs_outdoor_comfort(): - return dbc.Row( - className="container-row full-width three-inputs-container", + return dmc.SimpleGrid( + cols=2, children=[ - dbc.Col( - md=6, - sm=12, - children=[ - html.Div( - className="container-row center-block", - children=[ - html.H4( - children=["Select a scenario:"], - style={"flex": "30%"}, - ), - dropdown( - id=ElementIds.TAB7_DROPDOWN, - style={"flex": "60%"}, - options=outdoor_dropdown_names, - value="utci_Sun_Wind", - ), - html.Div( - id=ElementIds.IMAGE_SELECTION, style={"flex": "10%"} - ), - ], + 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", ), - dbc.Col( - md=6, - sm=12, - children=[ - dbc.Button( + dmc.Stack( + [ + dmc.Button( "Apply month and hour filter", - color="primary", - style={ - "width": "100%", - }, id=ElementIds.MONTH_HOUR_FILTER_OUTDOOR_COMFORT, - className="mb-2", - n_clicks=0, + variant="filled", + color="blue", ), - html.Div( - className="container-row full-width justify-center mt-2", - children=[ - html.H6("Month Range", style={"flex": "5%"}), - html.Div( + dmc.Group( + [ + dmc.Title("Month Range", order=5), + dmc.Stack( dcc.RangeSlider( id=ElementIds.OUTDOOR_COMFORT_MONTH_SLIDER, min=1, @@ -89,29 +75,21 @@ def inputs_outdoor_comfort(): step=1, value=[1, 12], marks={1: "1", 12: "12"}, - tooltip={ - "always_visible": False, - "placement": "top", - }, - allowCross=False, + tooltip={"always_visible": False}, ), - style={"flex": "50%"}, + flex=1, ), dcc.Checklist( - options=[ - {"label": "Invert", "value": "invert"}, - ], - value=[], id=ElementIds.INVERT_MONTH_OUTDOOR_COMFORT, - labelStyle={"flex": "30%"}, + options=[{"label": "Invert", "value": "invert"}], + value=[], ), ], ), - html.Div( - className="container-row align-center justify-center", - children=[ - html.H6("Hour Range", style={"flex": "5%"}), - html.Div( + dmc.Group( + [ + dmc.Title("Hour Range", order=5), + dmc.Stack( dcc.RangeSlider( id=ElementIds.OUTDOOR_COMFORT_HOUR_SLIDER, min=0, @@ -119,21 +97,14 @@ def inputs_outdoor_comfort(): step=1, value=[0, 24], marks={0: "0", 24: "24"}, - tooltip={ - "always_visible": False, - "placement": "topLeft", - }, - allowCross=False, + tooltip={"always_visible": False}, ), - style={"flex": "50%"}, + flex=1, ), dcc.Checklist( - options=[ - {"label": "Invert", "value": "invert"}, - ], - value=[], id=ElementIds.INVERT_HOUR_OUTDOOR_COMFORT, - labelStyle={"flex": "30%"}, + options=[{"label": "Invert", "value": "invert"}], + value=[], ), ], ), @@ -144,76 +115,76 @@ def inputs_outdoor_comfort(): def outdoor_comfort_chart(): - return html.Div( + return dmc.Stack( children=[ - html.Div(id=ElementIds.OUTDOOR_COMFORT_OUTPUT), - html.Div( - children=title_with_link( - text="UTCI heatmap chart", - id_button=IdButtons.UTCI_CHARTS_LABEL, - doc_link=DocLinks.UTCI_CHART, - ) + dmc.Paper( + id=ElementIds.OUTDOOR_COMFORT_OUTPUT, + ), + title_with_link( + text="UTCI heatmap chart", + id_button=IdButtons.UTCI_CHARTS_LABEL, + doc_link=DocLinks.UTCI_CHART, ), dcc.Loading( - html.Div(id=ElementIds.UTCI_HEATMAP), type="circle", + children=dmc.Paper( + id=ElementIds.UTCI_HEATMAP, + ), ), - html.Div( - children=title_with_link( - text="UTCI thermal stress chart", - id_button=IdButtons.UTCI_CHARTS_LABEL, - doc_link=DocLinks.UTCI_CHART, - ) + title_with_link( + text="UTCI thermal stress chart", + id_button=IdButtons.UTCI_CHARTS_LABEL, + doc_link=DocLinks.UTCI_CHART, ), dcc.Loading( - html.Div(id=ElementIds.UTCI_CATEGORY_HEATMAP), type="circle", + children=dmc.Paper( + id=ElementIds.UTCI_CATEGORY_HEATMAP, + ), ), - html.Div( - className="container-row align-center justify-center", + dmc.Group( + justify="center", children=[ - dbc.Checklist( - options=[ - {"label": "", "value": 1}, - ], - value=[1], + dmc.Switch( id=ElementIds.OUTDOOR_COMFORT_SWITCHES_INPUT, - switch=True, - style={ - "padding": "1rem", - "marginTop": "1rem", - "marginRight": "-2rem", - }, + checked=True, + color="blue", ), - html.Div( - children=title_with_tooltip( - text="Normalize data", - tooltip_text=( - "If normalized is enabled it calculates the % " - "time otherwise it calculates the total number of hours" - ), - id_button=IdButtons.OUTDOOR_COMFORT_NORMALIZE, + title_with_tooltip( + text="Normalize data", + tooltip_text=( + "If normalized is enabled it calculates the % time " + "otherwise it calculates the total number of hours" ), + id_button=IdButtons.OUTDOOR_COMFORT_NORMALIZE, ), ], ), dcc.Loading( - html.Div(id=ElementIds.UTCI_SUMMARY_CHART), type="circle", + children=dmc.Paper( + id=ElementIds.UTCI_SUMMARY_CHART, + # p="sm", + ), ), ], ) def layout(): - return ( - dcc.Loading( - type="circle", - children=html.Div( - className="container-col", - children=[inputs_outdoor_comfort(), outdoor_comfort_chart()], + return dmc.Stack( + p="md", + children=[ + dcc.Loading( + type="circle", + children=dmc.Stack( + children=[ + inputs_outdoor_comfort(), + outdoor_comfort_chart(), + ], + ), ), - ), + ], ) @@ -406,7 +377,7 @@ def update_tab_utci_category( [ Input(ElementIds.TAB7_DROPDOWN, "value"), Input(ElementIds.MONTH_HOUR_FILTER_OUTDOOR_COMFORT, "n_clicks"), - Input(ElementIds.OUTDOOR_COMFORT_SWITCHES_INPUT, "value"), + Input(ElementIds.OUTDOOR_COMFORT_SWITCHES_INPUT, "checked"), ], [ State(ElementIds.ID_OUTDOOR_DF_STORE, "data"), diff --git a/pages/psy-chart.py b/pages/psy-chart.py index bf96775..4aca01e 100644 --- a/pages/psy-chart.py +++ b/pages/psy-chart.py @@ -1,6 +1,6 @@ import dash -from dash import dcc, html -import dash_bootstrap_components as dbc +from dash import dcc +import dash_mantine_components as dmc from dash_extensions.enrich import Output, Input, State, callback from copy import deepcopy @@ -11,14 +11,11 @@ from config import PageUrls, DocLinks, PageInfo, UnitSystem from pages.lib.utils import get_max_min_value -from pages.lib.extract_df import convert_SI_to_IP from pages.lib.global_element_ids import ElementIds from pages.lib.global_column_names import ColNames from pages.lib.global_id_buttons import IdButtons from pages.lib.global_tab_names import TabNames from pages.lib.global_scheme import ( - container_row_center_full, - container_col_center_one_of_three, dropdown_names, sun_cloud_tab_dropdown_names, more_variables_dropdown, @@ -60,47 +57,36 @@ def inputs(): - """""" - return html.Div( - className="container-row full-width three-inputs-container", + return dmc.SimpleGrid( + cols=3, children=[ - html.Div( - className=container_col_center_one_of_three, - children=[ - html.Div( - className=container_row_center_full, - children=[ - html.H6( - children=["Color By:"], - style={"flex": "30%"}, - ), - dropdown( - id=ElementIds.PSY_COLOR_BY_DROPDOWN, - options=psy_dropdown_names, - value="Frequency", - style={"flex": "70%"}, - persistence_type="session", - persistence=True, - ), - ], + dmc.Group( + [ + 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, ), ], + align="flex-start", ), - html.Div( - className=container_col_center_one_of_three, - children=[ - dbc.Button( + dmc.Stack( + [ + dmc.Button( "Apply month and hour filter", - color="primary", id=ElementIds.MONTH_HOUR_FILTER, - className="mb-2", - n_clicks=0, + color="blue", ), - html.Div( - className="container-row full-width justify-center mt-2", - children=[ - html.H6("Month Range", style={"flex": "20%"}), - html.Div( + dmc.Group( + [ + dmc.Title("Month Range", order=5), + dmc.Stack( dcc.RangeSlider( id=ElementIds.PSY_MONTH_SLIDER, min=1, @@ -108,29 +94,21 @@ def inputs(): step=1, value=[1, 12], marks={1: "1", 12: "12"}, - tooltip={ - "always_visible": False, - "placement": "top", - }, - allowCross=False, + tooltip={"always_visible": False}, ), - style={"flex": "50%"}, + flex=1, ), dcc.Checklist( - options=[ - {"label": "Invert", "value": "invert"}, - ], - value=[], id=ElementIds.INVERT_MONTH_PSY, - labelStyle={"flex": "30%"}, + options=[{"label": "Invert", "value": "invert"}], + value=[], ), ], ), - html.Div( - className="container-row align-center justify-center", - children=[ - html.H6("Hour Range", style={"flex": "20%"}), - html.Div( + dmc.Group( + [ + dmc.Title("Hour Range", order=5), + dmc.Stack( dcc.RangeSlider( id=ElementIds.PSY_HOUR_SLIDER, min=0, @@ -138,75 +116,64 @@ def inputs(): step=1, value=[0, 24], marks={0: "0", 24: "24"}, - tooltip={ - "always_visible": False, - "placement": "topLeft", - }, - allowCross=False, + tooltip={"always_visible": False}, ), - style={"flex": "50%"}, + flex=1, ), dcc.Checklist( - options=[ - {"label": "Invert", "value": "invert"}, - ], - value=[], id=ElementIds.INVERT_HOUR_PSY, - labelStyle={"flex": "30%"}, + options=[{"label": "Invert", "value": "invert"}], + value=[], ), ], ), ], ), - html.Div( - className=container_col_center_one_of_three, - children=[ - dbc.Button( + dmc.Stack( + [ + dmc.Button( "Apply filter", - color="primary", id=ElementIds.DATA_FILTER, - className="mb-2", - n_clicks=0, + color="blue", ), - html.Div( - className=container_row_center_full, - children=[ - html.H6( - children=["Filter Variable:"], style={"flex": "30%"} - ), - dropdown( - id=ElementIds.PSY_VAR_DROPDOWN, - options=dropdown_names, - value=ColNames.RH, - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Filter Variable:", order=5), + dmc.Stack( + dropdown( + id=ElementIds.PSY_VAR_DROPDOWN, + options=dropdown_names, + value=ColNames.RH, + ), + flex=1, ), ], ), - html.Div( - className=container_row_center_full, - children=[ - html.H6(children=["Min Value:"], style={"flex": "30%"}), - dbc.Input( - id=ElementIds.PSY_MIN_VAL, - placeholder="Enter a number for the min val", - type="number", - step=1, - value=0, - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Min Value:", order=5), + dmc.Stack( + dmc.NumberInput( + id=ElementIds.PSY_MIN_VAL, + placeholder="Enter a number for the min val", + value=0, + step=1, + ), + flex=1, ), ], ), - html.Div( - className=container_row_center_full, - children=[ - html.H6(children=["Max Value:"], style={"flex": "30%"}), - dbc.Input( - id=ElementIds.PSY_MAX_VAL, - placeholder="Enter a number for the max val", - type="number", - value=100, - step=1, - style={"flex": "70%"}, + dmc.Group( + [ + dmc.Title("Max Value:", order=5), + dmc.Stack( + dmc.NumberInput( + id=ElementIds.PSY_MAX_VAL, + placeholder="Enter a number for the max val", + value=100, + step=1, + ), + flex=1, ), ], ), @@ -217,25 +184,29 @@ def inputs(): def layout(): - return ( - html.Div( - children=title_with_link( + return dmc.Stack( + p="md", + children=[ + title_with_link( text="Psychrometric Chart", id_button=IdButtons.PSYCHROMETRIC_CHART_CHART, doc_link=DocLinks.PSYCHROMETRIC_CHART, ), - ), - dcc.Loading( - type="circle", - children=html.Div( - className="container-col", - children=[inputs(), html.Div(id=ElementIds.PSYCH_CHART)], + dcc.Loading( + type="circle", + children=dmc.Stack( + children=[ + inputs(), + dmc.Paper( + id=ElementIds.PSYCH_CHART, + ), + ], + ), ), - ), + ], ) -# psychrometric chart @callback( Output(ElementIds.PSYCH_CHART, "children"), [ @@ -293,7 +264,7 @@ def update_psych_chart( if df.dropna(subset=[ColNames.MONTH]).shape[0] == 0: return ( - dbc.Alert( + dmc.Alert( "No data is available in this location under these conditions. Please " "either change the month and hour filters, or select a wider range for " "the filter variable", @@ -357,7 +328,7 @@ def update_psych_chart( if si_ip == UnitSystem.IP: for j in range(len(dbt_list)): - convert_SI_to_IP(dbt_list_convert, j) + dbt_list_convert[j] = dbt_list_convert[j] * 1.8 + 32 fig.add_trace( go.Scatter( diff --git a/pages/select.py b/pages/select.py index 86c22d8..63db215 100644 --- a/pages/select.py +++ b/pages/select.py @@ -3,7 +3,6 @@ import re import dash -import dash_bootstrap_components as dbc import dash_mantine_components as dmc import pandas as pd import plotly.express as px @@ -39,8 +38,8 @@ def layout(): """Contents in the first tab 'Select Weather File'""" - return html.Div( - className="container-col tab-container", + return dmc.Stack( + p="md", children=[ dcc.Loading( id=ElementIds.LOADING_ONE, @@ -50,49 +49,59 @@ def layout(): ), dcc.Upload( id=ElementIds.UPLOAD_DATA, - children=dbc.Button( + children=dmc.Button( [ "Drag and Drop or ", html.A("Select an EPW file from your computer"), ], id=ElementIds.UPLOAD_DATA_BUTTON, - outline=True, - color="secondary", - className="mt-2", - style={"borderRadius": "5px", "borderStyle": "dashed"}, + variant="outline", + color="gray", + style={"borderStyle": "dashed"}, + styles={"label": {"fontWeight": 400}}, ), # Allow multiple files to be uploaded multiple=True, - className="d-grid", + style={"display": "grid"}, ), dmc.Skeleton( visible=False, id=ElementIds.SKELETON_GRAPH_CONTAINER, height=500, - children=html.Div(id=ElementIds.TAB_ONE_MAP), + children=dmc.Box(id=ElementIds.TAB_ONE_MAP), ), - dbc.Modal( - [ - dbc.ModalHeader(id=ElementIds.MODAL_HEADER), - dbc.ModalFooter( - children=[ - dbc.Button( + dmc.Modal( + id=ElementIds.MODAL, + title=dmc.Text(id=ElementIds.MODAL_HEADER), + opened=False, + centered=True, + children=[ + dmc.Divider( + size="xs", + color="gray", + my="sm", + style={ + "borderTop": "1px solid var(--mantine-color-gray-4)", + "marginTop": "-6px", + }, + ), + dmc.Group( + [ + dmc.Button( "Close", id=ElementIds.MODAL_CLOSE_BUTTON, - className="ml-2", - color="light", + color="gray", + variant="outline", ), - dbc.Button( + dmc.Button( "Yes", id=ElementIds.MODAL_YES_BUTTON, - className="ml-2", - color="primary", + color="blue", ), - ] + ], + justify="flex-end", ), ], - id=ElementIds.MODAL, - is_open=False, ), ], ) @@ -100,12 +109,11 @@ def layout(): def alert(): """Alert layout for the submit button.""" - return dbc.Alert( + return dmc.Alert( messages_alert["start"], - color="primary", + color="blue", id=ElementIds.ALERT, - dismissable=False, - is_open=True, + withCloseButton=False, style={"maxHeight": "66px"}, ) @@ -115,7 +123,7 @@ def alert(): [ Output(ElementIds.ID_SELECT_META_STORE, "data"), Output(ElementIds.ID_SELECT_LINES_STORE, "data"), - Output(ElementIds.ALERT, "is_open"), + Output(ElementIds.ALERT, "visible"), Output(ElementIds.ALERT, "children"), Output(ElementIds.ALERT, "color"), ], @@ -149,7 +157,7 @@ def submitted_data( None, True, messages_alert["not_available"], - "warning", + "orange", ) location_info = get_location_info( lines, url_store @@ -159,7 +167,7 @@ def submitted_data( lines, True, messages_alert["success"], - "success", + "green", ) elif ( @@ -183,7 +191,7 @@ def submitted_data( lines, True, messages_alert["success"], - "success", + "green", ) else: return ( @@ -191,7 +199,7 @@ def submitted_data( None, True, messages_alert["invalid_format"], - "warning", + "orange", ) except (ValueError, IndexError, KeyError) as e: print(f"Error parsing EPW file: {e}") @@ -200,7 +208,7 @@ def submitted_data( None, True, messages_alert["wrong_extension"], - "warning", + "orange", ) raise PreventUpdate @@ -236,15 +244,16 @@ def switch_si_ip(_, si_ip_input, url_store, lines): @callback( [ - Output("/", "disabled"), - Output("/summary", "disabled"), - Output("/t-rh", "disabled"), - Output("/sun", "disabled"), - Output("/wind", "disabled"), - Output("/psy-chart", "disabled"), - Output("/explorer", "disabled"), - Output("/outdoor", "disabled"), - Output("/natural-ventilation", "disabled"), + Output(ElementIds.NAV, "disabled"), + Output(ElementIds.NAV_SUMMARY, "disabled"), + Output(ElementIds.NAV_T_RH, "disabled"), + Output(ElementIds.NAV_SUN, "disabled"), + Output(ElementIds.NAV_WIND, "disabled"), + Output(ElementIds.NAV_PSY_CHART, "disabled"), + 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"), ], [ @@ -266,6 +275,7 @@ def enable_tabs_when_data_is_loaded(meta, data): True, True, True, + True, # changelog always disabled default, ) else: @@ -279,13 +289,14 @@ def enable_tabs_when_data_is_loaded(meta, data): False, False, False, + True, # changelog always disabled "Current Location: " + meta[ColNames.CITY] + ", " + meta[ColNames.COUNTRY], ) @callback( [ - Output(ElementIds.MODAL, "is_open"), + Output(ElementIds.MODAL, "opened"), Output(ElementIds.ID_SELECT_URL_STORE, "data"), ], [ @@ -293,30 +304,25 @@ def enable_tabs_when_data_is_loaded(meta, data): Input(ElementIds.TAB_ONE_MAP, "clickData"), Input(ElementIds.MODAL_CLOSE_BUTTON, "n_clicks"), ], - [State(ElementIds.MODAL, "is_open")], + [State(ElementIds.MODAL, "opened")], prevent_initial_call=True, ) -def display_modal_when_data_clicked(_, click_map, __, is_open): +def display_modal_when_data_clicked(_, click_map, __, opened): """display the modal to the user and check if he wants to use that file""" if click_map: url = re.search( r'href=[\'"]?([^\'" >]+)', click_map["points"][0]["customdata"][-1] ).group(1) - return not is_open, url - return is_open, "" + return not opened, url + return opened, "" @callback( - [ - Output(ElementIds.MODAL_HEADER, "children"), - ], - [ - Input(ElementIds.TAB_ONE_MAP, "clickData"), - ], + [Output(ElementIds.MODAL_HEADER, "children")], + [Input(ElementIds.TAB_ONE_MAP, "clickData")], prevent_initial_call=True, ) def change_text_modal(click_map): - """change the text of the modal header""" if click_map: return [f"Analyse data from {click_map['points'][0]['hovertext']}?"] return ["Analyse data from this location?"] @@ -324,7 +330,7 @@ def change_text_modal(click_map): @callback( Output(ElementIds.SKELETON_GRAPH_CONTAINER, "children"), - Input("url", "pathname"), + Input(ElementIds.SELECT_URL, "pathname"), ) def plot_location_epw_files(pathname): # print(pathname) @@ -374,10 +380,8 @@ def plot_location_epw_files(pathname): fig.update_layout(mapbox_style="carto-positron") fig.update_layout(margin={"r": 0, "t": 0, "l": 0, "b": 0}) - return ( - dcc.Graph( - id=ElementIds.TAB_ONE_MAP, - figure=fig, - config=generate_chart_name(TabNames.EPW_LOCATION_SELECT), - ), + return dcc.Graph( + id=ElementIds.TAB_ONE_MAP, + figure=fig, + config=generate_chart_name(TabNames.EPW_LOCATION_SELECT), ) diff --git a/pages/summary.py b/pages/summary.py index de5412d..7f11a30 100644 --- a/pages/summary.py +++ b/pages/summary.py @@ -1,11 +1,11 @@ import dash -import dash_bootstrap_components as dbc -from dash.exceptions import PreventUpdate -from dash_extensions.enrich import dcc, html, Output, Input, State, callback - +import pandas as pd +import dash_mantine_components as dmc import plotly.graph_objects as go import requests +from dash.exceptions import PreventUpdate +from dash_extensions.enrich import dcc, Output, Input, State, callback from config import PageUrls, DocLinks, PageInfo, UnitSystem from pages.lib.charts_summary import world_map from pages.lib.extract_df import get_data @@ -35,13 +35,7 @@ def layout(): """Contents in the second tab 'Climate Summary'.""" - return html.Div( - className="container-col", - id=ElementIds.TAB_TWO_CONTAINER, - children=[ - # - ], - ) + return dmc.Stack(id=ElementIds.TAB_TWO_CONTAINER, p="md") @callback( @@ -56,134 +50,112 @@ def update_layout(si_ip): heating_setpoint = 50 cooling_setpoint = 64 - return html.Div( - className="container-col", + return dmc.Stack( id=ElementIds.TAB2_SCE1_CONTAINER, children=[ dcc.Loading( type="circle", - children=html.Div( - className="container-col", + children=dmc.Stack( id=ElementIds.LOCATION_INFO, - style={"padding": "12px"}, + gap=0, ), ), dcc.Loading( type="circle", - children=html.Div(className="tab-two-section", id=ElementIds.WORLD_MAP), + children=dmc.Stack(id=ElementIds.WORLD_MAP), ), - html.Div( - children=title_with_tooltip( - text="Download", - id_button=IdButtons.DOWNLOAD_BUTTON_LABEL, - tooltip_text="Use the following buttons to download either the Clima sourcefile or the EPW file", - ), + title_with_tooltip( + text="Download", + 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", - children=dbc.Row( - [ - dbc.Col( - dbc.Button( - "Download EPW", - color="primary", - id=ElementIds.DOWN_EPW_BUTTON, - ), - width="auto", + children=dmc.Group( + children=[ + dmc.Button( + "Download EPW", + id=ElementIds.DOWN_EPW_BUTTON, + color="blue", + variant="filled", ), - dbc.Col( - dbc.Button( - "Download Clima dataframe", - color="primary", - id=ElementIds.DOWNLOAD_BUTTON, - ), - width="auto", - ), - dbc.Col( - [ - dcc.Download(id=ElementIds.DOWNLOAD_DATAFRAME_CSV), - dcc.Download(id=ElementIds.DOWNLOAD_EPW), - ], - width=1, + dmc.Button( + "Download Clima dataframe", + id=ElementIds.DOWNLOAD_BUTTON, + color="blue", + variant="filled", ), + dcc.Download(id=ElementIds.DOWNLOAD_DATAFRAME_CSV), + dcc.Download(id=ElementIds.DOWNLOAD_EPW), ], ), ), - html.Div( - children=title_with_link( - text="Heating and Cooling Degree Days", - id_button=IdButtons.HDD_CDD_CHART, - doc_link=DocLinks.DEGREE_DAYS, - ), - ), - dbc.Alert( - "WARNING: Invalid Results! The CDD setpoint should be higher than the HDD setpoint!", - color="warning", - is_open=False, - id=ElementIds.WARNING_CDD_HIGHER_HDD, + title_with_link( + text="Heating and Cooling Degree Days", + id_button=IdButtons.HDD_CDD_CHART, + doc_link=DocLinks.DEGREE_DAYS, ), - dbc.Row( - [ - dbc.Col( - html.Label( - "Heating degree day (HDD) setpoint", - ), - width="auto", - ), - dbc.Col( - dbc.Input( - id=ElementIds.INPUT_HDD_SET_POINT, - type="number", - value=heating_setpoint, - style={"width": "4rem"}, - ), - width="auto", - ), - dbc.Col( - html.Label( - "Cooling degree day (CDD) setpoint", - ), - width="auto", + dmc.Stack(id=ElementIds.WARNING_CDD_HIGHER_HDD), + dmc.Group( + justify="center", + children=[ + dmc.Text("Heating degree day (HDD) setpoint"), + dmc.NumberInput( + id=ElementIds.INPUT_HDD_SET_POINT, + value=heating_setpoint, + step=1, + min=-100, + max=100, + w=80, + hideControls=False, ), - dbc.Col( - dbc.Input( - id=ElementIds.INPUT_CDD_SET_POINT, - type="number", - value=cooling_setpoint, - style={"width": "4rem"}, - ), - width="auto", + dmc.Text("Cooling degree day (CDD) setpoint"), + dmc.NumberInput( + id=ElementIds.INPUT_CDD_SET_POINT, + value=cooling_setpoint, + step=1, + min=-100, + max=100, + w=80, + hideControls=False, ), - dbc.Col( - dbc.Button( - id=ElementIds.SUBMIT_SET_POINTS, - children="Submit", - color="primary", - ), - width="auto", + dmc.Button( + id=ElementIds.SUBMIT_SET_POINTS, + children="Submit", + color="blue", + variant="filled", ), ], - align="center", - justify="center", ), dcc.Loading( type="circle", - children=html.Div(id=ElementIds.DEGREE_DAYS_CHART_WRAPPER), + children=dmc.Stack(id=ElementIds.DEGREE_DAYS_CHART_WRAPPER), ), - html.Div( - children=title_with_link( - text="Climate Profiles", - id_button=IdButtons.CLIMATE_PROFILES_CHART, - doc_link=DocLinks.CLIMATE_PROFILES, - ), + title_with_link( + text="Climate Profiles", + id_button=IdButtons.CLIMATE_PROFILES_CHART, + doc_link=DocLinks.CLIMATE_PROFILES, ), - dbc.Row( + dmc.Grid( id=ElementIds.GRAPH_CONTAINER, + gutter="md", children=[ - dbc.Col(id=ElementIds.TEMP_PROFILE_GRAPH, width=12, md=6, lg=3), - dbc.Col(id=ElementIds.HUMIDITY_PROFILE_GRAPH, width=12, md=6, lg=3), - dbc.Col(id=ElementIds.SOLAR_RADIATION_GRAPH, width=12, md=6, lg=3), - dbc.Col(id=ElementIds.WIND_SPEED_GRAPH, width=12, md=6, lg=3), + dmc.GridCol( + id=ElementIds.TEMP_PROFILE_GRAPH, + span={"base": 12, "sm": 6, "lg": 3}, + ), + dmc.GridCol( + id=ElementIds.HUMIDITY_PROFILE_GRAPH, + span={"base": 12, "sm": 6, "lg": 3}, + ), + dmc.GridCol( + id=ElementIds.SOLAR_RADIATION_GRAPH, + span={"base": 12, "sm": 6, "lg": 3}, + ), + dmc.GridCol( + id=ElementIds.WIND_SPEED_GRAPH, + span={"base": 12, "sm": 6, "lg": 3}, + ), ], ), ], @@ -207,14 +179,11 @@ def update_layout(si_ip): ) def update_map(meta): """Update the contents of tab two. Passing in the general info (df, meta).""" - map_world = dcc.Graph( - id=ElementIds.GH_RAD_PROFILE_GRAPH, + return dcc.Graph( config=generate_chart_name(TabNames.MAP, meta), figure=world_map(meta), ) - return map_world - @callback( Output(ElementIds.LOCATION_INFO, "children"), @@ -228,202 +197,173 @@ def update_map(meta): def update_location_info(ts, df, meta, si_ip): """Update the contents of tab two. Passing in the general info (df, meta).""" location = f"Location: {meta[ColNames.CITY]}, {meta[ColNames.COUNTRY]}" - lon = f"Longitude: {meta[ColNames.LON]}" + lon = f" Longitude: {meta[ColNames.LON]}" lat = f"Latitude: {meta[ColNames.LAT]}" - site_elevation = float(meta[ColNames.SITE_ELEVATION]) - site_elevation = round(site_elevation, 2) - - elevation = f"Elevation above sea level: {str(site_elevation)} m" + site_elevation = round(float(meta[ColNames.SITE_ELEVATION]), 2) if si_ip != UnitSystem.SI: - site_elevation = site_elevation * 3.281 - site_elevation = round(site_elevation, 2) - elevation = f"Elevation above sea level: {str(site_elevation)} ft" + site_elevation = round(site_elevation * 3.281, 2) + elevation = f"Elevation above sea level: {site_elevation} ft" + + else: + elevation = f"Elevation above sea level: {site_elevation} m" period = "" if meta[ColNames.PERIOD]: start, stop = meta[ColNames.PERIOD].split("-") period = f"This file is based on data collected between {start} and {stop}" - r = requests.get( - f"http://climateapi.scottpinkelman.com/api/v1/location/{meta[ColNames.LAT]}/{meta[ColNames.LON]}" - ) - climate_text = "" - if r.status_code == 200: - try: - climate_zone = r.json()["return_values"][0]["koppen_geiger_zone"] - zone_description = r.json()["return_values"][0]["zone_description"] - - climate_text = ( - f"Köppen–Geiger climate zone: {climate_zone}. {zone_description}." - ) - except KeyError: - pass + try: + r = requests.get( + f"http://climateapi.scottpinkelman.com/api/v1/location/{meta[ColNames.LAT]}/{meta[ColNames.LON]}" + ) + if r.status_code == 200: + j = r.json()["return_values"][0] + climate_text = f"Köppen-Geiger climate zone: {j['koppen_geiger_zone']}. {j['zone_description']}." + except Exception: + pass # global horizontal irradiance # Note that the value is divided by 1000, so a corresponding change is made in the unit: total_solar_rad_value = round(df[ColNames.GLOB_HOR_RAD].sum() / 1000, 2) - total_solar_rad_unit = ( - "k" + mapping_dictionary[ColNames.GLOB_HOR_RAD][si_ip][ColNames.UNIT] - ) + total_solar_rad_unit = "k" + mapping_dictionary[ColNames.GLOB_HOR_RAD][si_ip][ + ColNames.UNIT + ].replace("", "").replace("", "") total_solar_rad = f"Annual cumulative horizontal solar radiation: {total_solar_rad_value} {total_solar_rad_unit}" glob_sum = df[ColNames.GLOB_HOR_RAD].sum() - if glob_sum > 0: - diffuse_percentage = round(df[ColNames.DIF_HOR_RAD].sum() / glob_sum * 100, 1) - else: - diffuse_percentage = 0 + diffuse_percentage = ( + round(df[ColNames.DIF_HOR_RAD].sum() / glob_sum * 100, 1) if glob_sum > 0 else 0 + ) total_diffuse_rad = ( f"Percentage of diffuse horizontal solar radiation: {diffuse_percentage} %" ) + tmp_unit = mapping_dictionary[ColNames.DBT][si_ip][ColNames.UNIT] - average_yearly_tmp = ( - f"Average yearly temperature: {df[ColNames.DBT].mean().round(1)} " + tmp_unit - ) - hottest_yearly_tmp = ( - f"Hottest yearly temperature (99%): {df[ColNames.DBT].quantile(0.99).round(1)} " - + tmp_unit - ) - coldest_yearly_tmp = ( - f"Coldest yearly temperature (1%): {df[ColNames.DBT].quantile(0.01).round(1)} " - + tmp_unit - ) - location_info = dbc.Col( - [ - dbc.Row(location, style={"fontWeight": "bold"}), - dbc.Row(lon), - dbc.Row(lat), - dbc.Row(elevation), - dbc.Row(period), - dbc.Row(climate_text), - dbc.Row(average_yearly_tmp), - dbc.Row(hottest_yearly_tmp), - dbc.Row(coldest_yearly_tmp), - dbc.Row( - dcc.Markdown( - dangerously_allow_html=True, - children=[total_solar_rad], - style={"padding": 0}, - ) - ), - dbc.Row(total_diffuse_rad), - ], + average_yearly_tmp = ( + f"Average yearly temperature: {df[ColNames.DBT].mean().round(1)} {tmp_unit}" ) - - return location_info + hottest_yearly_tmp = f"Hottest yearly temperature (99%): {df[ColNames.DBT].quantile(0.99).round(1)} {tmp_unit}" + coldest_yearly_tmp = f"Coldest yearly temperature (1%): {df[ColNames.DBT].quantile(0.01).round(1)} {tmp_unit}" + + return [ + dmc.Text(location, fw=700), + dmc.Text(lon), + dmc.Text(lat), + dmc.Text(elevation), + dmc.Text(period) if period else None, + dmc.Text(climate_text) if climate_text else None, + dmc.Text(average_yearly_tmp), + dmc.Text(hottest_yearly_tmp), + dmc.Text(coldest_yearly_tmp), + dmc.Text(total_solar_rad), + dmc.Text(total_diffuse_rad), + ] @callback( [ Output(ElementIds.DEGREE_DAYS_CHART_WRAPPER, "children"), - Output(ElementIds.WARNING_CDD_HIGHER_HDD, "is_open"), + Output(ElementIds.WARNING_CDD_HIGHER_HDD, "is-open"), ], [ Input(ElementIds.ID_SUMMARY_DF_STORE, "modified_timestamp"), - Input(ElementIds.SUBMIT_SET_POINTS, "n_clicks_timestamp"), + Input(ElementIds.SUBMIT_SET_POINTS, "n_clicks"), ], [ State(ElementIds.ID_SUMMARY_DF_STORE, "data"), State(ElementIds.ID_SUMMARY_META_STORE, "data"), State(ElementIds.INPUT_HDD_SET_POINT, "value"), State(ElementIds.INPUT_CDD_SET_POINT, "value"), - State(ElementIds.SUBMIT_SET_POINTS, "n_clicks"), State(ElementIds.ID_SUMMARY_SI_IP_UNIT_STORE, "data"), ], + prevent_initial_call=False, ) -def degree_day_chart(ts, ts_click, df, meta, hdd_value, cdd_value, n_clicks, si_ip): - """Update the contents of tab two. Passing in the general info (df, meta).""" +def degree_day_chart(ts, n_clicks, df, meta, hdd_value, cdd_value, si_ip): + """Redraw HDD/CDD chart only when Submit is clicked.""" - ctx = dash.callback_context - - if ( - ctx.triggered[0][ColNames.PROP_ID] == "submit-set-points.n_clicks_timestamp" - or n_clicks is None - ): - hdd_setpoint = hdd_value - cdd_setpoint = cdd_value - - warning_setpoint = False - if cdd_setpoint < hdd_setpoint: - warning_setpoint = True - - color_hdd = "red" - color_cdd = "dodgerblue" - - hdd_array = [] - cdd_array = [] - months = df[ColNames.MONTH_NAMES].unique() - - for i in range(1, 13): - query_month = "month==" - - # calculates HDD per month - query = query_month + str(i) + " and DBT<=" + str(hdd_setpoint) - a = df.query(query)[ColNames.DBT].sub(hdd_setpoint) - hdd = a.sum(axis=0, skipna=True) - hdd = hdd / 24 - hdd = int(hdd) - hdd_array.append(hdd) - - # calculates CDD per month - query = query_month + str(i) + " and DBT>=" + str(cdd_setpoint) - a = df.query(query)[ColNames.DBT].sub(cdd_setpoint) - cdd = a.sum(axis=0, skipna=True) - cdd = cdd / 24 - cdd = int(cdd) - cdd_array.append(cdd) - - trace1 = go.Bar( - x=months, - y=hdd_array, - name="Heating Degree Days", - marker_color=color_hdd, - customdata=[abs(ele) for ele in hdd_array], - hovertemplate=( - " Heating Degree Days:
%{customdata} per month
" - ), - ) - trace2 = go.Bar( - x=months, - y=cdd_array, - name="Cooling Degree Days", - marker_color=color_cdd, - customdata=cdd_array, - hovertemplate=( - "Cooling Degree Days:
%{customdata} per month
" - ), - ) + if df is None or meta is None: + raise PreventUpdate - data = [trace2, trace1] + if isinstance(df, (list, tuple, dict)): + df = pd.DataFrame(df) - fig = go.Figure( - data=data, - ) - fig.update_layout( - barmode="relative", - margin=tight_margins, - template=template, - dragmode=False, - legend=dict( - orientation="h", yanchor="bottom", y=1.05, xanchor="right", x=1 - ), - ) + hdd_setpoint = hdd_value + cdd_setpoint = cdd_value + warning_setpoint = cdd_setpoint < hdd_setpoint + + color_hdd = "red" + color_cdd = "dodgerblue" + + hdd_array, cdd_array = [], [] + months = df[ColNames.MONTH_NAMES].unique() - fig.update_xaxes(showline=True, linewidth=1, linecolor="black", mirror=True) - fig.update_yaxes(showline=True, linewidth=1, linecolor="black", mirror=True) + for i in range(1, 13): + query_month = "month==" - custom_inputs = f"{hdd_value}-{cdd_value}" - units = generate_units_degree(si_ip) + a = df.query(query_month + str(i) + " and DBT<=" + str(hdd_setpoint))[ + ColNames.DBT + ].sub(hdd_setpoint) + hdd_array.append(int(a.sum(skipna=True) / 24)) - chart = dcc.Graph( - id=ElementIds.DEGREE_DAYS_CHART, - config=generate_chart_name(TabNames.HDD_CDD, meta, custom_inputs, units), - figure=fig, + a = df.query(query_month + str(i) + " and DBT>=" + str(cdd_setpoint))[ + ColNames.DBT + ].sub(cdd_setpoint) + cdd_array.append(int(a.sum(skipna=True) / 24)) + + trace1 = go.Bar( + x=months, + y=hdd_array, + name="Heating Degree Days", + marker_color=color_hdd, + customdata=[abs(x) for x in hdd_array], + hovertemplate=" Heating Degree Days:
%{customdata} per month
", + ) + + trace2 = go.Bar( + x=months, + y=cdd_array, + name="Cooling Degree Days", + marker_color=color_cdd, + customdata=cdd_array, + hovertemplate="Cooling Degree Days:
%{customdata} per month
", + ) + + fig = go.Figure(data=[trace2, trace1]) + fig.update_layout( + barmode="relative", + margin=tight_margins, + template=template, + dragmode=False, + legend=dict(orientation="h", yanchor="bottom", y=1.05, xanchor="right", x=1), + ) + fig.update_xaxes(showline=True, linewidth=1, linecolor="black", mirror=True) + fig.update_yaxes(showline=True, linewidth=1, linecolor="black", mirror=True) + + custom_inputs = f"{hdd_value}-{cdd_value}" + units = generate_units_degree(si_ip) + + chart = dcc.Graph( + id=ElementIds.DEGREE_DAYS_CHART, + config=generate_chart_name(TabNames.HDD_CDD, meta, custom_inputs, units), + figure=fig, + ) + + alert_children = ( + dmc.Alert( + "WARNING: Invalid Results! The CDD setpoint should be higher than the HDD setpoint!", + color="yellow", + variant="filled", + title="Warning", + withCloseButton=True, ) + if warning_setpoint + else None + ) - return chart, warning_setpoint + return chart, alert_children @callback( @@ -442,7 +382,6 @@ def update_violin_tdb(ts, global_local, df, meta, si_ip): units = generate_units_degree(si_ip) return dcc.Graph( id=ElementIds.TDB_PROFILE_GRAPH, - className="violin-container", config=generate_chart_name(TabNames.DRY_BULB_TEMPERATURE, meta, units), figure=violin(df, ColNames.DBT, global_local, si_ip), ) @@ -465,7 +404,6 @@ def update_tab_wind(ts, global_local, df, meta, si_ip): units = generate_units(si_ip) return dcc.Graph( id=ElementIds.WIND_PROFILE_GRAPH, - className="violin-container", config=generate_chart_name(TabNames.WIND_SPEED, meta, units), figure=violin(df, ColNames.WIND_SPEED, global_local, si_ip), ) @@ -488,7 +426,6 @@ def update_tab_rh(ts, global_local, df, meta, si_ip): units = generate_units(si_ip) return dcc.Graph( id=ElementIds.RH_PROFILE_GRAPH, - className="violin-container", config=generate_chart_name(TabNames.RELATIVE_HUMIDITY, meta, units), figure=violin(df, ColNames.RH, global_local, si_ip), ) @@ -511,7 +448,6 @@ def update_tab_gh_rad(ts, global_local, df, meta, si_ip): units = generate_units(si_ip) return dcc.Graph( id=ElementIds.GH_RAD_PROFILE_GRAPH, - className="violin-container", config=generate_chart_name(TabNames.GLOBAL_HORIZONTAL_RADIATION, meta, units), figure=violin(df, ColNames.GLOB_HOR_RAD, global_local, si_ip), ) diff --git a/pages/sun.py b/pages/sun.py index 919655f..5307acc 100644 --- a/pages/sun.py +++ b/pages/sun.py @@ -2,9 +2,10 @@ from pages.lib.global_element_ids import ElementIds import dash -import dash_bootstrap_components as dbc +import dash_mantine_components as dmc + import numpy as np -from dash import html, dcc +from dash import dcc from dash_extensions.enrich import Output, Input, State, callback from pages.lib.global_column_names import ColNames @@ -53,27 +54,29 @@ sc_dropdown_names.pop("UTCI: no Sun & no Wind : categories", None) +def layout(): + """Contents of tab four.""" + return dmc.Stack( + p="md", + id=ElementIds.TAB_FOUR_CONTAINER, + children=[sun_path(), static_section(), explore_daily_heatmap()], + ) + + def sun_path(): """Return the layout for the custom sun path and its dropdowns.""" - return html.Div( - className="container-col justify-center", + return dmc.Stack( children=[ - html.Div( - children=title_with_link( - text="Sun path chart", - id_button=IdButtons.SUN_PATH_CHART_LABEL, - doc_link=DocLinks.SUN_PATH_DIAGRAM, - ), + title_with_link( + text="Sun path chart", + id_button=IdButtons.SUN_PATH_CHART_LABEL, + doc_link=DocLinks.SUN_PATH_DIAGRAM, ), - dbc.Row( + dmc.Group( align="center", justify="center", children=[ - html.H6( - className="text-next-to-input", - children=["View: "], - style={"width": "10rem"}, - ), + dmc.Title("View: ", order=5), dropdown( id=ElementIds.CUSTOM_SUN_VIEW_DROPDOWN, options={ @@ -81,31 +84,25 @@ def sun_path(): "Cartesian": "cartesian", }, value="polar", - style={"width": "10rem"}, ), ], ), - dbc.Row( + dmc.Group( align="center", justify="center", children=[ - html.H6( - className="text-next-to-input", - children=["Select variable: "], - style={"width": "10rem"}, - ), + dmc.Title("Select Variable: ", order=5), dropdown( id=ElementIds.CUSTOM_SUN_VAR_DROPDOWN, options=sc_dropdown_names, value="None", - style={"width": "20rem"}, ), ], ), - dcc.Loading( - type="circle", - children=html.Div( - id=ElementIds.CUSTOM_SUNPATH, + dmc.Center( + dcc.Loading( + type="circle", + children=dmc.Stack(id=ElementIds.CUSTOM_SUNPATH, w="100%"), ), ), ], @@ -114,60 +111,45 @@ def sun_path(): def explore_daily_heatmap(): """Contents of the bottom part of the tab""" - return html.Div( - className="container-col full-width", + return dmc.Stack( + w="100%", children=[ - html.Div( - children=title_with_link( - text="Daily charts", - id_button=IdButtons.DAILY_CHART_LABEL, - doc_link=DocLinks.CUSTOM_HEATMAP, - ), + title_with_link( + text="Daily charts", + id_button=IdButtons.DAILY_CHART_LABEL, + doc_link=DocLinks.CUSTOM_HEATMAP, ), - html.Div( - className="container-row justify-center align-center mb-2", + dmc.Group( + align="center", + justify="center", children=[ - html.H6( - className="text-next-to-input", - children=["Select variable: "], - style={"width": "10rem"}, - ), + dmc.Title("Select variable: ", order=5), dropdown( id=ElementIds.TAB_EXPLORE_DROPDOWN, options=sun_cloud_tab_explore_dropdown_names, value="glob_hor_rad", - style={"width": "20rem"}, ), ], ), - dcc.Loading(type="circle", children=html.Div(id=ElementIds.TAB4_DAILY)), + dcc.Loading(type="circle", children=dmc.Stack(id=ElementIds.TAB4_DAILY)), dcc.Loading( type="circle", - children=html.Div(id=ElementIds.TAB4_HEATMAP), + children=dmc.Stack(id=ElementIds.TAB4_HEATMAP), ), ], ) def static_section(): - return html.Div( + return dmc.Stack( id=ElementIds.STATIC_SECTION, - className="container-col full-width", + w="100%", children=[ # ... ], ) -def layout(): - """Contents of tab four.""" - return html.Div( - className="container-col", - id=ElementIds.TAB_FOUR_CONTAINER, - children=[sun_path(), static_section(), explore_daily_heatmap()], - ) - - @callback( Output(ElementIds.STATIC_SECTION, "children"), [Input(ElementIds.ID_SUN_SI_IP_RADIO_INPUT, "value")], @@ -177,27 +159,23 @@ def update_static_section(si_ip): if si_ip == UnitSystem.IP: hor_unit = "Btu/ft²" return [ - html.Div( - children=title_with_link( - text="Global and Diffuse Horizontal Solar Radiation (" + hor_unit + ")", - id_button=IdButtons.MONTHLY_CHART_LABEL, - doc_link=DocLinks.SOLAR_RADIATION, - ), + title_with_link( + text="Global and Diffuse Horizontal Solar Radiation (" + hor_unit + ")", + id_button=IdButtons.MONTHLY_CHART_LABEL, + doc_link=DocLinks.SOLAR_RADIATION, ), dcc.Loading( type="circle", - children=html.Div(id=ElementIds.MONTHLY_SOLAR), + children=dmc.Stack(id=ElementIds.MONTHLY_SOLAR), ), - html.Div( - children=title_with_link( - text="Cloud coverage", - id_button=IdButtons.CLOUD_CHART_LABEL, - doc_link=DocLinks.CLOUD_COVER, - ), + title_with_link( + text="Cloud coverage", + id_button=IdButtons.CLOUD_CHART_LABEL, + doc_link=DocLinks.CLOUD_COVER, ), dcc.Loading( type="circle", - children=html.Div(id=ElementIds.CLOUD_COVER), + children=dmc.Stack(id=ElementIds.CLOUD_COVER), ), ] @@ -237,11 +215,13 @@ def monthly_and_cloud_chart(_, df, meta, si_ip): ) units = generate_units(si_ip) return dcc.Graph( + style={"width": "100%", "height": "520px"}, config=generate_chart_name( TabNames.GLOBAL_AND_DIFFUSE_HORIZONTAL_SOLAR_RADIATION, meta, units ), figure=monthly, ), dcc.Graph( + style={"width": "100%", "height": "520px"}, config=generate_chart_name(TabNames.CLOUD_COVER, meta, units), figure=cover, ) @@ -267,6 +247,7 @@ def sun_path_chart(_, view, var, global_local, df, meta, si_ip): units = "" if var == "None" else generate_units(si_ip) if view == "polar": return dcc.Graph( + style={"width": "100%", "height": "520px"}, config=generate_chart_name( TabNames.SPHERICAL_SUNPATH, meta, custom_inputs, units ), @@ -274,6 +255,7 @@ def sun_path_chart(_, view, var, global_local, df, meta, si_ip): ) else: return dcc.Graph( + style={"width": "100%", "height": "520px"}, config=generate_chart_name( TabNames.CARTESIAN_SUNPATH, meta, custom_inputs, units ), @@ -299,6 +281,7 @@ def daily(_, var, global_local, df, meta, si_ip): custom_inputs = generate_custom_inputs(var) units = generate_units(si_ip) return dcc.Graph( + style={"width": "100%", "height": "520px"}, config=generate_chart_name(TabNames.DAILY, meta, custom_inputs, units), figure=daily_profile(df, var, global_local, si_ip), ) @@ -321,6 +304,7 @@ def update_heatmap(_, var, global_local, df, meta, si_ip): custom_inputs = generate_custom_inputs(var) units = generate_units(si_ip) return dcc.Graph( + style={"width": "100%", "height": "520px"}, config=generate_chart_name(TabNames.HEATMAP, meta, custom_inputs, units), figure=heatmap(df, var, global_local, si_ip), ) diff --git a/pages/t_rh.py b/pages/t_rh.py index 6729a31..a9c01b6 100644 --- a/pages/t_rh.py +++ b/pages/t_rh.py @@ -1,5 +1,7 @@ import dash -from dash_extensions.enrich import Output, Input, State, dcc, html, callback +from dash_extensions.enrich import Output, Input, State, dcc, callback +import dash_mantine_components as dmc + from config import PageUrls, DocLinks, PageInfo from pages.lib.global_scheme import dropdown_names from pages.lib.template_graphs import heatmap, yearly_profile, daily_profile @@ -30,71 +32,56 @@ def layout(): - return html.Div( - className="container-col full-width", + return dmc.Stack( + p="md", children=[ - html.Div( - className="container-row full-width align-center justify-center", - children=[ - html.H4( - className="text-next-to-input", children=["Select a variable: "] - ), + dmc.Center( + [ + dmc.Title("Select a variable:", order=5, mr="md"), dropdown( id=ElementIds.ID_T_RH_DROPDOWN, - className="dropdown-t-rh", options={var: dropdown_names[var] for var in var_to_plot}, value=dropdown_names[var_to_plot[0]], ), - ], + ] ), - html.Div( - className="container-col", - children=[ - html.Div( - children=title_with_link( - text="Yearly Chart", - id_button=IdButtons.YEARLY_CHART_LABEL, - doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, - ), - ), - dcc.Loading( - type="circle", - children=html.Div(id=ElementIds.YEARLY_CHART), - ), - html.Div( - children=title_with_link( - text="Daily chart", - id_button=IdButtons.DAILY_CHART_LABEL, - doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, - ), - ), - dcc.Loading( - type="circle", - children=html.Div(id=ElementIds.DAILY), - ), - html.Div( - children=title_with_link( - text="Heatmap chart", - id_button=IdButtons.HEATMAP_CHART_LABEL, - doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, - ), - ), - dcc.Loading( - type="circle", - children=html.Div(id=ElementIds.HEATMAP), - ), - html.Div( - children=title_with_tooltip( - text="Descriptive statistics", - tooltip_text="count, mean, std, min, max, and percentiles", - id_button=IdButtons.TABLE_TMP_RH, - ), - ), - html.Div( - id=ElementIds.TABLE_TMP_HUM, - ), - ], + # Yearly Chart + title_with_link( + text="Yearly Chart", + id_button=IdButtons.YEARLY_CHART_LABEL, + doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, + ), + dcc.Loading( + type="circle", + children=dmc.Stack(id=ElementIds.YEARLY_CHART), + ), + # Daily chart + title_with_link( + text="Daily chart", + id_button=IdButtons.DAILY_CHART_LABEL, + doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, + ), + dcc.Loading( + type="circle", + children=dmc.Stack(id=ElementIds.DAILY), + ), + # Heatmap chart + title_with_link( + text="Heatmap chart", + id_button=IdButtons.HEATMAP_CHART_LABEL, + doc_link=DocLinks.TEMP_HUMIDITY_EXPLAINED, + ), + dcc.Loading( + type="circle", + children=dmc.Stack(id=ElementIds.HEATMAP), + ), + # Descriptive statistics + title_with_tooltip( + text="Descriptive statistics", + tooltip_text="count, mean, std, min, max, and percentiles", + id_button=IdButtons.TABLE_TMP_RH, ), + dmc.Stack(id=ElementIds.TABLE_TMP_HUM), ], ) @@ -192,7 +179,7 @@ def update_daily(_, global_local, dd_value, df, meta, si_ip): @callback( - [Output(ElementIds.HEATMAP, "children")], + Output(ElementIds.HEATMAP, "children"), [ Input(ElementIds.ID_T_RH_DF_STORE, "modified_timestamp"), Input(ElementIds.ID_T_RH_GLOBAL_LOCAL_RADIO_INPUT, "value"), @@ -205,7 +192,7 @@ def update_daily(_, global_local, dd_value, df, meta, si_ip): ], ) def update_heatmap(_, global_local, dd_value, df, meta, si_ip): - """Update the contents of tab three. Passing in general info (df, meta).""" + """Update heatmap content.""" if dd_value == dropdown_names[var_to_plot[0]]: units = generate_units_degree(si_ip) return dcc.Graph( @@ -260,7 +247,7 @@ def update_heatmap(_, global_local, dd_value, df, meta, si_ip): ], ) def update_table(_, dd_value, df, si_ip): - """Update the contents of tab three. Passing in general info (df, meta).""" + """Update the contents of descriptive statistics table.""" return summary_table_tmp_rh_tab( df[[ColNames.MONTH, ColNames.HOUR, dd_value, ColNames.MONTH_NAMES]], dd_value, diff --git a/pages/wind.py b/pages/wind.py index 8b74444..195f4b6 100644 --- a/pages/wind.py +++ b/pages/wind.py @@ -1,10 +1,11 @@ import dash -from dash import dcc, html +from dash import dcc +import dash_mantine_components as dmc from dash_extensions.enrich import Output, Input, State, callback from pages.lib.global_element_ids import ElementIds from config import PageUrls, DocLinks, PageInfo -from pages.lib.global_scheme import month_lst, container_row_center_full +from pages.lib.global_scheme import month_lst from pages.lib.template_graphs import heatmap, wind_rose from pages.lib.global_column_names import ColNames from pages.lib.global_id_buttons import IdButtons @@ -29,14 +30,12 @@ def sliders(): """Returns 2 sliders for the hour""" - return html.Div( - className="container-col justify-center", + return dmc.Stack( id=ElementIds.SLIDER_CONTAINER, children=[ - html.Div( - className="container-row each-slider", + dmc.Group( children=[ - html.P("Month Range"), + dmc.Title("Month Range", order=5), dcc.RangeSlider( id=ElementIds.MONTH_SLIDER, min=1, @@ -49,10 +48,9 @@ def sliders(): ), ], ), - html.Div( - className="container-row each-slider", + dmc.Group( children=[ - html.P("Hour Range"), + dmc.Title("Hour Range", order=5), dcc.RangeSlider( id=ElementIds.HOUR_SLIDER, min=1, @@ -71,87 +69,71 @@ def sliders(): def seasonal_wind_rose(): """Return the section with the 4 seasonal wind rose graphs.""" - return html.Div( - className="container-col", + return dmc.Stack( children=[ - html.Div( - children=title_with_link( - text="Seasonal Wind Rose", - id_button=IdButtons.SEASONAL_WIND_ROSE_DOC, - doc_link=DocLinks.WIND_ROSE, - ), + title_with_link( + text="Seasonal Wind Rose", + id_button=IdButtons.SEASONAL_WIND_ROSE_DOC, + doc_link=DocLinks.WIND_ROSE, ), - html.Div( - className=container_row_center_full, + dmc.Grid( + gutter="md", children=[ - html.Div( - className="container-col", - children=[ - dcc.Loading( - type="circle", - children=html.Div( - id=ElementIds.WINTER_WIND_ROSE, - className="daily-wind-graph", + dmc.GridCol( + span=6, + children=dmc.Stack( + children=[ + dcc.Loading( + type="circle", + children=dmc.Stack( + id=ElementIds.WINTER_WIND_ROSE, + ), ), - ), - html.P( - className="seasonal-text", - id=ElementIds.WINTER_WIND_ROSE_TEXT, - ), - ], + dmc.Text(id=ElementIds.WINTER_WIND_ROSE_TEXT), + ], + ), ), - html.Div( - className="container-col", - children=[ - dcc.Loading( - type="circle", - children=html.Div( - id=ElementIds.SPRING_WIND_ROSE, - className="daily-wind-graph", + dmc.GridCol( + span=6, + children=dmc.Stack( + children=[ + dcc.Loading( + type="circle", + children=dmc.Stack( + id=ElementIds.SPRING_WIND_ROSE, + ), ), - ), - html.P( - className="seasonal-text", - id=ElementIds.SPRING_WIND_ROSE_TEXT, - ), - ], + dmc.Text(id=ElementIds.SPRING_WIND_ROSE_TEXT), + ], + ), ), - ], - ), - html.Div( - className=container_row_center_full, - children=[ - html.Div( - className="container-col", - children=[ - dcc.Loading( - type="circle", - children=html.Div( - id=ElementIds.SUMMER_WIND_ROSE, - className="daily-wind-graph", + dmc.GridCol( + span=6, + children=dmc.Stack( + children=[ + dcc.Loading( + type="circle", + children=dmc.Stack( + id=ElementIds.SUMMER_WIND_ROSE, + ), ), - ), - html.P( - className="seasonal-text", - id=ElementIds.SUMMER_WIND_ROSE_TEXT, - ), - ], + dmc.Text(id=ElementIds.SUMMER_WIND_ROSE_TEXT), + ], + ), ), - html.Div( - className="container-col", - children=[ - dcc.Loading( - type="circle", - children=html.Div( - id=ElementIds.FALL_WIND_ROSE, - className="daily-wind-graph", + dmc.GridCol( + span=6, + children=dmc.Stack( + children=[ + dcc.Loading( + type="circle", + children=dmc.Stack( + id=ElementIds.FALL_WIND_ROSE, + ), ), - ), - html.P( - className="seasonal-text", - id=ElementIds.FALL_WIND_ROSE_TEXT, - ), - ], + dmc.Text(id=ElementIds.FALL_WIND_ROSE_TEXT), + ], + ), ), ], ), @@ -161,74 +143,57 @@ def seasonal_wind_rose(): def daily_wind_rose(): """Return the section for the 3 daily wind rose graphs.""" - return html.Div( - className="container-col full-width", + return dmc.Stack( id=ElementIds.TAB5_DAILY_CONTAINER, children=[ - html.Div( - children=title_with_link( - text="Daily Wind Rose", - id_button=IdButtons.DAILY_ROSE_CHART, - doc_link=DocLinks.WIND_ROSE, - ), + title_with_link( + text="Daily Wind Rose", + id_button=IdButtons.DAILY_ROSE_CHART, + doc_link=DocLinks.WIND_ROSE, ), - html.Div( - id=ElementIds.DAILY_WIND_ROSE_OUTER_CONTAINER, - className="container-row full-width", + dmc.Grid( children=[ - html.Div( - className="container-col", - children=[ - html.Div( + dmc.GridCol( + span=4, + children=dmc.Stack( + children=[ dcc.Loading( type="circle", - children=html.Div( - className="daily-wind-graph", + children=dmc.Stack( id=ElementIds.MORNING_WIND_ROSE, ), ), - ), - html.P( - className="daily-text", - id=ElementIds.MORNING_WIND_ROSE_TEXT, - ), - ], + dmc.Text(id=ElementIds.MORNING_WIND_ROSE_TEXT), + ], + ), ), - html.Div( - className="container-col", - children=[ - html.Div( + dmc.GridCol( + span=4, + children=dmc.Stack( + children=[ dcc.Loading( type="circle", - children=html.Div( - className="daily-wind-graph", + children=dmc.Stack( id=ElementIds.NOON_WIND_ROSE, ), ), - ), - html.P( - className="daily-text", - id=ElementIds.NOON_WIND_ROSE_TEXT, - ), - ], + dmc.Text(id=ElementIds.NOON_WIND_ROSE_TEXT), + ], + ), ), - html.Div( - className="container-col", - children=[ - html.Div( + dmc.GridCol( + span=4, + children=dmc.Stack( + children=[ dcc.Loading( type="circle", - children=html.Div( - className="daily-wind-graph", + children=dmc.Stack( id=ElementIds.NIGHT_WIND_ROSE, ), ), - ), - html.P( - className="daily-text", - id=ElementIds.NIGHT_WIND_ROSE_TEXT, - ), - ], + dmc.Text(id=ElementIds.NIGHT_WIND_ROSE_TEXT), + ], + ), ), ], ), @@ -237,103 +202,98 @@ def daily_wind_rose(): def custom_wind_rose(): - return html.Div( - className="container-col justify-center full-width", + return dmc.Stack( children=[ - html.Div( - children=title_with_tooltip( - text="Customizable Wind Rose", - tooltip_text=None, - id_button=IdButtons.CUSTOM_ROSE_CHART, - ), + title_with_tooltip( + text="Customizable Wind Rose", + tooltip_text=None, + id_button=IdButtons.CUSTOM_ROSE_CHART, ), - html.Div( - className="container-row full-width justify-center", - id=ElementIds.TAB5_CUSTOM_DROPDOWN_CONTAINER, + dmc.Grid( + gutter="md", + maw=900, + mx="auto", children=[ - html.Div( - className="container-col justify-center p-2 mr-2", - children=[ - html.Div( - className=container_row_center_full, - children=[ - html.H6( - style={"width": "8rem"}, - children=["Start Month:"], - ), - dropdown( - id=ElementIds.TAB5_CUSTOM_START_MONTH, - options={ - j: i + 1 for i, j in enumerate(month_lst) - }, - value=1, - style={"width": "6rem"}, - ), - ], - ), - html.Div( - className=container_row_center_full, - children=[ - html.H6( - style={"width": "8rem"}, - children=["Start Hour:"], - ), - dropdown( - id=ElementIds.TAB5_CUSTOM_START_HOUR, - options={ - str(i) + ":00": i for i in range(0, 24) - }, - value=0, - style={"width": "6rem"}, - ), - ], - ), - ], + dmc.GridCol( + span=6, + children=dmc.Stack( + children=[ + dmc.Group( + children=[ + dmc.Title( + "Start Month:", + order=6, + 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, + ), + ], + ), + ], + ), ), - html.Div( - className="container-col justify-center p-2 ml-2", - children=[ - html.Div( - className=container_row_center_full, - children=[ - html.H6( - style={"width": "8rem"}, - children=["End Month:"], - ), - dropdown( - id=ElementIds.TAB5_CUSTOM_END_MONTH, - options={ - j: i + 1 for i, j in enumerate(month_lst) - }, - value=12, - style={"width": "6rem"}, - ), - ], - ), - html.Div( - className=container_row_center_full, - children=[ - html.H6( - style={"width": "8rem"}, - children=["End Hour:"], - ), - dropdown( - id=ElementIds.TAB5_CUSTOM_END_HOUR, - options={ - str(i) + ":00": i for i in range(1, 25) - }, - value=24, - style={"width": "6rem"}, - ), - ], - ), - ], + 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=html.Div(id=ElementIds.CUSTOM_WIND_ROSE), + children=dmc.Stack(id=ElementIds.CUSTOM_WIND_ROSE, maw=900, mx="auto"), ), ], ) @@ -341,29 +301,25 @@ def custom_wind_rose(): def layout(): """Contents in the fifth tab 'Wind'.""" - return html.Div( - className="container-col justify-center", + return dmc.Stack( + p="md", children=[ - html.Div( - children=title_with_link( - text="Annual Wind Rose", - id_button=IdButtons.WIND_ROSE_LABEL, - doc_link=DocLinks.WIND_ROSE, - ), + title_with_link( + text="Annual Wind Rose", + id_button=IdButtons.WIND_ROSE_LABEL, + doc_link=DocLinks.WIND_ROSE, ), dcc.Loading( type="circle", - children=html.Div( - id=ElementIds.WIND_ROSE, - ), + children=dmc.Stack(id=ElementIds.WIND_ROSE), ), dcc.Loading( type="circle", - children=html.Div(id=ElementIds.WIND_SPEED), + children=dmc.Stack(id=ElementIds.WIND_SPEED), ), dcc.Loading( type="circle", - children=html.Div(id=ElementIds.WIND_DIRECTION), + children=dmc.Stack(id=ElementIds.WIND_DIRECTION), ), seasonal_wind_rose(), daily_wind_rose(), @@ -372,7 +328,6 @@ def layout(): ) -# wind rose @callback( Output(ElementIds.WIND_ROSE, "children"), Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), @@ -383,8 +338,6 @@ def layout(): ], ) def update_annual_wind_rose(_, df, meta, si_ip): - """Update the contents of tab five. Passing in the info from the sliders and the general info (df, meta).""" - annual = wind_rose(df, "", [1, 12], [1, 24], True, si_ip) units = generate_units(si_ip) return dcc.Graph( @@ -393,10 +346,8 @@ def update_annual_wind_rose(_, df, meta, si_ip): ) -# wind speed @callback( Output(ElementIds.WIND_SPEED, "children"), - # General [ Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), Input(ElementIds.ID_WIND_GLOBAL_LOCAL_RADIO_INPUT, "value"), @@ -408,8 +359,6 @@ def update_annual_wind_rose(_, df, meta, si_ip): ], ) def update_tab_wind_speed(_, global_local, df, meta, si_ip): - """Update the contents of tab five. Passing in the info from the sliders and the general info (df, meta).""" - speed = heatmap(df, ColNames.WIND_SPEED, global_local, si_ip) units = generate_units(si_ip) return dcc.Graph( @@ -418,13 +367,9 @@ def update_tab_wind_speed(_, global_local, df, meta, si_ip): ) -# wind direction @callback( Output(ElementIds.WIND_DIRECTION, "children"), - # General - [ - Input(ElementIds.ID_WIND_GLOBAL_LOCAL_RADIO_INPUT, "value"), - ], + [Input(ElementIds.ID_WIND_GLOBAL_LOCAL_RADIO_INPUT, "value")], [ State(ElementIds.ID_WIND_DF_STORE, "data"), State(ElementIds.ID_WIND_META_STORE, "data"), @@ -432,8 +377,6 @@ def update_tab_wind_speed(_, global_local, df, meta, si_ip): ], ) def update_tab_wind_direction(global_local, df, meta, si_ip): - """Update the contents of tab five. Passing in the info from the sliders and the general info (df, meta).""" - direction = heatmap(df, ColNames.WIND_DIR, global_local, si_ip) units = generate_units(si_ip) return dcc.Graph( @@ -442,10 +385,8 @@ def update_tab_wind_direction(global_local, df, meta, si_ip): ) -# Custom Wind rose @callback( Output(ElementIds.CUSTOM_WIND_ROSE, "children"), - # Custom Graph Input [ Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), Input(ElementIds.TAB5_CUSTOM_START_MONTH, "value"), @@ -462,14 +403,11 @@ def update_tab_wind_direction(global_local, df, meta, si_ip): def update_custom_wind_rose( _, start_month, start_hour, end_month, end_hour, df, meta, si_ip ): - """Update the contents of tab five. Passing in the info from the sliders and the general info (df, meta).""" - start_hour = int(start_hour) end_hour = int(end_hour) start_month = int(start_month) end_month = int(end_month) - # Wind Rose Graphs if start_month <= end_month: df = df.loc[ (df[ColNames.MONTH] >= start_month) & (df[ColNames.MONTH] <= end_month) @@ -482,6 +420,7 @@ def update_custom_wind_rose( df = df.loc[(df[ColNames.HOUR] >= start_hour) & (df[ColNames.HOUR] <= end_hour)] else: df = df.loc[(df[ColNames.HOUR] <= end_hour) | (df[ColNames.HOUR] >= start_hour)] + custom = wind_rose( df, "", [start_month, end_month], [start_hour, end_hour], True, si_ip ) @@ -508,9 +447,7 @@ def update_custom_wind_rose( Output(ElementIds.SUMMER_WIND_ROSE_TEXT, "children"), Output(ElementIds.FALL_WIND_ROSE_TEXT, "children"), ], - [ - Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), - ], + [Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp")], [ State(ElementIds.ID_WIND_DF_STORE, "data"), State(ElementIds.ID_WIND_META_STORE, "data"), @@ -524,18 +461,17 @@ def update_seasonal_graphs(_, df, meta, si_ip): summer_months = [6, 8] fall_months = [9, 12] - # Wind Rose Graphs winter = wind_rose(df, "", winter_months, hours, False, si_ip) spring = wind_rose(df, "", spring_months, hours, True, si_ip) summer = wind_rose(df, "", summer_months, hours, False, si_ip) fall = wind_rose(df, "", fall_months, hours, False, si_ip) - # Text + query_calm_wind = f"{ColNames.WIND_SPEED} == 0" + winter_df = df.loc[ (df[ColNames.MONTH] <= winter_months[1]) | (df[ColNames.MONTH] >= winter_months[0]) ] - query_calm_wind = f"{ColNames.WIND_SPEED} == 0" winter_total_count = winter_df.shape[0] winter_calm_count = winter_df.query(query_calm_wind).shape[0] @@ -564,8 +500,7 @@ def seasonal_chart_caption(month_start, month_end, count, n_calm): f"Observations between the months of {month_start} and {month_end} " f"between 01:00 hours and 24:00 hours. " f"Selected observations {str(count)} of 8760, or " - f"{str(int(100 * (count / 8760)))} %. {str(n_calm)} observations have " - f"calm winds." + f"{str(int(100 * (count / 8760)))} %. {str(n_calm)} observations have calm winds." ) winter_text = seasonal_chart_caption( @@ -592,6 +527,7 @@ def seasonal_chart_caption(month_start, month_end, count, n_calm): fall_total_count, fall_calm_count, ) + units = generate_units(si_ip) return ( dcc.Graph( @@ -618,7 +554,6 @@ def seasonal_chart_caption(month_start, month_end, count, n_calm): @callback( - # Daily Graphs [ Output(ElementIds.MORNING_WIND_ROSE, "children"), Output(ElementIds.NOON_WIND_ROSE, "children"), @@ -627,7 +562,6 @@ 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"), ], - # General Input(ElementIds.ID_WIND_DF_STORE, "modified_timestamp"), [ State(ElementIds.ID_WIND_DF_STORE, "data"), @@ -636,20 +570,17 @@ def seasonal_chart_caption(month_start, month_end, count, n_calm): ], ) def update_daily_graphs(_, df, meta, si_ip): - """Update the contents of tab five. Passing in the info from the sliders and the general info (df, meta).""" - months = [1, 12] morning_times = [6, 13] noon_times = [14, 21] night_times = [22, 5] - # Wind Rose Graphs morning = wind_rose(df, "", months, morning_times, False, si_ip) noon = wind_rose(df, "", months, noon_times, False, si_ip) night = wind_rose(df, "", months, night_times, True, si_ip) - # Text query_calm_wind = f"{ColNames.WIND_SPEED} == 0" + morning_df = df.loc[ (df[ColNames.HOUR] >= morning_times[0]) & (df[ColNames.HOUR] <= morning_times[1]) @@ -675,21 +606,19 @@ def daily_chart_caption(hour_start, hour_end, count, calm_count): f"Observations between the months of Jan and Dec between " f"{str(hour_start)}:00 hours and {str(hour_end)}:00 hours. " f"Selected observations {count} of 8760, or " - f"{str(int(100 * (count / 8760)))}%. {calm_count} " - f"observations have calm winds." + f"{str(int(100 * (count / 8760)))}%. {calm_count} observations have calm winds." ) morning_text = daily_chart_caption( morning_times[0], morning_times[1], morning_total_count, morning_calm_count ) - noon_text = daily_chart_caption( noon_times[0], noon_times[1], noon_total_count, noon_calm_count ) - night_text = daily_chart_caption( night_times[0], night_times[1], night_total_count, night_calm_count ) + units = generate_units(si_ip) return ( dcc.Graph( diff --git a/requirements.txt b/requirements.txt index d2d7baa..14cdb68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,13 +7,13 @@ certifi==2025.7.14 charset-normalizer==3.4.2 cleanpy==0.5.1 click==8.2.1 -dash==2.15.0 +dash==3.2.0 dash-bootstrap-components==1.2.0 dash-core-components==2.0.0 dash-extensions==1.0.7 dash-html-components==2.0.0 dash-iconify==0.1.2 -dash-mantine-components==0.12.1 +dash-mantine-components==2.2.1 dash-table==5.0.0 dataclass-wizard==0.22.3 EditorConfig==0.17.1 diff --git a/tests/node/cypress/e2e/spec.cy.js b/tests/node/cypress/e2e/spec.cy.js index 8cb4133..38dea4f 100644 --- a/tests/node/cypress/e2e/spec.cy.js +++ b/tests/node/cypress/e2e/spec.cy.js @@ -1,7 +1,24 @@ +/* + ⚠️ IMPORTANT: This code is only needed when running tests with Cypress's default browser (Electron) + If you run tests with other browsers (e.g., Chrome, Firefox), comment out this entire block + The URL.canParse() API is not available in Cypress's bundled Node.js 18.17.1 environment + but is available in newer Node.js versions used by other browsers +*/ +/*Cypress.on('uncaught:exception', (err, runnable) => { + // Workaround for Cypress environment lacking support for `URL.canParse()` API + // This error does not happen in real browsers; it's safe to ignore during tests + if (err.message.includes('URL.canParse is not a function')) { + return false; + } +});*/ + function click_tab(name) { - cy.get('.nav-item') - .contains(name) - .click(); + // Open the sidebar (burger button is fixed on screen) + // Expand the main nav group if collapsed + cy.get('#nav-group-main').click({ force: true }); + // Locate tab item by ID prefix, then find label by text + cy.get('[id^="nav-"]', { timeout: 10000 }).contains(name).click({ force: true }); + cy.wait(500); } function load_epw() { @@ -13,7 +30,7 @@ describe('Clima', () => { cy.visit('http://127.0.0.1:8080'); cy.contains('CBE Clima Tool'); cy.contains('Current Location: N/A'); - + // Upload load_epw() cy.contains('The EPW was successfully loaded!'); @@ -26,7 +43,7 @@ describe('Clima', () => { cy.contains('Latitude: 44.5308'); cy.contains('Elevation above sea level: 37.0 m'); cy.contains('This file is based on data collected between 2004 and 2018'); - cy.contains('Köppen–Geiger climate zone: Cfa. Humid subtropical, no dry season.'); + cy.contains('Köppen-Geiger climate zone: Cfa. Humid subtropical, no dry season.'); cy.contains('Average yearly temperature: 14.5 °C'); cy.contains('Hottest yearly temperature (99%): 34.0 °C'); cy.contains('Coldest yearly temperature (1%): -2.0 °C'); @@ -101,9 +118,12 @@ describe('Clima', () => { load_epw() cy.contains('The EPW was successfully loaded!'); click_tab('Temperature and Humidity') - cy.contains('Global Value Ranges').click(); + // Expand the "Data Display Options" nav section to access controls + cy.get('#nav-group-controls', { timeout: 10000 }).should('exist').click({ force: true }); + cy.contains('Global', { timeout: 10000 }).click({ force: true }); cy.contains('-40'); // Global minimum: not something you see in Italy! - cy.contains('IP').click(); + cy.get('#nav-group-controls', { timeout: 10000 }).should('exist').click({ force: true }); + cy.contains('IP').click({ force: true }); cy.contains('100'); // Not a Celsius temperature! cy.contains('Dry bulb temperature (°F)'); });