diff --git a/.gitignore b/.gitignore index abea78e1af02098b09375c46cf37b1c817188008..7225628642c96ccb0fce0745faf79fb4483a8ece 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ log *.log *.swp *.swo +/js/tmp/ +was_dev_erose.json diff --git a/assets/style.css b/assets/style.css index 67b43361c53541ad73abec3e8fc9d2a6ff4f6983..c6e521271b9c9f8371509d5ed33d444ab0c9ee0c 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1,3 +1,7 @@ +body { + background-color: #212529; +} + a.modebar-btn { font-size: 2rem !important; margin: 1rem .2rem; @@ -81,7 +85,7 @@ a.modebar-btn { .description-title>p { height: 35px; - color: #2F4047; + color: #2Ferror_7; font-family: "Libre Baskerville"; font-size: 24px; font-weight: bold; @@ -765,3 +769,23 @@ div.SingleDatePickerInput { position: absolute; right: 5px; } + +/*styles for 404 page*/ +.error_links { + color: #F1B545; + padding-left: 20px; + text-decoration: underline; +} + +#error_title { + font-family:"Libre Baskerville", serif; + font-size: 4rem; +} + +#error_div { + position: absolute; + top: 30%; + left: 40%; + text-align:center; + color: white; +} diff --git a/assets/url.js b/assets/url.js new file mode 100644 index 0000000000000000000000000000000000000000..9e5a00eb21a7f3f45c772528e932188fb9b11f3b --- /dev/null +++ b/assets/url.js @@ -0,0 +1,159 @@ +// ==================== ROUTING FUNCTIONS ============================= +//convert date from DD MON YYYY to YYYYMMDD +function getDate() { + const dict = { + 'Jan': '01', + 'Feb': '02', + 'Mar': '03', + 'Apr': '04', + 'May': '05', + 'Jun': '06', + 'Jul': '07', + 'Aug': '08', + 'Sep': '09', + 'Oct': '10', + 'Nov': '11', + 'Dec': '12' + }; + date = document.getElementById('date').value; + splitDate = date.split(' '); + month = dict[splitDate[1]]; + return '&date=' + splitDate[2] + month + splitDate[0]; +} + +// LISTEN FOR MESSAGES FROM CMS +window.addEventListener("message", (event) => { + // CREATE LIST OF ADDRESSES + const hosts = [ + 'http://bscesdust02.bsc.es', + 'https://dust.aemet.es', + 'https://dust03.bsc.es' + ]; + if (hosts.includes(event.origin)){ + //console.log('DASH: Success!! ' + event.data); + window.history.pushState("Models", "Models", event.data); + return; + }else{ +// console.log('DASH: that is not the right address' + event.origin); + return; + } +}, false); + +// NOW ASSESS HOW THE URL SHOULD BE OUTPUT +function sendURL(url) { + window.history.pushState("Models", "Models", url); + parent.postMessage(url, '*'); +} + +//GET THE VARIABLE VALUE +function getVar() { + curvar = document.querySelector('.Select-value-label').innerHTML; + curvar = curvar.split(' ').join('_') + return 'var=' + curvar.toLowerCase() + '&'; +} + +// GET MODELS CHECKED +function getModels(){ + const data = [...document.querySelectorAll('.custom-control-input:checked')].map(e => e.value); + data.forEach(function(item, index){ + this[index] = 'model=' + item + }, data); + return data.join('&'); +} + +// CREATE OUTPUT URL FOR MODELS-APPLY +$(document).ready(function () { + $(document).on('click', "#models-apply", function () { + curvar = getVar() + models = getModels() + outputDate = getDate(); + url = "?" + curvar + models + outputDate; + sendURL(url); + }) +}); + +// CREATE OUTPUT URL FOR TABS +$(document).ready(function () { + $(document).on('click', ".tab--selected", function () { + tab = document.getElementsByClassName('tab--selected')[0].firstElementChild.innerHTML + tab = tab.toLowerCase() + url = "?tab=" + tab; + sendURL(url); + }) +}); + +// CREATE OUTPUT URL FOR GROUP-1-TOGGLE(MODELS) +$(document).ready(function () { + $(document).on('click', "#group-1-toggle", function () { + url = "?tab=forecast§ion=models"; + sendURL(url); + }) +}); + +// CREATE OUTPUT URL FOR GROUP-2-TOGGLE(PROBABILITY) +$(document).ready(function () { + $(document).on('click', "#group-2-toggle", function () { + url = "?tab=forecast§ion=prob"; + sendURL(url); + }) +}); + +// CREATE OUTPUT URL FOR GROUP-3-TOGGLE(WAS) +$(document).ready(function () { + $(document).on('click', "#group-3-toggle", function () { + url = "?tab=forecast§ion=was"; + sendURL(url); + }) +}); + +// CREATE WAS OUTPUT +$(document).ready(function () { + $(document).on('click', "#was-apply", function () { + const checked = document.getElementById('was-dropdown'); + var text = checked.querySelector('input[type="radio"]:checked').parentElement.innerText; + text = text.split(' ').join('_').toLowerCase(); + url = "?tab=forecast§ion=was&country=" + text; + sendURL(url); + }) +}); + +// CREATE OUTPUT URL FOR NRT-EVALUATION(VISUAL_COMPARISION) +$(document).ready(function () { + $(document).on('click', "#nrt-evaluation", function () { + url = "?tab=evaluation§ion=visual_comparison"; + sendURL(url); + }) +}); + +// CREATE OUTPUT URL FOR SCORES-EVALUATION(STATISTICS) +$(document).ready(function () { + $(document).on('click', "#scores-evaluation", function () { + url = "?tab=evaluation§ion=statistics"; + sendURL(url); + }) +}); + +// CREATE OUTPUT URL FOR SCORES-APPLY(STATISTICS) +$(document).ready(function () { + $(document).on('click', "#scores-apply", function () { + url = "?tab=evaluation§ion=statistics"; + sendURL(url); + }) +}); + +// CREATE OUTPUT URL FOR RGB(EUMETSAT RGB) +$(document).ready(function () { + $(document).on('click', "#rgb", function () { + url = "?tab=observations§ion=eumetsat-rgb"; + sendURL(url); + }) +}); + +// CREATE OUTPUT URL FOR VISIBILITY +$(document).ready(function () { + $(document).on('click', "#visibility", function () { + url = "?tab=observations§ion=visibility"; + sendURL(url); + }) +}); + diff --git a/conf/was.json b/conf/was.json deleted file mode 100644 index 64e9df938e013376ebd4f6730f6b8fa4ee3681cc..0000000000000000000000000000000000000000 --- a/conf/was.json +++ /dev/null @@ -1,253 +0,0 @@ -{ - "burkinafaso": - { - "name": "BURKINA FASO", - "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/burkinafaso/shp/BFA_adm1.shp", - "name_field": "NAME_1", - "center": [12.30, -1.20], - "zoom": 6.5, - "model": "median", - "var": "SCONC_DUST", - "path": "/data/daily_dashboard/was/{was}/h5/{date}", - "template": "{date}_{var}.h5", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" - }, - "values": { - "Boucle du Mouhoun" : [328, 411, 533], - "Cascades" : [237, 304, 379], - "Centre-Est" : [321, 387, 502], - "Centre-Nord" : [359, 453, 582], - "Centre-Ouest" : [308, 366, 456], - "Centre-Sud" : [301, 366, 452], - "Centre" : [301, 354, 460], - "Est" : [391, 476, 619], - "Haut-Bassins" : [287, 344, 448], - "Nord" : [365, 447, 616], - "Plateau-Central" : [310, 384, 465], - "Sahel" : [473, 596, 766], - "Sud-Ouest" : [258, 320, 392] - }, - "title": "Barcelona Dust Regional Center - Burkina Faso WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" - }, - "senegal": - { - "name": "SENEGAL", - "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/senegal/shp/SEN_adm1.shp", - "name_field": "ADM1_FR", - "center": [14.5, -14.4], - "zoom": 6.5, - "model": "median", - "var": "SCONC_DUST", - "path": "/data/daily_dashboard/was/{was}/h5/{date}", - "template": "{date}_{var}.h5", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" - }, - "values": { - "Diourbel" : [365, 464, 655], - "Fatick" : [314, 415, 549], - "Kaffrine" : [323, 404, 543], - "Kaolack" : [270, 343, 468], - "Kedougou" : [235, 292, 421], - "Kolda" : [250, 298, 410], - "Louga" : [572, 715, 932], - "Matam" : [566, 695, 935], - "Saint Louis" : [657, 798, 1037], - "Sedhiou" : [230, 285, 400], - "Tambacounda" : [379, 460, 606], - "Thies" : [322, 429, 616], - "Ziguinchor" : [241, 300, 423] - }, - "correspondence": { - "Dakar": "Thies" - }, - "title": "Barcelona Dust Regional Center - Senegal WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" - }, - "chad": - { - "name": "CHAD", - "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/chad/shp/TCD_adm1.shp", - "name_field": "admin1Na_1", - "center": [15, 19], - "zoom": 5.4, - "model": "median", - "var": "SCONC_DUST", - "path": "/data/daily_dashboard/was/{was}/h5/{date}", - "template": "{date}_{var}.h5", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" - }, - "values": { - "BATHA" : [1348, 1716, 2152], - "BORKOU" : [2389, 2643, 3345], - "CHARI-BAGUIRMI" : [552, 729, 889], - "GUERA" : [367, 475, 711], - "HADJER LAMIS" : [791, 969, 1213], - "KANEM" : [1840, 2210, 2692], - "LAC" : [1096, 1322, 1613], - "LOGONE OCCIDENTAL": [277, 403, 574], - "LOGONE ORIENTAL" : [238, 336, 518], - "MANDOUL" : [185, 253, 424], - "MAYO-KEBBI EST" : [439, 592, 790], - "MAYO-KEBBI OUEST" : [396, 522, 705], - "MOYEN-CHARI" : [174, 240, 398], - "OUADDAI" : [347, 427, 590], - "SALAMAT" : [225, 302, 501], - "TANDJILE" : [299, 408, 615], - "WADI FIRA" : [623, 757, 1096], - "BARH-EL-GAZEL" : [1512, 1918, 2314], - "ENNEDI EST" : [532, 689, 957], - "SILA" : [223, 281, 440], - "TIBESTI" : [1491, 1725, 2222], - "ENNEDI OUEST" : [1309, 1697, 2263] - }, - "correspondence": { - "NDJAMENA": "HADJER LAMIS" - }, - "title": "Barcelona Dust Regional Center - Chad WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" - }, - "mali": - { - "name": "MALI", - "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/mali/shp/MLI_adm1.shp", - "name_field": "ADM1_FR", - "center": [17, -4], - "zoom": 5.4, - "model": "median", - "var": "SCONC_DUST", - "path": "/data/daily_dashboard/was/{was}/h5/{date}", - "template": "{date}_{var}.h5", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" - }, - "values": { - "Gao" : [871, 1100, 1603], - "Kayes" : [365, 453, 618], - "Kidal" : [884, 1179, 1780], - "Koulikoro" : [360, 466, 647], - "Mopti" : [475, 586, 812], - "Ségou" : [399, 500, 718], - "Sikasso" : [292, 364, 474], - "Tombouctou": [1003, 1279, 1676] - }, - "correspondence": { - "Bamako": "Koulikoro", - "Menaka": "Kidal" - }, - "title": "Barcelona Dust Regional Center - Mali WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" - }, - "niger": - { - "name": "NIGER", - "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/niger/shp/NER_adm1.shp", - "name_field": "NOMREG", - "center": [17, 8], - "zoom": 5.4, - "model": "median", - "var": "SCONC_DUST", - "path": "/data/daily_dashboard/was/{was}/h5/{date}", - "template": "{date}_{var}.h5", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" - }, - "values": { - "AGADEZ" : [1346, 1607, 2014], - "DIFFA" : [1733, 2029, 2535], - "DOSSO" : [474, 584, 752], - "MARADI" : [592, 768, 976], - "TAHOUA" : [751, 963, 1397], - "TILLABERI" : [540, 671, 901], - "ZINDER" : [1110, 1316, 1698] - }, - "correspondence": { - "NIAMEY": "TILLABERI" - }, - "title": "Barcelona Dust Regional Center - Niger WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" - }, - "cabo_verde": - { - "name": "CAPE VERDE", - "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/cabo_verde/shp/CPV_admX.shp", - "name_field": "Ilha_1", - "center": [15.5, -24], - "zoom": 6, - "model": "median", - "var": "SCONC_DUST", - "path": "/data/daily_dashboard/was/{was}/h5/{date}", - "template": "{date}_{var}.h5", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" - }, - "values": { - "CaboVerdeBarlavento" : [137, 196, 345], - "CaboVerdeSotavento" : [142, 203, 345] - }, - "correspondence": { - "BOAVISTA" : "CaboVerdeBarlavento", - "BRAVA" : "CaboVerdeSotavento", - "FOGO" : "CaboVerdeSotavento", - "MAIO" : "CaboVerdeSotavento", - "SAL" : "CaboVerdeBarlavento", - "SANTIAGO" : "CaboVerdeSotavento", - "SANTO ANTAO" : "CaboVerdeBarlavento", - "SAO VICENTE" : "CaboVerdeBarlavento", - "SANTA LUZIA" : "CaboVerdeBarlavento", - "SAO NICOLAU" : "CaboVerdeBarlavento" - }, - "exclude" : [ "CaboVerdeBarlavento", "CaboVerdeSotavento"], - "title": "Barcelona Dust Regional Center - Cape Verde WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" - }, - "mauritania": - { - "name": "MAURITANIA", - "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/mauritania/shp/MRT_adm1.shp", - "name_field": "ADM1_EN", - "center": [21, -10], - "zoom": 5.4, - "model": "median", - "var": "SCONC_DUST", - "path": "/data/daily_dashboard/was/{was}/h5/{date}", - "template": "{date}_{var}.h5", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" - }, - "values": { - "Adrar" : [742, 921, 1213], - "Assaba" : [579, 727, 970], - "Brakna" : [770, 966, 1256], - "Dakhlet-Nouadhibou": [760, 893, 1126], - "Gorgol" : [584, 700, 947], - "Guidimakha" : [366, 436, 612], - "Hodh El Chargi" : [571, 707, 982], - "Hodh El Gharbi" : [504, 651, 892], - "Inchiri" : [800, 938, 1206], - "Nouakchott" : [507, 661, 890], - "Tagant" : [581, 766, 1048], - "Trarza" : [945, 1160, 1406], - "Tris-Zemmour" : [952, 1189, 1579] - }, - "title": "Barcelona Dust Regional Center - Mauritania WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" - } -} diff --git a/conf/was.json b/conf/was.json new file mode 120000 index 0000000000000000000000000000000000000000..63135090e3d4b56ff4a86280575f70b642bf6cb9 --- /dev/null +++ b/conf/was.json @@ -0,0 +1 @@ +was_dev_erose.json \ No newline at end of file diff --git a/conf/was_prod.json b/conf/was_prod.json new file mode 100644 index 0000000000000000000000000000000000000000..64e9df938e013376ebd4f6730f6b8fa4ee3681cc --- /dev/null +++ b/conf/was_prod.json @@ -0,0 +1,253 @@ +{ + "burkinafaso": + { + "name": "BURKINA FASO", + "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/burkinafaso/shp/BFA_adm1.shp", + "name_field": "NAME_1", + "center": [12.30, -1.20], + "zoom": 6.5, + "model": "median", + "var": "SCONC_DUST", + "path": "/data/daily_dashboard/was/{was}/h5/{date}", + "template": "{date}_{var}.h5", + "colors": { + "green" : "Normal", + "gold" : "High", + "darkorange": "Very High", + "red" : "Extremely High" + }, + "values": { + "Boucle du Mouhoun" : [328, 411, 533], + "Cascades" : [237, 304, 379], + "Centre-Est" : [321, 387, 502], + "Centre-Nord" : [359, 453, 582], + "Centre-Ouest" : [308, 366, 456], + "Centre-Sud" : [301, 366, 452], + "Centre" : [301, 354, 460], + "Est" : [391, 476, 619], + "Haut-Bassins" : [287, 344, 448], + "Nord" : [365, 447, 616], + "Plateau-Central" : [310, 384, 465], + "Sahel" : [473, 596, 766], + "Sud-Ouest" : [258, 320, 392] + }, + "title": "Barcelona Dust Regional Center - Burkina Faso WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + }, + "senegal": + { + "name": "SENEGAL", + "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/senegal/shp/SEN_adm1.shp", + "name_field": "ADM1_FR", + "center": [14.5, -14.4], + "zoom": 6.5, + "model": "median", + "var": "SCONC_DUST", + "path": "/data/daily_dashboard/was/{was}/h5/{date}", + "template": "{date}_{var}.h5", + "colors": { + "green" : "Normal", + "gold" : "High", + "darkorange": "Very High", + "red" : "Extremely High" + }, + "values": { + "Diourbel" : [365, 464, 655], + "Fatick" : [314, 415, 549], + "Kaffrine" : [323, 404, 543], + "Kaolack" : [270, 343, 468], + "Kedougou" : [235, 292, 421], + "Kolda" : [250, 298, 410], + "Louga" : [572, 715, 932], + "Matam" : [566, 695, 935], + "Saint Louis" : [657, 798, 1037], + "Sedhiou" : [230, 285, 400], + "Tambacounda" : [379, 460, 606], + "Thies" : [322, 429, 616], + "Ziguinchor" : [241, 300, 423] + }, + "correspondence": { + "Dakar": "Thies" + }, + "title": "Barcelona Dust Regional Center - Senegal WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + }, + "chad": + { + "name": "CHAD", + "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/chad/shp/TCD_adm1.shp", + "name_field": "admin1Na_1", + "center": [15, 19], + "zoom": 5.4, + "model": "median", + "var": "SCONC_DUST", + "path": "/data/daily_dashboard/was/{was}/h5/{date}", + "template": "{date}_{var}.h5", + "colors": { + "green" : "Normal", + "gold" : "High", + "darkorange": "Very High", + "red" : "Extremely High" + }, + "values": { + "BATHA" : [1348, 1716, 2152], + "BORKOU" : [2389, 2643, 3345], + "CHARI-BAGUIRMI" : [552, 729, 889], + "GUERA" : [367, 475, 711], + "HADJER LAMIS" : [791, 969, 1213], + "KANEM" : [1840, 2210, 2692], + "LAC" : [1096, 1322, 1613], + "LOGONE OCCIDENTAL": [277, 403, 574], + "LOGONE ORIENTAL" : [238, 336, 518], + "MANDOUL" : [185, 253, 424], + "MAYO-KEBBI EST" : [439, 592, 790], + "MAYO-KEBBI OUEST" : [396, 522, 705], + "MOYEN-CHARI" : [174, 240, 398], + "OUADDAI" : [347, 427, 590], + "SALAMAT" : [225, 302, 501], + "TANDJILE" : [299, 408, 615], + "WADI FIRA" : [623, 757, 1096], + "BARH-EL-GAZEL" : [1512, 1918, 2314], + "ENNEDI EST" : [532, 689, 957], + "SILA" : [223, 281, 440], + "TIBESTI" : [1491, 1725, 2222], + "ENNEDI OUEST" : [1309, 1697, 2263] + }, + "correspondence": { + "NDJAMENA": "HADJER LAMIS" + }, + "title": "Barcelona Dust Regional Center - Chad WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + }, + "mali": + { + "name": "MALI", + "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/mali/shp/MLI_adm1.shp", + "name_field": "ADM1_FR", + "center": [17, -4], + "zoom": 5.4, + "model": "median", + "var": "SCONC_DUST", + "path": "/data/daily_dashboard/was/{was}/h5/{date}", + "template": "{date}_{var}.h5", + "colors": { + "green" : "Normal", + "gold" : "High", + "darkorange": "Very High", + "red" : "Extremely High" + }, + "values": { + "Gao" : [871, 1100, 1603], + "Kayes" : [365, 453, 618], + "Kidal" : [884, 1179, 1780], + "Koulikoro" : [360, 466, 647], + "Mopti" : [475, 586, 812], + "Ségou" : [399, 500, 718], + "Sikasso" : [292, 364, 474], + "Tombouctou": [1003, 1279, 1676] + }, + "correspondence": { + "Bamako": "Koulikoro", + "Menaka": "Kidal" + }, + "title": "Barcelona Dust Regional Center - Mali WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + }, + "niger": + { + "name": "NIGER", + "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/niger/shp/NER_adm1.shp", + "name_field": "NOMREG", + "center": [17, 8], + "zoom": 5.4, + "model": "median", + "var": "SCONC_DUST", + "path": "/data/daily_dashboard/was/{was}/h5/{date}", + "template": "{date}_{var}.h5", + "colors": { + "green" : "Normal", + "gold" : "High", + "darkorange": "Very High", + "red" : "Extremely High" + }, + "values": { + "AGADEZ" : [1346, 1607, 2014], + "DIFFA" : [1733, 2029, 2535], + "DOSSO" : [474, 584, 752], + "MARADI" : [592, 768, 976], + "TAHOUA" : [751, 963, 1397], + "TILLABERI" : [540, 671, 901], + "ZINDER" : [1110, 1316, 1698] + }, + "correspondence": { + "NIAMEY": "TILLABERI" + }, + "title": "Barcelona Dust Regional Center - Niger WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + }, + "cabo_verde": + { + "name": "CAPE VERDE", + "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/cabo_verde/shp/CPV_admX.shp", + "name_field": "Ilha_1", + "center": [15.5, -24], + "zoom": 6, + "model": "median", + "var": "SCONC_DUST", + "path": "/data/daily_dashboard/was/{was}/h5/{date}", + "template": "{date}_{var}.h5", + "colors": { + "green" : "Normal", + "gold" : "High", + "darkorange": "Very High", + "red" : "Extremely High" + }, + "values": { + "CaboVerdeBarlavento" : [137, 196, 345], + "CaboVerdeSotavento" : [142, 203, 345] + }, + "correspondence": { + "BOAVISTA" : "CaboVerdeBarlavento", + "BRAVA" : "CaboVerdeSotavento", + "FOGO" : "CaboVerdeSotavento", + "MAIO" : "CaboVerdeSotavento", + "SAL" : "CaboVerdeBarlavento", + "SANTIAGO" : "CaboVerdeSotavento", + "SANTO ANTAO" : "CaboVerdeBarlavento", + "SAO VICENTE" : "CaboVerdeBarlavento", + "SANTA LUZIA" : "CaboVerdeBarlavento", + "SAO NICOLAU" : "CaboVerdeBarlavento" + }, + "exclude" : [ "CaboVerdeBarlavento", "CaboVerdeSotavento"], + "title": "Barcelona Dust Regional Center - Cape Verde WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + }, + "mauritania": + { + "name": "MAURITANIA", + "shp": "/home/fbeninca/interactive-forecast-viewer/conf/was/mauritania/shp/MRT_adm1.shp", + "name_field": "ADM1_EN", + "center": [21, -10], + "zoom": 5.4, + "model": "median", + "var": "SCONC_DUST", + "path": "/data/daily_dashboard/was/{was}/h5/{date}", + "template": "{date}_{var}.h5", + "colors": { + "green" : "Normal", + "gold" : "High", + "darkorange": "Very High", + "red" : "Extremely High" + }, + "values": { + "Adrar" : [742, 921, 1213], + "Assaba" : [579, 727, 970], + "Brakna" : [770, 966, 1256], + "Dakhlet-Nouadhibou": [760, 893, 1126], + "Gorgol" : [584, 700, 947], + "Guidimakha" : [366, 436, 612], + "Hodh El Chargi" : [571, 707, 982], + "Hodh El Gharbi" : [504, 651, 892], + "Inchiri" : [800, 938, 1206], + "Nouakchott" : [507, 661, 890], + "Tagant" : [581, 766, 1048], + "Trarza" : [945, 1160, 1406], + "Tris-Zemmour" : [952, 1189, 1579] + }, + "title": "Barcelona Dust Regional Center - Mauritania WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + } +} diff --git a/dash_server.py b/dash_server.py index 9ca676275a6de7b401861c4daa0bbda0c0fb4876..a760979c1282ee983a03d686803828c38011a6fc 100755 --- a/dash_server.py +++ b/dash_server.py @@ -12,45 +12,45 @@ from dash.dependencies import Input from dash.dependencies import State from dash.dependencies import ALL from dash.dependencies import MATCH +from dash.dependencies import ClientsideFunction from dash.exceptions import PreventUpdate import flask from flask import g, make_response, request from flask_caching import Cache #from pyinstrument import Profiler from pathlib import Path +from datetime import datetime as dt +from datetime import timedelta +from urllib.parse import urlparse, parse_qs from data_handler import DEFAULT_VAR from data_handler import DEFAULT_MODEL from data_handler import VARS from data_handler import MODELS from data_handler import DEBUG +from data_handler import DATES +from data_handler import cache, cache_timeout +from data_handler import pathname +from data_handler import ALIASES +from data_handler import ROUTE_DEFAULTS from tabs.forecast import tab_forecast from tabs.forecast import sidebar_forecast -from tabs.forecast_callbacks import register_callbacks as fcst_callbacks from tabs.evaluation import tab_evaluation from tabs.evaluation import sidebar_evaluation -from tabs.evaluation_callbacks import register_callbacks as eval_callbacks from tabs.observations import tab_observations from tabs.observations import sidebar_observations -from tabs.observations_callbacks import register_callbacks as obs_callbacks from tabs.fullscreen import go_fullscreen import uuid import socket -HOSTNAME = socket.gethostbyname_ex(socket.gethostname())[0] - -if HOSTNAME in ('bscesdust03.bsc.es', 'dust.hqads.aemet.es'): - pathname = '/daily_dashboard/' -else: - pathname = '/dashboard/' +from router import * TIMEOUT = 10 fontawesome = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css' leaflet = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" -# fontawesome = 'https://use.fontawesome.com/releases/v5.15.4/css/all.css' srv = flask.Flask(__name__) app = dash.Dash(__name__, @@ -69,17 +69,7 @@ app.scripts.config.serve_locally = True app.config.suppress_callback_exceptions = True server = app.server -cache_dir = "/dev/shm/{}".format(str(uuid.uuid1())) -Path(cache_dir).mkdir(parents=True, exist_ok=True) - -cache_config = { - "DEBUG": True, - "CACHE_TYPE": "FileSystemCache", - "CACHE_DIR": cache_dir, -} - -cache = Cache(server, config=cache_config) -cache_timeout = 86400 +cache.init_app(server) try: cache.clear() @@ -87,7 +77,6 @@ except Exception as e: print('CACHE CLEAR ERROR:', str(e)) pass - app.index_string = """ @@ -132,61 +121,15 @@ app.index_string = """ """ if DEBUG: print('SERVER: start creating app layout') -app.layout = html.Div( - children=[ - html.Div( - id='app-sidebar', - children=sidebar_forecast(VARS, DEFAULT_VAR, MODELS, DEFAULT_MODEL), - className='sidebar' - ), - dcc.Tabs(id='app-tabs', value='forecast-tab', children=[ - tab_forecast(), - tab_evaluation(), - tab_observations(), - # go_fullscreen(pathname), - ]), - ], - className="content", -) +#SET APP LAYOUT TO BE POPULATED +app.layout = html.Div([dcc.Location(id="url", refresh=False)], id="content") + if DEBUG: print('SERVER: stop creating app layout') - -@app.callback( - Output('app-sidebar', 'children'), - [Input('app-tabs', 'value')], -) -def render_sidebar(tab): - """ Function rendering requested tab """ - tabs = { - 'forecast-tab' : [ - sidebar_forecast, - (VARS, DEFAULT_VAR, MODELS, DEFAULT_MODEL) - ], - 'evaluation-tab' : [ - sidebar_evaluation, - None - ], - 'observations-tab' : [ - sidebar_observations, - None - ], - 'fullscreen-tab' : [ - 'Full' - ] - } - - if tabs[tab][0] is 'Full': - return - elif tabs[tab][1] is None: - return tabs[tab][0]() - return tabs[tab][0](*tabs[tab][1]) - - -fcst_callbacks(app, cache=cache, cache_timeout=cache_timeout) -eval_callbacks(app, cache=cache, cache_timeout=cache_timeout) -obs_callbacks(app, cache=cache, cache_timeout=cache_timeout) - +from tabs.forecast_callbacks import * +from tabs.evaluation_callbacks import * +from tabs.observations_callbacks import * if __name__ == '__main__': app.run_server(debug=True, # use_reloader=False, # processes=4, threaded=False, diff --git a/data_handler.py b/data_handler.py index da3b67af223fc8fc637fcd5d9a051ee54599eb8e..28479cf19eb3288fe36efd68c76639f196a34c5f 100644 --- a/data_handler.py +++ b/data_handler.py @@ -23,12 +23,41 @@ import requests import calendar import time import os +from datetime import datetime as dt +from datetime import timedelta from utils import concat_dataframes from utils import retrieve_timeseries from utils import retrieve_single_point from utils import get_colorscale +from pathlib import Path +import flask +from flask_caching import Cache +import uuid +import socket + +#SETUP BASE URL +HOSTNAME = socket.gethostbyname_ex(socket.gethostname())[0] + +if HOSTNAME in ('bscesdust03.bsc.es', 'dust.hqads.aemet.es'): + pathname = '/daily_dashboard/' +else: + pathname = '/dashboard/' + +#SETUP CACHE +cache_dir = "/dev/shm/{}".format(str(uuid.uuid1())) +Path(cache_dir).mkdir(parents=True, exist_ok=True) + +cache_config = { + "DEBUG": True, + "CACHE_TYPE": "FileSystemCache", + "CACHE_DIR": cache_dir, +} + +cache = Cache(config=cache_config) +cache_timeout = 86400 + DIR_PATH = os.path.dirname(os.path.realpath(__file__)) @@ -62,6 +91,35 @@ WAS = json.load(open(os.path.join(DIR_PATH, 'conf/was.json'))) PROB = json.load(open(os.path.join(DIR_PATH, 'conf/prob.json'))) DATES = json.load(open(os.path.join(DIR_PATH, 'conf/dates.json'))) +ALIASES= { + 'forecast': 'forecast-tab', + 'evaluation': 'evaluation-tab', + 'observations': 'observations-tab', + 'aod': 'OD550_DUST', + 'concentration': 'SCONC_DUST', + 'dry_deposition': 'DUST_DEPD', + 'wet_deposition': 'DUST_DEPW', + 'load': 'DUST_LOAD', + 'extinction':'DUST_EXT', + 'visual_comparison': 'nrt', + 'statistics': 'scores', + 'eumetsat-rgb': 'rgb', + 'burkina_faso': 'burkinafaso', + 'cape_verde': 'cabo_verde' + } + +ROUTE_DEFAULTS = { + 'tab':['forecast'], + 'var': ['OD550_DUST'], + 'model': ['median'], + 'for_option': ['models'], + 'eval_option': ['nrt'], + 'obs_option': ['rgb'], + 'country': ['burkinafaso'], + 'download': [None], + 'date': [DATES['end_date'] or (dt.now() - timedelta(days=1)).strftime("%Y%m%d")] + } + STATS = OrderedDict({ 'bias': 'BIAS', 'corr': 'CORR', 'rmse': 'RMSE', 'frge': 'FGE', 'totn': 'TOTAL CASES' }) STATS_CONF = OrderedDict( { diff --git a/router.py b/router.py new file mode 100644 index 0000000000000000000000000000000000000000..2cce658a537f1122a7b04a2237aae03c623f8047 --- /dev/null +++ b/router.py @@ -0,0 +1,133 @@ +from urllib.parse import urlparse, parse_qs +import dash +import dash_bootstrap_components as dbc +from dash import dcc +from dash import html +from dash.dependencies import Output +from dash.dependencies import Input +from dash.dependencies import State +from dash.dependencies import ALL +from data_handler import DEFAULT_VAR +from data_handler import DEFAULT_MODEL +from data_handler import VARS +from data_handler import MODELS +from data_handler import DEBUG +from data_handler import DATES +from data_handler import cache, cache_timeout +from data_handler import pathname +from data_handler import ALIASES +from data_handler import ROUTE_DEFAULTS + +from tabs.forecast import tab_forecast +from tabs.forecast import sidebar_forecast +from tabs.evaluation import tab_evaluation +from tabs.evaluation import sidebar_evaluation +from tabs.observations import tab_observations +from tabs.observations import sidebar_observations +from tabs.fullscreen import go_fullscreen + +#-------------------- ADD ROUTING FUNCTIONS ---------------------- +def get_input_aliases(route_selections): + """ Change user inputs into internal variables""" + for key, value in route_selections.items(): + #preserve the list of models by only changing the individual model name + if key == 'model': + route_selections[key] = ['median' if x=='multi-model' else x for x in route_selections[key]] + elif value[0] in ALIASES: + route_selections[key] = [ALIASES[value[0]]] + return route_selections + +def eval_section_query(queries): + """pull SECTION var from url query and assign value to appropriate option""" + if 'section' in queries: + if queries['tab']==['forecast-tab']: + queries['for_option'] = queries['section'] + elif queries['tab']==['evaluation-tab']: + queries['eval_option'] = queries['section'] + elif queries['tab']==['observations-tab']: + queries['obs_option'] = queries['section'] + return queries + +def get_url_queries(url, ROUTE_DEFAULTS=ROUTE_DEFAULTS): + """ pull all of the keyword arguments from url and update ROUTE_DEFAULTS dict as necessary""" + parsed_url = urlparse(url, scheme='http', allow_fragments=False) + queries = parse_qs(parsed_url[4]) + route_selections = ROUTE_DEFAULTS.copy() + route_selections.update(queries) + route_selections = get_input_aliases(route_selections) + route_selections = eval_section_query(route_selections) + return route_selections + +@dash.callback( + Output('app-sidebar', 'children'), + [Input('app-tabs', 'value')], + prevent_initial_call=True +) +def render_sidebar(tab='forecast-tab', route_selections=ROUTE_DEFAULTS): + """ Function rendering requested tab """ + tabs = { + 'forecast-tab' : sidebar_forecast(VARS, route_selections['var'][0], MODELS, route_selections['model'], window=route_selections['for_option'][0], country=route_selections['country'][0]), + 'evaluation-tab' : sidebar_evaluation(route_selections['eval_option'][0]), + 'observations-tab' : sidebar_observations(route_selections['obs_option'][0]), + #'fullscreen-tab' : 'Full' + } + if tabs[tab][0] is 'Full': + return + return tabs[tab] + +def render404(): + """Create a 404 page""" + #setup routes for links + forecast_link = pathname + "/?tab=forecast" + eval_link = pathname + "/?tab=evaluation" + obs_link = pathname + "/?tab=observations" + + page = [html.Div( + className='background', + children=[ + html.Div( + id='error_div', + children=[html.H2('404 Error', id='error_title'), + html.P("Sorry we can't find the page you were looking for."), + html.P("Here are some helpful links that might help:"), + dcc.Link("Forecast", id='forecast_link', href=forecast_link, className='error_links',target='_parent', refresh=True), + html.Br(), + html.Br(), + dcc.Link("Evaluation", id='evaluation_link', href=eval_link, className='error_links',target='_parent', refresh=True), + html.Br(), + html.Br(), + dcc.Link("Observations", id='observations_link', href=obs_link, className='error_links',target='_parent', refresh=True) + ], + ) + ] + ) + ] + return page + +@dash.callback( + Output("content", "children"), + Input("url", "href"), +) +def router(url): + """ Get url search queries and build layout for app""" + route_selections = get_url_queries(url) + print('===== route_selections', route_selections) + try: + children = [ + html.Div( + id='app-sidebar', + children=render_sidebar(route_selections['tab'][0], + route_selections), + className='sidebar' + ), + dcc.Tabs(id='app-tabs', value=route_selections['tab'][0] , children=[ + tab_forecast(window=route_selections['for_option'][0], end_date=route_selections['date'][0]),#'models', 'prob', 'was' + tab_evaluation(route_selections['eval_option'][0]), #nrt or scores + tab_observations(route_selections['obs_option'][0]),#rgb or visibility + # go_fullscreen(pathname), + ]), + ] + except: #This handles when user inputs incorrect URL params + children = render404() + return children + diff --git a/tabs/evaluation.py b/tabs/evaluation.py index 3274af8497b0cf71910aa2a57a82524763bfe794..0d22557a162482ef5a8d0e0f551017b42c1445df 100644 --- a/tabs/evaluation.py +++ b/tabs/evaluation.py @@ -428,7 +428,16 @@ def tab_evaluation(window='nrt'): ) -def sidebar_evaluation(): +def sidebar_evaluation(window='nrt'): + """ Build the evaluation sidebar""" + + #Highlight the selected section + nrt_style = { 'fontWeight': 'bold' } + scores_style = { 'fontWeight': 'normal' } + if window == 'scores': + nrt_style={ 'fontWeight': 'normal' } + scores_style= { 'fontWeight': 'bold' } + return [html.Div([ html.Label("Variable"), dcc.Dropdown( @@ -447,57 +456,17 @@ def sidebar_evaluation(): html.Div([ dbc.Button("Visual comparison", color="link", - id='nrt-evaluation' + id='nrt-evaluation', + style = nrt_style )], className="sidebar-item", ), html.Div([ dbc.Button("Statistics", color="link", - id='scores-evaluation' + id='scores-evaluation', + style = scores_style )], className="sidebar-item", ), -# html.Div([ -# dbc.Row([ -# dbc.Col( -# dbc.Button( -# "", -# id="info-button", -# ), -# width=12, -# ), -# ], -# no_gutters=True, -# ), -# dbc.Row([ -# dbc.Col([ -# dbc.Collapse( -# dbc.Card(dbc.CardBody( -# [ -# html.Button('USER GUIDE', -# id='btn-userguide-download', -# n_clicks=0, -# className='download-section', -# ), -# html.H6("Glossary"), -# html.P(""" -# 1. Variables: Lorem ipsum dolor sit amet, consectetur adipiscing elit."""), -# html.P(""" -# 2. Comparing: Lorem ipsum dolor sit , conssit amet, sit amet, consecte dolor elit."""), -# html.P(""" -# 3. User Oriented Products: Lorem ipsum dolor sit amet, consectetur adipiscing elitLorem consectetur adipiscing elit"""), -# ], -# className="card-text", -# )), -# id="info-collapse", -# is_open=False, -# ), -# ], -# className="collapsible-cards", -# ), -# ], -# className="sidebar-bottom", -# ) -# ]) ] diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 2cda840be0596d79ed1a32f8887ff0ad896c5f7f..20238fab24f2c917d9e3ff2012683bf571a8b41d 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -38,6 +38,7 @@ import orjson import os.path from random import random +from data_handler import cache, cache_timeout SCORES = list(STATS.keys()) start_date = DATES['start_date'] @@ -54,718 +55,719 @@ def extend_l(l): return res -def register_callbacks(app, cache, cache_timeout): - """ Registering callbacks """ - - @app.callback( - [Output('evaluation-tab', 'children'), - Output('nrt-evaluation', 'style'), - Output('scores-evaluation', 'style')], - [Input('nrt-evaluation', 'n_clicks'), - Input('scores-evaluation', 'n_clicks')], - ) - def render_evaluation_tab(nrtbutton, scoresbutton): - """ Function rendering requested tab """ - bold = { 'fontWeight': 'bold' } - norm = { 'fontWeight': 'normal' } - ctx = dash.callback_context - - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - - if button_id == "nrt-evaluation" and nrtbutton: - return tab_evaluation('nrt'), bold, norm - elif button_id == "scores-evaluation" and scoresbutton: - return tab_evaluation('scores'), norm, bold - - return dash.no_update, bold, norm - #raise PreventUpdate - - @app.callback( - [Output('obs-selection-dropdown','options'), - Output('obs-selection-dropdown','placeholder')], - [Input('obs-timescale-dropdown', 'value')], - [State('obs-network-dropdown', 'value')], - prevent_initial_call=True - ) - def update_time_selection(timescale, network): - - if timescale is None: - raise PreventUpdate - - if network == 'modis': - start_date = '20180101' - elif network == 'aeronet': - start_date = '20120101' - - seasons = { - '03': 'Spring', - '06': 'Summer', - '09': 'Autumn', - '12': 'Winter' - } - - if timescale == 'seasonal': - ret = [{ - 'label' : '{} {}'.format(seasons[mon.strftime('%m')], - mon.strftime('%Y')), - 'value' : '{}-{}'.format( - mon.strftime('%Y%m'), (mon + relativedelta(months=2)).strftime('%Y%m')) - } - for mon in pd.date_range(start_date, end_date, freq='Q')[::-1]][1:] - placeholder = 'Select season' - elif timescale == 'annual': - ret = [{ - 'label': mon.strftime('%Y'), - 'value': '{year}01-{year}12'.format(year=mon.strftime('%Y')), - } for mon in - pd.date_range(start_date, end_date, freq='A')[::-1]] - placeholder = 'Select year' - else: # timescale == 'monthly': - ret = [{ - 'label': mon.strftime('%B %Y'), - 'value': mon.strftime('%Y%m'), - } for mon in - pd.date_range(start_date, end_date, freq='M')[::-1]] - placeholder = 'Select month' - - return ret, placeholder - - def _no_modis_data(): - """ Return the data needed to make MODIS table output NO DATA""" - return [{'name': 'NO DATA', 'id': ''}], [], { 'display': 'block'} - - @app.callback( - [Output('modis-scores-table', 'columns'), - Output('modis-scores-table', 'data'), - Output('modis-scores-table', 'style_table')], - [Input('scores-apply', 'n_clicks')], - [State('obs-models-dropdown', 'value'), - State('obs-statistics-dropdown', 'value'), - State('obs-network-dropdown', 'value'), - State('obs-timescale-dropdown', 'value'), - State('obs-selection-dropdown', 'value')], - prevent_initial_call=True - ) - def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection): - """ Read scores tables and show data """ - - if not n or network != 'modis': - return dash.no_update, dash.no_update, { 'display': 'none' } - - if isinstance(models, str): - models = [models] - - if isinstance(stat, str): - stat = [stat] - - stat = ['model'] + stat - - if DEBUG: print("###########", models, stat, network, timescale, selection, n) - filedir = OBS[network]['path'] - filename = "{}_scores.h5".format(selection) - tab_name = "total_{}".format(selection) - filepath = os.path.join(filedir, "h5", filename) - if not os.path.exists(filepath): - return _no_modis_data() - df = pd.read_hdf(filepath, tab_name) - ret = df.loc[df['model'].isin(models), stat] - ret['model'] = ret['model'].map({k:MODELS[k]['name'] for k in MODELS}) - if DEBUG: print('---', ret.columns) - if DEBUG: print('---', ret.to_dict('records')) - # Check if there is any data in returned table, return No Data if not - if len(ret.to_dict('records')) < 1: - return _no_modis_data() - columns = [{'name': i in SCORES and - STATS[i] or '', 'id': i} for - i in stat] - return columns, ret.replace('_', ' ', regex=True).to_dict('records'), { 'display': 'block' } - - - @app.callback( - [Output('scores-map-modalbody', 'figure'), - Output('scores-map-modal', 'is_open'), - Output('obs-models-dropdown-modal', 'value'), - Output('obs-statistics-dropdown-modal', 'value')], - [Input('scores-map-apply', 'n_clicks'), - Input('obs-models-dropdown-modal', 'value'), - Input('obs-statistics-dropdown-modal', 'value'),], - [State('obs-network-dropdown', 'value'), - State('obs-selection-dropdown', 'value'), - State('obs-models-dropdown', 'value'), - State('obs-statistics-dropdown', 'value')], - ) - @cache.memoize(timeout=cache_timeout) - def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, orig_stats): - """ Read scores tables and plot maps """ - from tools import get_scores_figure - mb = MODEBAR_LAYOUT_TS +#def register_callbacks(app, cache, cache_timeout): +# """ Registering callbacks """ + +@dash.callback( + [Output('evaluation-tab', 'children'), + Output('nrt-evaluation', 'style'), + Output('scores-evaluation', 'style')], + [Input('nrt-evaluation', 'n_clicks'), + Input('scores-evaluation', 'n_clicks')], + prevent_initial_call=True +) +def render_evaluation_tab(nrtbutton, scoresbutton): + """ Function rendering requested tab """ + bold = { 'fontWeight': 'bold' } + norm = { 'fontWeight': 'normal' } + ctx = dash.callback_context + + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + + if button_id == "nrt-evaluation" and nrtbutton: + return tab_evaluation('nrt'), bold, norm + elif button_id == "scores-evaluation" and scoresbutton: + return tab_evaluation('scores'), norm, bold + + return dash.no_update, bold, norm + #raise PreventUpdate + +@dash.callback( + [Output('obs-selection-dropdown','options'), + Output('obs-selection-dropdown','placeholder')], + [Input('obs-timescale-dropdown', 'value')], + [State('obs-network-dropdown', 'value')], + prevent_initial_call=True +) +def update_time_selection(timescale, network): + + if timescale is None: + raise PreventUpdate - ctx = dash.callback_context - - if DEBUG: print(':::', n_clicks, model, score, network, selection) - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id != "scores-map-apply": - if model is not None and score is not None: - if DEBUG: print('::: 1 :::') - figure = get_scores_figure(network, model, score, selection) - figure.update_layout(mb) - return figure, True, model, score - - raise PreventUpdate - - if orig_model and orig_stats: - if DEBUG: print(':::', orig_model, orig_stats, ':::') - curr_model = [mod for mod in MODELS if mod in orig_model][0] - curr_stat = [sc for sc in SCORES if sc in orig_stats][0] - figure = get_scores_figure(network, curr_model, curr_stat, selection) - figure.update_layout(mb) - return figure, True, curr_model, curr_stat - else: - print('::: 2.5 :::') + if network == 'modis': + start_date = '20180101' + elif network == 'aeronet': + start_date = '20120101' + + seasons = { + '03': 'Spring', + '06': 'Summer', + '09': 'Autumn', + '12': 'Winter' + } + + if timescale == 'seasonal': + ret = [{ + 'label' : '{} {}'.format(seasons[mon.strftime('%m')], + mon.strftime('%Y')), + 'value' : '{}-{}'.format( + mon.strftime('%Y%m'), (mon + relativedelta(months=2)).strftime('%Y%m')) + } + for mon in pd.date_range(start_date, end_date, freq='Q')[::-1]][1:] + placeholder = 'Select season' + elif timescale == 'annual': + ret = [{ + 'label': mon.strftime('%Y'), + 'value': '{year}01-{year}12'.format(year=mon.strftime('%Y')), + } for mon in + pd.date_range(start_date, end_date, freq='A')[::-1]] + placeholder = 'Select year' + else: # timescale == 'monthly': + ret = [{ + 'label': mon.strftime('%B %Y'), + 'value': mon.strftime('%Y%m'), + } for mon in + pd.date_range(start_date, end_date, freq='M')[::-1]] + placeholder = 'Select month' + + return ret, placeholder + +def _no_modis_data(): + """ Return the data needed to make MODIS table output NO DATA""" + return [{'name': 'NO DATA', 'id': ''}], [], { 'display': 'block'} + +@dash.callback( + [Output('modis-scores-table', 'columns'), + Output('modis-scores-table', 'data'), + Output('modis-scores-table', 'style_table')], + [Input('scores-apply', 'n_clicks')], + [State('obs-models-dropdown', 'value'), + State('obs-statistics-dropdown', 'value'), + State('obs-network-dropdown', 'value'), + State('obs-timescale-dropdown', 'value'), + State('obs-selection-dropdown', 'value')], + prevent_initial_call=True +) +def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection): + """ Read scores tables and show data """ + + if not n or network != 'modis': + return dash.no_update, dash.no_update, { 'display': 'none' } + + if isinstance(models, str): + models = [models] + + if isinstance(stat, str): + stat = [stat] + + stat = ['model'] + stat + + if DEBUG: print("###########", models, stat, network, timescale, selection, n) + filedir = OBS[network]['path'] + filename = "{}_scores.h5".format(selection) + tab_name = "total_{}".format(selection) + filepath = os.path.join(filedir, "h5", filename) + if not os.path.exists(filepath): + return _no_modis_data() + df = pd.read_hdf(filepath, tab_name) + ret = df.loc[df['model'].isin(models), stat] + ret['model'] = ret['model'].map({k:MODELS[k]['name'] for k in MODELS}) + if DEBUG: print('---', ret.columns) + if DEBUG: print('---', ret.to_dict('records')) + # Check if there is any data in returned table, return No Data if not + if len(ret.to_dict('records')) < 1: + return _no_modis_data() + columns = [{'name': i in SCORES and + STATS[i] or '', 'id': i} for + i in stat] + return columns, ret.replace('_', ' ', regex=True).to_dict('records'), { 'display': 'block' } + + +@dash.callback( + [Output('scores-map-modalbody', 'figure'), + Output('scores-map-modal', 'is_open'), + Output('obs-models-dropdown-modal', 'value'), + Output('obs-statistics-dropdown-modal', 'value')], + [Input('scores-map-apply', 'n_clicks'), + Input('obs-models-dropdown-modal', 'value'), + Input('obs-statistics-dropdown-modal', 'value'),], + [State('obs-network-dropdown', 'value'), + State('obs-selection-dropdown', 'value'), + State('obs-models-dropdown', 'value'), + State('obs-statistics-dropdown', 'value')], +) +@cache.memoize(timeout=cache_timeout) +def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, orig_stats): + """ Read scores tables and plot maps """ + from tools import get_scores_figure + mb = MODEBAR_LAYOUT_TS + + ctx = dash.callback_context + + if DEBUG: print(':::', n_clicks, model, score, network, selection) + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if button_id != "scores-map-apply": + if model is not None and score is not None: + if DEBUG: print('::: 1 :::') figure = get_scores_figure(network, model, score, selection) figure.update_layout(mb) return figure, True, model, score + raise PreventUpdate + + if orig_model and orig_stats: + if DEBUG: print(':::', orig_model, orig_stats, ':::') + curr_model = [mod for mod in MODELS if mod in orig_model][0] + curr_stat = [sc for sc in SCORES if sc in orig_stats][0] + figure = get_scores_figure(network, curr_model, curr_stat, selection) + figure.update_layout(mb) + return figure, True, curr_model, curr_stat + else: + print('::: 2.5 :::') + figure = get_scores_figure(network, model, score, selection) + figure.update_layout(mb) + return figure, True, model, score + # if model is not None and score is not None: # if DEBUG: print('::: 3 :::') # figure = get_scores_figure(network, model, score, selection) # figure.update_layout(mb) # return figure, True, orig_model[0], orig_stats[0] - return dash.no_update, False, dash.no_update, dash.no_update # PreventUpdate - - - @app.callback( - extend_l([ - [Output('aeronet-scores-table-{}'.format(score), 'columns'), - Output('aeronet-scores-table-{}'.format(score), 'data'), - Output('aeronet-scores-table-{}'.format(score), 'style_table'), - Output('aeronet-scores-table-{}'.format(score), 'selected_cells'), - Output('aeronet-scores-table-{}'.format(score), 'active_cell')] - for score in SCORES]), - [Input('scores-apply', 'n_clicks'), - *[Input('aeronet-scores-table-{}'.format(score), 'active_cell') - for score in SCORES]], - [State('obs-models-dropdown', 'value'), - State('obs-statistics-dropdown', 'value'), - State('obs-network-dropdown', 'value'), - State('obs-timescale-dropdown', 'value'), - State('obs-selection-dropdown', 'value')] + - extend_l([[State('aeronet-scores-table-{}'.format(score), 'columns'), - State('aeronet-scores-table-{}'.format(score), 'data'), - State('aeronet-scores-table-{}'.format(score), 'style_table')] - for score in SCORES]), - prevent_initial_call=True - ) - def aeronet_scores_tables_retrieve(n, *args): # *activel_cells, models, stat, network, timescale, selection, *tables): - """ Read scores tables and show data """ - - ctx = dash.callback_context - active_cells = list(args[:len(SCORES)]) - tables = list(args[-len(SCORES)*3:]) - if DEBUG: print("ACTIVES", active_cells) - - if DEBUG: print("###########", args[len(SCORES):-len(SCORES)*3]) - models, stat, network, timescale, selection = args[len(SCORES):-len(SCORES)*3] - - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("BUTTON", button_id) - if button_id not in ['scores-apply'] + ['aeronet-scores-table-{}'.format(score) for score in SCORES]: - raise PreventUpdate - - if not n or network != 'aeronet': - return extend_l([[dash.no_update, dash.no_update, { 'display': 'none' }, dash.no_update, dash.no_update] for score in SCORES]) - - # ORDER is IMPORTANT - areas = ['Europe', 'Mediterranean', 'MiddleEast', 'NAfrica', 'Total'] - - if isinstance(models, str): - models = [models] - - if isinstance(stat, str): - stat = [stat] - - models = ['station'] + models - - if DEBUG: print("@@@@@@@@@@@", models, stat, network, timescale, selection, n, len(tables)) - filedir = OBS[network]['path'] - - stat_idxs = [SCORES.index(st) for st in stat] - - # 5 outputs per score - # 3 inputs (states) per score - ret_tables = [None for _ in range(len(SCORES)*5)] - for table_idx in range(int(len(tables)/3)): - obj_idx = table_idx * 3 - ret_idx = table_idx * 5 - ret_tables[ret_idx] = tables[obj_idx] - ret_tables[ret_idx+1] = tables[obj_idx+1] - ret_tables[ret_idx+2] = tables[obj_idx+2] - curr_columns = tables[obj_idx] - curr_data = tables[obj_idx+1] - if active_cells[table_idx] is not None and \ - active_cells[table_idx]['column_id'] == 'station': - curr_active_cell = active_cells[table_idx] - else: - curr_active_cell = None - if table_idx in stat_idxs: - filename = "{}_{}.h5".format(selection, SCORES[table_idx]) - tab_name = "{}_{}".format(SCORES[table_idx], selection) - filepath = os.path.join(filedir, "h5", filename) - if not os.path.exists(filepath): - if DEBUG: print ("TABLES 0", tables) - #Build a no_data list of values to return - no_data = [] - for i in range(len(SCORES)): - no_data += [[], [], {'display': 'block'}, dash.no_update, None] - # Add no data update for UI output only for 1 output - no_data[0].append({'name':['NO DATA'], 'id':'station'}) - if DEBUG: print ("TABLES 1", tables) - if DEBUG: print ("No Data Table ", no_data) - return no_data - - df = pd.read_hdf(filepath, tab_name) # .replace('_', ' ', regex=True) # .round(decimals=2).fillna('-') - # replace "tables" columns - ret_tables[ret_idx] = [{'name': i in MODELS and - [STATS[SCORES[table_idx]], MODELS[i]['name']] or - [STATS[SCORES[table_idx]], ''], 'id': i} for - i in models] - # replace "tables" data - if curr_active_cell is not None: - if DEBUG: print("ACTIVE", curr_active_cell) - curr_data = tables[obj_idx+1] - if not curr_data: - continue - row_number = curr_active_cell['row'] - # 1st case: - if DEBUG: - print('CURRDATA', curr_data) - print('ROWNUMBER', row_number) - value = curr_data[row_number]['station'] - if value not in areas[:-1]: - raise PreventUpdate - val_idx = df.loc[df['station']==value].index[0] - # check following data - if row_number < len(curr_data)-1: - foll_val = curr_data[row_number+1]['station'] - if foll_val in areas: - foll_idx = df.loc[df['station']==foll_val].index[0] - tables[obj_idx+1] = [table_row for table_row in curr_data if curr_data.index(table_row) < row_number] + df.iloc[val_idx:foll_idx-1][models].to_dict('rows') + [table_row for table_row in curr_data if curr_data.index(table_row) > row_number] - else: - foll_area = areas[areas.index(value)+1] - if DEBUG: - print("'''", curr_data) - print("---", foll_area) - foll_idx = curr_data.index([row for row in curr_data if row['station'] == foll_area][0]) - tables[obj_idx+1] = [table_row for table_row in curr_data if curr_data.index(table_row) <= row_number] + [table_row for table_row in curr_data if curr_data.index(table_row) >= foll_idx] - ret_tables[ret_idx+1] = tables[obj_idx+1] - ret_tables[ret_idx+2] = { 'display': 'block' } - ret_tables[ret_idx+3] = [] - ret_tables[ret_idx+4] = None - else: - # ret_tables[ret_idx+1] = df.loc[df['station'].isin(areas), models].to_dict('records') - ret_tables[ret_idx+1] = df[df['station'].isin(areas)].reindex(columns=models).to_dict('records') - ret_tables[ret_idx+2] = { 'display': 'block' } - ret_tables[ret_idx+3] = dash.no_update - ret_tables[ret_idx+4] = dash.no_update - - else: - ret_tables[ret_idx] = [] - ret_tables[ret_idx+1] = [] - ret_tables[ret_idx+2] = { 'display': 'block' } - ret_tables[ret_idx+3] = dash.no_update - ret_tables[ret_idx+4] = dash.no_update - - - if DEBUG: print('LEN', len(ret_tables)) - if DEBUG: print ("TABLES RET", ret_tables) - return ret_tables - - - @app.callback( - [Output('ts-eval-modis-modal', 'children'), - Output('ts-eval-modis-modal', 'is_open')], - [Input('ts-eval-modis-button', 'n_clicks')], - [State('modis-clicked-coords', 'data'), - State('eval-date-picker', 'date'), - State('obs-dropdown', 'value'), - State('obs-mod-dropdown', 'value')], - prevent_initial_call=True - ) - @cache.memoize(timeout=cache_timeout) - def show_eval_modis_timeseries(nclicks, coords, date, obs, model): - """ Retrieve MODIS evaluation timeseries according to station selected """ - from tools import get_timeseries - if coords is None or nclicks == 0: + return dash.no_update, False, dash.no_update, dash.no_update # PreventUpdate + + +@dash.callback( + extend_l([ + [Output('aeronet-scores-table-{}'.format(score), 'columns'), + Output('aeronet-scores-table-{}'.format(score), 'data'), + Output('aeronet-scores-table-{}'.format(score), 'style_table'), + Output('aeronet-scores-table-{}'.format(score), 'selected_cells'), + Output('aeronet-scores-table-{}'.format(score), 'active_cell')] + for score in SCORES]), + [Input('scores-apply', 'n_clicks'), + *[Input('aeronet-scores-table-{}'.format(score), 'active_cell') + for score in SCORES]], + [State('obs-models-dropdown', 'value'), + State('obs-statistics-dropdown', 'value'), + State('obs-network-dropdown', 'value'), + State('obs-timescale-dropdown', 'value'), + State('obs-selection-dropdown', 'value')] + + extend_l([[State('aeronet-scores-table-{}'.format(score), 'columns'), + State('aeronet-scores-table-{}'.format(score), 'data'), + State('aeronet-scores-table-{}'.format(score), 'style_table')] + for score in SCORES]), + prevent_initial_call=True +) +def aeronet_scores_tables_retrieve(n, *args): # *activel_cells, models, stat, network, timescale, selection, *tables): + """ Read scores tables and show data """ + + ctx = dash.callback_context + active_cells = list(args[:len(SCORES)]) + tables = list(args[-len(SCORES)*3:]) + if DEBUG: print("ACTIVES", active_cells) + + if DEBUG: print("###########", args[len(SCORES):-len(SCORES)*3]) + models, stat, network, timescale, selection = args[len(SCORES):-len(SCORES)*3] + + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if DEBUG: print("BUTTON", button_id) + if button_id not in ['scores-apply'] + ['aeronet-scores-table-{}'.format(score) for score in SCORES]: raise PreventUpdate - ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("CTXT", ctxt, type(ctxt)) - if not ctxt or ctxt is None or ctxt != 'ts-eval-modis-button': # or nclicks == 0:P: - raise PreventUpdate + if not n or network != 'aeronet': + return extend_l([[dash.no_update, dash.no_update, { 'display': 'none' }, dash.no_update, dash.no_update] for score in SCORES]) - if DEBUG: print('TRIGGER', ctxt, type(ctxt)) - lat, lon, val = coords - print(coords, date) - models = [obs, model] # [model for model in MODELS] - if DEBUG: print('SHOW MODIS EVAL TS"""""', coords) - figure = get_timeseries(models, date, DEFAULT_VAR, lat, lon) - mb = MODEBAR_LAYOUT_TS - figure.update_layout(mb) - return dbc.ModalBody( - dcc.Graph( - id='timeseries-eval-modal', - figure=figure, - config=MODEBAR_CONFIG_TS - ) - ), True - - # return dash.no_update, False # PreventUpdate - - @app.callback( - [#Output('alert-eval-popup2', 'is_open'), - Output('modis-clicked-coords', 'data'), - Output(dict(tag='modis-map', index='modis'), 'children')], - [Input(dict(tag='modis-map', index='modis'), 'click_lat_lng')], - [State(dict(tag='modis-map', index='modis'), 'children'), - State('eval-date-picker', 'date'), - State('obs-dropdown', 'value'), - State('obs-mod-dropdown', 'value')], - ) - def modis_popup(click_data, mapid, date, obs, model): - from tools import get_single_point - if DEBUG: print("CLICK:", str(click_data)) - if not click_data: - raise PreventUpdate - ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] - ctxt = orjson.loads(dash.callback_context.triggered[0]["prop_id"].split(".")[0]) - if DEBUG: print("CTXT", ctxt, type(ctxt)) - if not ctxt or ctxt is None or ctxt != {'index': 'modis', 'tag': 'modis-map'}: - raise PreventUpdate - - lat, lon = click_data - value = get_single_point(model, date, 0, DEFAULT_VAR, lat, lon) - if DEBUG: print("VALUE", value) - - if not value: - raise PreventUpdate + # ORDER is IMPORTANT + areas = ['Europe', 'Mediterranean', 'MiddleEast', 'NAfrica', 'Total'] - if DEBUG: print("VALUE", value) - try: - valid_dt = dt.strptime(date, '%Y%m%d') + timedelta(hours=12) - except: - valid_dt = dt.strptime(date, '%Y-%m-%d') + timedelta(hours=12) - - marker = dl.Popup( - children=[ - html.Div([ - html.Span(html.P( - '{:.2f}'.format(value*VARS[DEFAULT_VAR]['mul'])), - className='popup-map-value', - ), - html.Span([ - html.B("Lat {:.2f}, Lon {:.2f}".format(lat, lon)), html.Br(), - "DATE {:02d} {} {} {:02d}UTC".format(valid_dt.day, dt.strftime(valid_dt, '%b'), valid_dt.year, valid_dt.hour), - html.Br(), - html.Button("EXPLORE TIMESERIES", - id='ts-eval-modis-button', - n_clicks=0, - className='popup-ts-button' - )], - className='popup-map-body', - )], - ) - ], - id='modis-map-point', - position=[lat, lon], - autoClose=True, - closeOnEscapeKey=True, - closeOnClick=True, - closeButton=True, - className='popup-map-point' - ) + if isinstance(models, str): + models = [models] - return [lat, lon, value], mapid + [marker] + if isinstance(stat, str): + stat = [stat] + models = ['station'] + models - @app.callback( - [#Output('alert-eval-popup', 'is_open'), - Output('stations-clicked-coords', 'data'), - Output(dict(tag='empty-map', index='None'), 'children')], - [Input(dict(tag='empty-map', index='None'), 'click_lat_lng')], - [State(dict(tag='empty-map', index='None'), 'children'), - State('stations-dataframe', 'data')], - prevent_initial_call=True - ) - @cache.memoize(timeout=cache_timeout) - def stations_popup(click_data, mapid, stations): - if not click_data: - raise PreventUpdate + if DEBUG: print("@@@@@@@@@@@", models, stat, network, timescale, selection, n, len(tables)) + filedir = OBS[network]['path'] - if DEBUG: print("CLICK:", str(click_data)) - - ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("CTXT", ctxt, type(ctxt)) - if not ctxt or ctxt is None: - raise PreventUpdate + stat_idxs = [SCORES.index(st) for st in stat] - trigger = orjson.loads(ctxt) - if DEBUG: print('TRIGGER', trigger, type(trigger)) + # 5 outputs per score + # 3 inputs (states) per score + ret_tables = [None for _ in range(len(SCORES)*5)] + for table_idx in range(int(len(tables)/3)): + obj_idx = table_idx * 3 + ret_idx = table_idx * 5 + ret_tables[ret_idx] = tables[obj_idx] + ret_tables[ret_idx+1] = tables[obj_idx+1] + ret_tables[ret_idx+2] = tables[obj_idx+2] + curr_columns = tables[obj_idx] + curr_data = tables[obj_idx+1] + if active_cells[table_idx] is not None and \ + active_cells[table_idx]['column_id'] == 'station': + curr_active_cell = active_cells[table_idx] + else: + curr_active_cell = None + if table_idx in stat_idxs: + filename = "{}_{}.h5".format(selection, SCORES[table_idx]) + tab_name = "{}_{}".format(SCORES[table_idx], selection) + filepath = os.path.join(filedir, "h5", filename) + if not os.path.exists(filepath): + if DEBUG: print ("TABLES 0", tables) + #Build a no_data list of values to return + no_data = [] + for i in range(len(SCORES)): + no_data += [[], [], {'display': 'block'}, dash.no_update, None] + # Add no data update for UI output only for 1 output + no_data[0].append({'name':['NO DATA'], 'id':'station'}) + if DEBUG: print ("TABLES 1", tables) + if DEBUG: print ("No Data Table ", no_data) + return no_data + + df = pd.read_hdf(filepath, tab_name) # .replace('_', ' ', regex=True) # .round(decimals=2).fillna('-') + # replace "tables" columns + ret_tables[ret_idx] = [{'name': i in MODELS and + [STATS[SCORES[table_idx]], MODELS[i]['name']] or + [STATS[SCORES[table_idx]], ''], 'id': i} for + i in models] + # replace "tables" data + if curr_active_cell is not None: + if DEBUG: print("ACTIVE", curr_active_cell) + curr_data = tables[obj_idx+1] + if not curr_data: + continue + row_number = curr_active_cell['row'] + # 1st case: + if DEBUG: + print('CURRDATA', curr_data) + print('ROWNUMBER', row_number) + value = curr_data[row_number]['station'] + if value not in areas[:-1]: + raise PreventUpdate + val_idx = df.loc[df['station']==value].index[0] + # check following data + if row_number < len(curr_data)-1: + foll_val = curr_data[row_number+1]['station'] + if foll_val in areas: + foll_idx = df.loc[df['station']==foll_val].index[0] + tables[obj_idx+1] = [table_row for table_row in curr_data if curr_data.index(table_row) < row_number] + df.iloc[val_idx:foll_idx-1][models].to_dict('rows') + [table_row for table_row in curr_data if curr_data.index(table_row) > row_number] + else: + foll_area = areas[areas.index(value)+1] + if DEBUG: + print("'''", curr_data) + print("---", foll_area) + foll_idx = curr_data.index([row for row in curr_data if row['station'] == foll_area][0]) + tables[obj_idx+1] = [table_row for table_row in curr_data if curr_data.index(table_row) <= row_number] + [table_row for table_row in curr_data if curr_data.index(table_row) >= foll_idx] + ret_tables[ret_idx+1] = tables[obj_idx+1] + ret_tables[ret_idx+2] = { 'display': 'block' } + ret_tables[ret_idx+3] = [] + ret_tables[ret_idx+4] = None + else: + # ret_tables[ret_idx+1] = df.loc[df['station'].isin(areas), models].to_dict('records') + ret_tables[ret_idx+1] = df[df['station'].isin(areas)].reindex(columns=models).to_dict('records') + ret_tables[ret_idx+2] = { 'display': 'block' } + ret_tables[ret_idx+3] = dash.no_update + ret_tables[ret_idx+4] = dash.no_update - if trigger != {'index': 'None', 'tag': 'empty-map'}: - raise PreventUpdate + else: + ret_tables[ret_idx] = [] + ret_tables[ret_idx+1] = [] + ret_tables[ret_idx+2] = { 'display': 'block' } + ret_tables[ret_idx+3] = dash.no_update + ret_tables[ret_idx+4] = dash.no_update + + + if DEBUG: print('LEN', len(ret_tables)) + if DEBUG: print ("TABLES RET", ret_tables) + return ret_tables + + +@dash.callback( + [Output('ts-eval-modis-modal', 'children'), + Output('ts-eval-modis-modal', 'is_open')], + [Input('ts-eval-modis-button', 'n_clicks')], + [State('modis-clicked-coords', 'data'), + State('eval-date-picker', 'date'), + State('obs-dropdown', 'value'), + State('obs-mod-dropdown', 'value')], + prevent_initial_call=True +) +@cache.memoize(timeout=cache_timeout) +def show_eval_modis_timeseries(nclicks, coords, date, obs, model): + """ Retrieve MODIS evaluation timeseries according to station selected """ + from tools import get_timeseries + if coords is None or nclicks == 0: + raise PreventUpdate - df_stations = pd.DataFrame(stations) - lat, lon = click_data - curr_station = df_stations[(df_stations['lon'].round(2) == round(lon, 2)) & \ - (df_stations['lat'].round(2) == round(lat, 2))]['stations'].values - - if DEBUG: print("CURR_STATION", curr_station) + ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] + if DEBUG: print("CTXT", ctxt, type(ctxt)) + if not ctxt or ctxt is None or ctxt != 'ts-eval-modis-button': # or nclicks == 0:P: + raise PreventUpdate - if not curr_station: - raise PreventUpdate + if DEBUG: print('TRIGGER', ctxt, type(ctxt)) + lat, lon, val = coords + print(coords, date) + models = [obs, model] # [model for model in MODELS] + if DEBUG: print('SHOW MODIS EVAL TS"""""', coords) + figure = get_timeseries(models, date, DEFAULT_VAR, lat, lon) + mb = MODEBAR_LAYOUT_TS + figure.update_layout(mb) + return dbc.ModalBody( + dcc.Graph( + id='timeseries-eval-modal', + figure=figure, + config=MODEBAR_CONFIG_TS + ) + ), True + + # return dash.no_update, False # PreventUpdate + +@dash.callback( + [#Output('alert-eval-popup2', 'is_open'), + Output('modis-clicked-coords', 'data'), + Output(dict(tag='modis-map', index='modis'), 'children')], + [Input(dict(tag='modis-map', index='modis'), 'click_lat_lng')], + [State(dict(tag='modis-map', index='modis'), 'children'), + State('eval-date-picker', 'date'), + State('obs-dropdown', 'value'), + State('obs-mod-dropdown', 'value')], +) +def modis_popup(click_data, mapid, date, obs, model): + from tools import get_single_point + if DEBUG: print("CLICK:", str(click_data)) + if not click_data: + raise PreventUpdate + ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] + ctxt = orjson.loads(dash.callback_context.triggered[0]["prop_id"].split(".")[0]) + if DEBUG: print("CTXT", ctxt, type(ctxt)) + if not ctxt or ctxt is None or ctxt != {'index': 'modis', 'tag': 'modis-map'}: + raise PreventUpdate - curr_station = curr_station[0] + lat, lon = click_data + value = get_single_point(model, date, 0, DEFAULT_VAR, lat, lon) + if DEBUG: print("VALUE", value) + + if not value: + raise PreventUpdate - marker = dl.Popup( - children=[ - html.Div([ + if DEBUG: print("VALUE", value) + try: + valid_dt = dt.strptime(date, '%Y%m%d') + timedelta(hours=12) + except: + valid_dt = dt.strptime(date, '%Y-%m-%d') + timedelta(hours=12) + + marker = dl.Popup( + children=[ + html.Div([ + html.Span(html.P( + '{:.2f}'.format(value*VARS[DEFAULT_VAR]['mul'])), + className='popup-map-value', + ), html.Span([ html.B("Lat {:.2f}, Lon {:.2f}".format(lat, lon)), html.Br(), - "STATION: ", html.B("{}".format(curr_station)), html.Br(), + "DATE {:02d} {} {} {:02d}UTC".format(valid_dt.day, dt.strftime(valid_dt, '%b'), valid_dt.year, valid_dt.hour), + html.Br(), html.Button("EXPLORE TIMESERIES", - id='ts-eval-button', + id='ts-eval-modis-button', n_clicks=0, className='popup-ts-button' )], - className='popup-map-eval-body', - style={ 'z-index': '10001' } + className='popup-map-body', )], ) - ], - id=dict( - tag='empty-map-point', - index=int(random()*100) - ), - position=[lat, lon], - autoClose=True, - closeOnEscapeKey=True, - closeOnClick=True, - closeButton=True, - className='popup-map-point', - ) + ], + id='modis-map-point', + position=[lat, lon], + autoClose=True, + closeOnEscapeKey=True, + closeOnClick=True, + closeButton=True, + className='popup-map-point' + ) - curr_data = df_stations[(df_stations['lon'].round(2) == round(lon, 2)) & \ - (df_stations['lat'].round(2) == round(lat, 2))].to_dict() + return [lat, lon, value], mapid + [marker] + + +@dash.callback( + [#Output('alert-eval-popup', 'is_open'), + Output('stations-clicked-coords', 'data'), + Output(dict(tag='empty-map', index='None'), 'children')], + [Input(dict(tag='empty-map', index='None'), 'click_lat_lng')], + [State(dict(tag='empty-map', index='None'), 'children'), + State('stations-dataframe', 'data')], + prevent_initial_call=True +) +@cache.memoize(timeout=cache_timeout) +def stations_popup(click_data, mapid, stations): + if not click_data: + raise PreventUpdate - print("MARKER", marker.to_plotly_json()) - last = mapid[-1] - if last['type'] == 'Popup': - mapid[-1] = marker.to_plotly_json() - else: - mapid.append(marker.to_plotly_json()) - print("LAST", type(mapid), type(last), last) - for pos, log in enumerate(mapid): - if log is not None: - # mapid[pos]['id']['random'] = - if DEBUG: - print("********", type(log), log.keys()) - # print(log['type']) - return curr_data, mapid - - - @app.callback( - [Output('ts-eval-modal', 'children'), - Output('ts-eval-modal', 'is_open')], - [Input('ts-eval-button', 'n_clicks')], - [State('stations-clicked-coords', 'data'), - State('eval-date-picker', 'start_date'), - State('eval-date-picker', 'end_date'), - State('obs-dropdown', 'value'), - State('obs-mod-dropdown', 'value')], - prevent_initial_call=True - ) - @cache.memoize(timeout=cache_timeout) - def show_eval_aeronet_timeseries(nclicks, cdata, start_date, end_date, obs, model): - """ Retrieve AERONET evaluation timeseries according to station selected """ - from tools import get_eval_timeseries - ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("CTXT", ctxt, type(ctxt)) - if not ctxt or ctxt is None: - raise PreventUpdate + if DEBUG: print("CLICK:", str(click_data)) - if ctxt != 'ts-eval-button' or nclicks == 0: - raise PreventUpdate + ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] + if DEBUG: print("CTXT", ctxt, type(ctxt)) + if not ctxt or ctxt is None: + raise PreventUpdate - if not cdata: - raise PreventUpdate + trigger = orjson.loads(ctxt) + if DEBUG: print('TRIGGER', trigger, type(trigger)) - print(start_date, end_date, obs, model, cdata) - if DEBUG: print('EVAL AERONET CLICKDATA', cdata) - cdata = pd.DataFrame(cdata) - idx = int(cdata.index.values[0]) -# lon = cdata.lon.round(2).values[0] -# lat = cdata.lat.round(2).values[0] - stat = cdata.stations.values[0] - if idx != 0: - figure = get_eval_timeseries(obs, start_date, end_date, DEFAULT_VAR, idx, stat, model) - mb = MODEBAR_LAYOUT_TS - figure.update_layout(mb) - if DEBUG: print('SHOW AERONET EVAL TS"""""', obs, idx, stat, start_date, end_date) - return dbc.ModalBody( - dcc.Graph( - id='timeseries-eval-modal', - figure=figure, - config=MODEBAR_CONFIG_TS - ) - ), True + if trigger != {'index': 'None', 'tag': 'empty-map'}: + raise PreventUpdate + + df_stations = pd.DataFrame(stations) + lat, lon = click_data + curr_station = df_stations[(df_stations['lon'].round(2) == round(lon, 2)) & \ + (df_stations['lat'].round(2) == round(lat, 2))]['stations'].values + + if DEBUG: print("CURR_STATION", curr_station) + if not curr_station: raise PreventUpdate + curr_station = curr_station[0] + + marker = dl.Popup( + children=[ + html.Div([ + html.Span([ + html.B("Lat {:.2f}, Lon {:.2f}".format(lat, lon)), html.Br(), + "STATION: ", html.B("{}".format(curr_station)), html.Br(), + html.Button("EXPLORE TIMESERIES", + id='ts-eval-button', + n_clicks=0, + className='popup-ts-button' + )], + className='popup-map-eval-body', + style={ 'z-index': '10001' } + )], + ) + ], + id=dict( + tag='empty-map-point', + index=int(random()*100) + ), + position=[lat, lon], + autoClose=True, + closeOnEscapeKey=True, + closeOnClick=True, + closeButton=True, + className='popup-map-point', + ) - @app.callback( - [Output('stations-dataframe', 'data'), - Output('graph-eval-aeronet', 'children')], - [Input('eval-apply', 'n_clicks')], - [State('eval-date-picker', 'start_date'), - State('eval-date-picker', 'end_date'), - State('obs-dropdown', 'value')], - prevent_initial_call=True) - @cache.memoize(timeout=cache_timeout) - def update_eval_aeronet(n_clicks, sdate, edate, obs): - """ Update AERONET evaluation figure according to all parameters """ - ctx = dash.callback_context - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("BUTTON", button_id) - if button_id != 'eval-apply': - raise PreventUpdate - else: - raise PreventUpdate + curr_data = df_stations[(df_stations['lon'].round(2) == round(lon, 2)) & \ + (df_stations['lat'].round(2) == round(lat, 2))].to_dict() + + print("MARKER", marker.to_plotly_json()) + last = mapid[-1] + if last['type'] == 'Popup': + mapid[-1] = marker.to_plotly_json() + else: + mapid.append(marker.to_plotly_json()) + print("LAST", type(mapid), type(last), last) + for pos, log in enumerate(mapid): + if log is not None: + # mapid[pos]['id']['random'] = + if DEBUG: + print("********", type(log), log.keys()) + # print(log['type']) + return curr_data, mapid + + +@dash.callback( + [Output('ts-eval-modal', 'children'), + Output('ts-eval-modal', 'is_open')], + [Input('ts-eval-button', 'n_clicks')], + [State('stations-clicked-coords', 'data'), + State('eval-date-picker', 'start_date'), + State('eval-date-picker', 'end_date'), + State('obs-dropdown', 'value'), + State('obs-mod-dropdown', 'value')], + prevent_initial_call=True +) +@cache.memoize(timeout=cache_timeout) +def show_eval_aeronet_timeseries(nclicks, cdata, start_date, end_date, obs, model): + """ Retrieve AERONET evaluation timeseries according to station selected """ + from tools import get_eval_timeseries + ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] + if DEBUG: print("CTXT", ctxt, type(ctxt)) + if not ctxt or ctxt is None: + raise PreventUpdate + + if ctxt != 'ts-eval-button' or nclicks == 0: + raise PreventUpdate + + if not cdata: + raise PreventUpdate + + print(start_date, end_date, obs, model, cdata) + if DEBUG: print('EVAL AERONET CLICKDATA', cdata) + cdata = pd.DataFrame(cdata) + idx = int(cdata.index.values[0]) +# lon = cdata.lon.round(2).values[0] +# lat = cdata.lat.round(2).values[0] + stat = cdata.stations.values[0] + if idx != 0: + figure = get_eval_timeseries(obs, start_date, end_date, DEFAULT_VAR, idx, stat, model) + mb = MODEBAR_LAYOUT_TS + figure.update_layout(mb) + if DEBUG: print('SHOW AERONET EVAL TS"""""', obs, idx, stat, start_date, end_date) + return dbc.ModalBody( + dcc.Graph( + id='timeseries-eval-modal', + figure=figure, + config=MODEBAR_CONFIG_TS + ) + ), True - if sdate is None or edate is None or obs != 'aeronet': + raise PreventUpdate + + +@dash.callback( + [Output('stations-dataframe', 'data'), + Output('graph-eval-aeronet', 'children')], + [Input('eval-apply', 'n_clicks')], + [State('eval-date-picker', 'start_date'), + State('eval-date-picker', 'end_date'), + State('obs-dropdown', 'value')], + prevent_initial_call=True) +@cache.memoize(timeout=cache_timeout) +def update_eval_aeronet(n_clicks, sdate, edate, obs): + """ Update AERONET evaluation figure according to all parameters """ + ctx = dash.callback_context + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if DEBUG: print("BUTTON", button_id) + if button_id != 'eval-apply': raise PreventUpdate + else: + raise PreventUpdate - from tools import get_figure - from tools import get_obs1d - if DEBUG: print('SERVER: calling figure from EVAL picker callback') - if DEBUG: print('SERVER: SDATE', str(sdate)) + if sdate is None or edate is None or obs != 'aeronet': + raise PreventUpdate - sdate = sdate.split()[0] + from tools import get_figure + from tools import get_obs1d + if DEBUG: print('SERVER: calling figure from EVAL picker callback') + if DEBUG: print('SERVER: SDATE', str(sdate)) + + sdate = sdate.split()[0] + try: + sdate = dt.strptime( + sdate, "%Y-%m-%d").strftime("%Y%m%d") + except: + sdate = end_date + pass + if DEBUG: print('SERVER: callback start_date {}'.format(sdate)) + + if edate is not None: + edate = edate.split()[0] try: - sdate = dt.strptime( - sdate, "%Y-%m-%d").strftime("%Y%m%d") + edate = dt.strptime( + edate, "%Y-%m-%d").strftime("%Y%m%d") except: - sdate = end_date pass - if DEBUG: print('SERVER: callback start_date {}'.format(sdate)) - - if edate is not None: - edate = edate.split()[0] - try: - edate = dt.strptime( - edate, "%Y-%m-%d").strftime("%Y%m%d") - except: - pass - if DEBUG: print('SERVER: callback end_date {}'.format(edate)) - else: - edate = end_date - - stations, points_layer = get_obs1d(sdate, edate, obs, DEFAULT_VAR) - fig = get_figure(model=None, var=DEFAULT_VAR, layer=points_layer) - # if DEBUG: print("---", fig) - return stations.to_dict(), fig - - - @app.callback( - [Output('graph-eval-modis-obs', 'children'), - Output('graph-eval-modis-mod', 'children')], - [Input('eval-apply', 'n_clicks')], - [State('eval-date-picker', 'date'), - State('obs-mod-dropdown', 'value'), - State('obs-dropdown', 'value'), - State('graph-eval-modis-mod', 'children')], - prevent_initial_call=True) - @cache.memoize(timeout=cache_timeout) - def update_eval_modis(n_clicks, date, mod, obs, mod_div): - """ Update MODIS evaluation figure according to all parameters """ - ctx = dash.callback_context - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("BUTTON", button_id) - if button_id != 'eval-apply': - raise PreventUpdate - else: + if DEBUG: print('SERVER: callback end_date {}'.format(edate)) + else: + edate = end_date + + stations, points_layer = get_obs1d(sdate, edate, obs, DEFAULT_VAR) + fig = get_figure(model=None, var=DEFAULT_VAR, layer=points_layer) + # if DEBUG: print("---", fig) + return stations.to_dict(), fig + + +@dash.callback( + [Output('graph-eval-modis-obs', 'children'), + Output('graph-eval-modis-mod', 'children')], + [Input('eval-apply', 'n_clicks')], + [State('eval-date-picker', 'date'), + State('obs-mod-dropdown', 'value'), + State('obs-dropdown', 'value'), + State('graph-eval-modis-mod', 'children')], + prevent_initial_call=True) +@cache.memoize(timeout=cache_timeout) +def update_eval_modis(n_clicks, date, mod, obs, mod_div): + """ Update MODIS evaluation figure according to all parameters """ + ctx = dash.callback_context + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if DEBUG: print("BUTTON", button_id) + if button_id != 'eval-apply': raise PreventUpdate + else: + raise PreventUpdate - if date is None or mod is None or obs != 'modis': - raise PreventUpdate + if date is None or mod is None or obs != 'modis': + raise PreventUpdate - from tools import get_figure - if DEBUG: print('SERVER: calling figure from EVAL picker callback') - if DEBUG: print(mod_div) - mod_center = mod_div['props']['center'] - mod_zoom = mod_div['props']['zoom'] - - if date is not None: - date = date.split()[0] - try: - date = dt.strptime( - date, "%Y-%m-%d").strftime("%Y%m%d") - except: - pass - if DEBUG: print('SERVER: callback date {}'.format(date)) - else: - date = end_date + from tools import get_figure + if DEBUG: print('SERVER: calling figure from EVAL picker callback') + if DEBUG: print(mod_div) + mod_center = mod_div['props']['center'] + mod_zoom = mod_div['props']['zoom'] - if DEBUG: print("ZOOM", mod_zoom, "CENTER", mod_center) - if MODELS[mod]['start'] == 12: - tstep = 4 - else: - tstep = 0 - fig_mod = get_figure(model=mod, var=DEFAULT_VAR, selected_date=date, tstep=tstep, hour=12, center=mod_center, zoom=mod_zoom) - fig_obs = get_figure(model=obs, var=DEFAULT_VAR, selected_date=date, tstep=0, center=mod_center, zoom=mod_zoom) - - if DEBUG: print("MODIS", fig_obs) - return fig_obs, fig_mod - - - @app.callback( - [Output('eval-date', 'children'), - Output('eval-graph', 'children'), - Output('obs-dropdown', 'value'), - Output('obs-mod-dropdown-span', 'style')], - [Input('obs-dropdown', 'value')], - prevent_initial_call=True) - @cache.memoize(timeout=cache_timeout) - def update_eval(obs): - """ Update evaluation figure according to all parameters """ - from tools import get_figure - # from tools import get_obs1d - if DEBUG: print('SERVER: calling figure from EVAL picker callback') - # if DEBUG: print('SERVER: interval ' + str(n)) - - if obs == 'aeronet': - - eval_date = [ - html.Label("Date range"), - dcc.DatePickerRange( - id='eval-date-picker', - min_date_allowed=dt.strptime(start_date, "%Y%m%d"), - max_date_allowed=dt.strptime(end_date, "%Y%m%d"), - initial_visible_month=dt.strptime(end_date, "%Y%m%d"), - display_format='DD MMM YYYY', - # end_date=end_date, - updatemode='bothdates', - )] - - eval_graph = html.Div( - get_figure(), - id='graph-eval-aeronet', - ) + if date is not None: + date = date.split()[0] + try: + date = dt.strptime( + date, "%Y-%m-%d").strftime("%Y%m%d") + except: + pass + if DEBUG: print('SERVER: callback date {}'.format(date)) + else: + date = end_date + + if DEBUG: print("ZOOM", mod_zoom, "CENTER", mod_center) + if MODELS[mod]['start'] == 12: + tstep = 4 + else: + tstep = 0 + fig_mod = get_figure(model=mod, var=DEFAULT_VAR, selected_date=date, tstep=tstep, hour=12, center=mod_center, zoom=mod_zoom) + fig_obs = get_figure(model=obs, var=DEFAULT_VAR, selected_date=date, tstep=0, center=mod_center, zoom=mod_zoom) + + if DEBUG: print("MODIS", fig_obs) + return fig_obs, fig_mod + + +@dash.callback( + [Output('eval-date', 'children'), + Output('eval-graph', 'children'), + Output('obs-dropdown', 'value'), + Output('obs-mod-dropdown-span', 'style')], + [Input('obs-dropdown', 'value')], + prevent_initial_call=True) +@cache.memoize(timeout=cache_timeout) +def update_eval(obs): + """ Update evaluation figure according to all parameters """ + from tools import get_figure + # from tools import get_obs1d + if DEBUG: print('SERVER: calling figure from EVAL picker callback') + # if DEBUG: print('SERVER: interval ' + str(n)) + + if obs == 'aeronet': + + eval_date = [ + html.Label("Date range"), + dcc.DatePickerRange( + id='eval-date-picker', + min_date_allowed=dt.strptime(start_date, "%Y%m%d"), + max_date_allowed=dt.strptime(end_date, "%Y%m%d"), + initial_visible_month=dt.strptime(end_date, "%Y%m%d"), + display_format='DD MMM YYYY', + # end_date=end_date, + updatemode='bothdates', + )] + + eval_graph = html.Div( + get_figure(), + id='graph-eval-aeronet', + ) # eval_graph = [dbc.Spinner( # get_graph( # gid='graph-eval-aeronet', @@ -773,58 +775,58 @@ def register_callbacks(app, cache, cache_timeout): # )), # ] - style = { 'display': 'none' } + style = { 'display': 'none' } - elif obs == 'modis': + elif obs == 'modis': - eval_date = [ - html.Label("Date"), - dcc.DatePickerSingle( - id='eval-date-picker', - min_date_allowed=dt.strptime(start_date, "%Y%m%d"), - max_date_allowed=dt.strptime(end_date, "%Y%m%d"), - initial_visible_month=dt.strptime(end_date, "%Y%m%d"), - display_format='DD MMM YYYY', - # date=end_date, - # with_portal=True, - )] + eval_date = [ + html.Label("Date"), + dcc.DatePickerSingle( + id='eval-date-picker', + min_date_allowed=dt.strptime(start_date, "%Y%m%d"), + max_date_allowed=dt.strptime(end_date, "%Y%m%d"), + initial_visible_month=dt.strptime(end_date, "%Y%m%d"), + display_format='DD MMM YYYY', + # date=end_date, + # with_portal=True, + )] # fig_mod = get_figure(model='median', var=DEFAULT_VAR, # selected_date=end_date, tstep=4) # fig_obs = get_figure(model=obs, var=DEFAULT_VAR, # selected_date=end_date, tstep=0, zoom=fig_mod.zoom, center=fig_mod.center) - fig_mod = get_figure() - fig_obs = get_figure() + fig_mod = get_figure() + fig_obs = get_figure() - graph_obs = html.Div([ - html.Div( - fig_obs, - id='graph-eval-modis-obs', - ), - html.Div(DISCLAIMER_OBS, - className='disclaimer') - ], - ) - graph_mod = html.Div([ - html.Div( - fig_mod, - id='graph-eval-modis-mod', - ), - html.Div(DISCLAIMER_NO_FORECAST, - className='disclaimer') - ], - ) - eval_graph = [dbc.Row([ - dbc.Col(graph_obs, width=6), - dbc.Col(graph_mod, width=6) - ], - align='start', - no_gutters=True - )] + graph_obs = html.Div([ + html.Div( + fig_obs, + id='graph-eval-modis-obs', + ), + html.Div(DISCLAIMER_OBS, + className='disclaimer') + ], + ) + graph_mod = html.Div([ + html.Div( + fig_mod, + id='graph-eval-modis-mod', + ), + html.Div(DISCLAIMER_NO_FORECAST, + className='disclaimer') + ], + ) + eval_graph = [dbc.Row([ + dbc.Col(graph_obs, width=6), + dbc.Col(graph_mod, width=6) + ], + align='start', + no_gutters=True + )] - style = { 'display': 'table-cell' } + style = { 'display': 'table-cell' } - else: - raise PreventUpdate + else: + raise PreventUpdate - return eval_date, eval_graph, obs, style + return eval_date, eval_graph, obs, style diff --git a/tabs/forecast.py b/tabs/forecast.py index 0ac932f7de6386b273fc17a813193f7f74b2b6bd..8baca6e2c48b9f57385ab91c7cc3287b12e27cee 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -125,47 +125,48 @@ layout_layers = html.Div([ ), )]) -time_slider = html.Div([ - html.Span( - dcc.DatePickerSingle( - id='model-date-picker', - min_date_allowed=dt.strptime(start_date, "%Y%m%d"), - max_date_allowed=dt.strptime(end_date, "%Y%m%d"), - initial_visible_month=dt.strptime(end_date, "%Y%m%d"), - display_format='DD MMM YYYY', - date=end_date, - with_portal=True, +def time_slider(end_date=end_date): + return html.Div([ + html.Span( + dcc.DatePickerSingle( + id='model-date-picker', + min_date_allowed=dt.strptime(start_date, "%Y%m%d"), + max_date_allowed=dt.strptime(end_date, "%Y%m%d"), + initial_visible_month=dt.strptime(end_date, "%Y%m%d"), + display_format='DD MMM YYYY', + date=end_date, + with_portal=True, + ), + className="timesliderline", ), - className="timesliderline", - ), - html.Span( - children=[ - html.Button(title='Play', - id='btn-play', n_clicks=0, - className='fa fa-play text-center'), - html.Button(title='Stop', - id='btn-stop', n_clicks=0, - className='fa fa-pause text-center')], - className="timesliderline anim-buttons", - ), - html.Span( - dcc.Slider( - id='slider-graph', - min=0, max=72, step=3, value=0, - marks={ - tstep: '{:d}'.format(tstep) - # if tstep%2 == 0 else '' - for tstep in range(0, 75, 3) - }, - # updatemode='drag', + html.Span( + children=[ + html.Button(title='Play', + id='btn-play', n_clicks=0, + className='fa fa-play text-center'), + html.Button(title='Stop', + id='btn-stop', n_clicks=0, + className='fa fa-pause text-center')], + className="timesliderline anim-buttons", ), - className="timesliderline", - ), - html.Div(DISCLAIMER_MODELS, - className='disclaimer'), - ], - className="timeslider" -) + html.Span( + dcc.Slider( + id='slider-graph', + min=0, max=72, step=3, value=0, + marks={ + tstep: '{:d}'.format(tstep) + # if tstep%2 == 0 else '' + for tstep in range(0, 75, 3) + }, + # updatemode='drag', + ), + className="timesliderline", + ), + html.Div(DISCLAIMER_MODELS, + className='disclaimer'), + ], + className="timeslider" + ) prob_time_slider = html.Div([ @@ -229,8 +230,7 @@ was_time_slider = html.Div([ className="timeslider" ) - -def tab_forecast(window='models'): +def tab_forecast(window='models', end_date=end_date): models_children = [ html.Div( id=dict( @@ -284,7 +284,7 @@ def tab_forecast(window='models'): disabled=True )), html.Div([ - time_slider, + time_slider(end_date), layout_view, # layout_layers, ], @@ -354,7 +354,21 @@ def tab_forecast(window='models'): ) -def sidebar_forecast(variables, default_var, models, default_model): +def expand_dropdown(window): + """ Build a dictionary to return appropriate expanded sidebar dropdown""" + + expand_dropdown = { + 'models':False, + 'prob':False, + 'was':False + } + expand_dropdown[window]=True + return expand_dropdown + +def sidebar_forecast(variables, default_var, models, default_model, window='models', country='burkinafaso'): + #get which sidebar dropdown should be expanded for complex url search + dropdown = expand_dropdown(window) + return [ html.Div([ html.Label("Variable"), @@ -381,14 +395,15 @@ def sidebar_forecast(variables, default_var, models, default_model): )), dbc.Collapse( id='collapse-1', - is_open=True, + is_open=dropdown['models'], children=[ dbc.CardBody([ dbc.Checklist( id='model-dropdown', options=[{'label': models[model]['name'], 'value': model} for model in models], - value=[default_model,], + #value=[default_model,], + value=default_model, className="sidebar-dropdown", ), html.Span([ @@ -410,6 +425,7 @@ def sidebar_forecast(variables, default_var, models, default_model): )), dbc.Collapse( id='collapse-2', + is_open=dropdown['prob'], children=[ dbc.CardBody([ dcc.RadioItems( @@ -437,13 +453,14 @@ def sidebar_forecast(variables, default_var, models, default_model): )), dbc.Collapse( id='collapse-3', + is_open=dropdown['was'], children=[ dbc.CardBody([ dcc.RadioItems( id='was-dropdown', options=[{'label': WAS[was]['name'], 'value': was} for was in WAS], - value='burkinafaso', + value=country, className="sidebar-dropdown" ), html.Div( diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index 78d5273f980fc42e93dbcf97523e8c761ca08865..690d52b5acd5cb5ebf5921f3381345861e65ab76 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -46,109 +46,108 @@ from random import random start_date = DATES['start_date'] end_date = DATES['end_date'] or (dt.now() - timedelta(days=1)).strftime("%Y%m%d") - -def register_callbacks(app, cache, cache_timeout): - """ Registering callbacks """ - - - @app.callback( - [Output('collapse-1', 'is_open'), - Output('collapse-2', 'is_open'), - Output('collapse-3', 'is_open'), - Output('group-2-toggle', 'disabled'), - Output('group-3-toggle', 'disabled'), - Output('variable-dropdown-forecast','value')], - [Input('group-1-toggle', 'n_clicks'), - Input('group-2-toggle', 'n_clicks'), - Input('group-3-toggle', 'n_clicks'), - Input('variable-dropdown-forecast', 'value')], - [State('collapse-1', 'is_open'), - State('collapse-2', 'is_open'), - State('collapse-3', 'is_open'),], - prevent_initial_call=True - ) - def render_forecast_tab(modbutton, probbutton, wasbutton, var, modopen, probopen, wasopen): - """ Function rendering requested tab """ - ctx = dash.callback_context - - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - - if button_id == "group-1-toggle" and modbutton: - if modopen is True: - return not modopen, False, False, dash.no_update, dash.no_update, dash.no_update +from data_handler import cache, cache_timeout +#def register_callbacks(app, cache, cache_timeout): +# """ Registering callbacks """ + +@dash.callback( + [Output('collapse-1', 'is_open'), + Output('collapse-2', 'is_open'), + Output('collapse-3', 'is_open'), + Output('group-2-toggle', 'disabled'), + Output('group-3-toggle', 'disabled'), + Output('variable-dropdown-forecast','value')], + [Input('group-1-toggle', 'n_clicks'), + Input('group-2-toggle', 'n_clicks'), + Input('group-3-toggle', 'n_clicks'), + Input('variable-dropdown-forecast', 'value')], + [State('collapse-1', 'is_open'), + State('collapse-2', 'is_open'), + State('collapse-3', 'is_open'),], + prevent_initial_call=True +) +def render_forecast_tab(modbutton, probbutton, wasbutton, var, modopen, probopen, wasopen): + """ Function rendering requested tab """ + ctx = dash.callback_context + + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + + if button_id == "group-1-toggle" and modbutton: + if modopen is True: return not modopen, False, False, dash.no_update, dash.no_update, dash.no_update - elif button_id == "group-2-toggle" and probbutton: - if probopen is True: - return False, not probopen, False, dash.no_update, dash.no_update, dash.no_update + return not modopen, False, False, dash.no_update, dash.no_update, dash.no_update + elif button_id == "group-2-toggle" and probbutton: + if probopen is True: return False, not probopen, False, dash.no_update, dash.no_update, dash.no_update - elif button_id == "group-3-toggle" and wasbutton: - if wasopen is True: - return False, False, not wasopen, dash.no_update, dash.no_update, 'SCONC_DUST' + return False, not probopen, False, dash.no_update, dash.no_update, dash.no_update + elif button_id == "group-3-toggle" and wasbutton: + if wasopen is True: return False, False, not wasopen, dash.no_update, dash.no_update, 'SCONC_DUST' + return False, False, not wasopen, dash.no_update, dash.no_update, 'SCONC_DUST' - if var == 'SCONC_DUST': - # raise PreventUpdate - return modopen, probopen, wasopen, False, False, dash.no_update - elif var == 'OD550_DUST': - # only models and prob can be opened - if wasopen: - return True, False, False, False, True, dash.no_update - return modopen, probopen, wasopen, False, True, dash.no_update - else: - if modopen: - return True, False, False, True, True, dash.no_update + if var == 'SCONC_DUST': + # raise PreventUpdate + return modopen, probopen, wasopen, False, False, dash.no_update + elif var == 'OD550_DUST': + # only models and prob can be opened + if wasopen: + return True, False, False, False, True, dash.no_update + return modopen, probopen, wasopen, False, True, dash.no_update + else: + if modopen: return True, False, False, True, True, dash.no_update - - raise PreventUpdate - - @app.callback( - [Output('prob-dropdown', 'options'), - Output('prob-dropdown', 'value')], - [Input('variable-dropdown-forecast', 'value')], - prevent_initial_call=False + return True, False, False, True, True, dash.no_update + + raise PreventUpdate + +@dash.callback( + [Output('prob-dropdown', 'options'), + Output('prob-dropdown', 'value')], + [Input('variable-dropdown-forecast', 'value')], + prevent_initial_call=False +) +def update_prob_dropdown(var): + """ Update Prob maps dropdown """ + if var in ['OD550_DUST','SCONC_DUST']: + opt_list = PROB[var]['prob_thresh'] + units = PROB[var]['units'] + return [ + {'label': '> {} {}'.format(prob, units), + 'value': 'prob_{}'.format(prob)} + for prob in opt_list], 'prob_{}'.format(opt_list[0]) + + raise PreventUpdate + + +@dash.callback( + [ # Output('alert-models-auto', 'is_open'), + Output('model-dropdown', 'options'), + Output('model-dropdown', 'value'), + Output('btn-anim-download', 'style')], + [Input('variable-dropdown-forecast', 'value')], + [Input('model-dropdown', 'value'),], + prevent_initial_call=True ) - def update_prob_dropdown(var): - """ Update Prob maps dropdown """ - if var in ['OD550_DUST','SCONC_DUST']: - opt_list = PROB[var]['prob_thresh'] - units = PROB[var]['units'] - return [ - {'label': '> {} {}'.format(prob, units), - 'value': 'prob_{}'.format(prob)} - for prob in opt_list], 'prob_{}'.format(opt_list[0]) - - raise PreventUpdate - - - @app.callback( - [ # Output('alert-models-auto', 'is_open'), - Output('model-dropdown', 'options'), - Output('model-dropdown', 'value'), - Output('btn-anim-download', 'style')], - [Input('variable-dropdown-forecast', 'value')], - [Input('model-dropdown', 'value'),], - prevent_initial_call=True - ) - def update_models_dropdown(variable, checked): - - btn_style = { 'display' : 'block' } - models = VARS[variable]['models'] - if models == 'all': - models = list(MODELS.keys()) - else: - models = eval(models) - - options = [{ - 'label': MODELS[model]['name'], - 'value': model, - 'disabled': model not in models, - } for model in MODELS] - - checked = [c for c in models if c in checked or len(models)==1] - if len(checked) > 1: - btn_style = { 'display' : 'none' } - if DEBUG: print('MODELS', models, 'OPTS', type(options), options) +def update_models_dropdown(variable, checked): + + btn_style = { 'display' : 'block' } + models = VARS[variable]['models'] + if models == 'all': + models = list(MODELS.keys()) + else: + models = eval(models) + + options = [{ + 'label': MODELS[model]['name'], + 'value': model, + 'disabled': model not in models, + } for model in MODELS] + + checked = [c for c in models if c in checked or len(models)==1] + if len(checked) > 1: + btn_style = { 'display' : 'none' } + if DEBUG: print('MODELS', models, 'OPTS', type(options), options) # if len(checked) >= 16: # options = [{ # 'label': MODELS[model]['name'], @@ -158,75 +157,74 @@ def register_callbacks(app, cache, cache_timeout): # # return True, options, checked - return options, checked, btn_style - - @app.callback( - Output('caret1', 'style'), - [Input('group-1-toggle', 'n_clicks')] - ) - def rotate_models_caret(n_clicks): - rotate_caret = { - 'top':'.05rem', - 'transform': 'rotate(180deg)', - '-ms-transform': 'rotate(180deg)', - '-webkit-transform': 'rotate(180deg)' - } - if n_clicks %2==1: - return rotate_caret - - @app.callback( - Output('caret2', 'style'), - [Input('group-2-toggle', 'n_clicks')], - ) - def rotate_prob_caret(n_clicks): - rotate_caret = { - 'transform': 'rotate(0deg)', - '-ms-transform': 'rotate(0deg)', - '-webkit-transform': 'rotate(0deg)' - } - if n_clicks %2==1: - return rotate_caret - - @app.callback( - Output('caret3', 'style'), - [Input('group-3-toggle', 'n_clicks')] - ) - def rotate_was_caret(n_clicks): - rotate_caret = { - 'transform': 'rotate(0deg)', - '-ms-transform': 'rotate(0deg)', - '-webkit-transform': 'rotate(0deg)' - } - if n_clicks %2==1: - return rotate_caret - - - @app.callback( - [Output('info-collapse', 'is_open'), - Output('download-collapse', 'is_open')], - [Input('info-button', 'n_clicks'), - Input('download-button', 'n_clicks')], - [State('info-collapse', 'is_open'), - State('download-collapse', 'is_open')], - prevent_initial_call=True - ) - def sidebar_bottom(n_info, n_download, open_info, open_download): - ctx = dash.callback_context - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - - if button_id == 'info-button': - if DEBUG: print('clicked INFO', not open_info, False) - return not open_info, False - elif button_id == 'download-button': - if DEBUG: print('clicked DOWN', False, not open_download) - return False, not open_download - - if DEBUG: print('clicked NONE', False, False) - raise PreventUdate - - -# @app.callback( + return options, checked, btn_style + +@dash.callback( + Output('caret1', 'style'), + [Input('collapse-1', 'is_open')], +) +def rotate_models_caret(collapse_open): + rotate_caret = { + 'top':'.05rem', + 'transform': 'rotate(180deg)', + '-ms-transform': 'rotate(180deg)', + '-webkit-transform': 'rotate(180deg)' + } + if not collapse_open: + return rotate_caret + +@dash.callback( + Output('caret2', 'style'), + [Input('collapse-2', 'is_open')], +) +def rotate_prob_caret(collapse_open): + rotate_caret = { + 'transform': 'rotate(0deg)', + '-ms-transform': 'rotate(0deg)', + '-webkit-transform': 'rotate(0deg)' + } + if collapse_open: + return rotate_caret + +@dash.callback( + Output('caret3', 'style'), + [Input('collapse-3', 'is_open')], +) +def rotate_was_caret(collapse_open): + rotate_caret = { + 'transform': 'rotate(0deg)', + '-ms-transform': 'rotate(0deg)', + '-webkit-transform': 'rotate(0deg)' + } + if collapse_open: + return rotate_caret + +@dash.callback( + [Output('info-collapse', 'is_open'), + Output('download-collapse', 'is_open')], + [Input('info-button', 'n_clicks'), + Input('download-button', 'n_clicks')], + [State('info-collapse', 'is_open'), + State('download-collapse', 'is_open')], + prevent_initial_call=True +) +def sidebar_bottom(n_info, n_download, open_info, open_download): + ctx = dash.callback_context + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + + if button_id == 'info-button': + if DEBUG: print('clicked INFO', not open_info, False) + return not open_info, False + elif button_id == 'download-button': + if DEBUG: print('clicked DOWN', False, not open_download) + return False, not open_download + + if DEBUG: print('clicked NONE', False, False) + raise PreventUdate + + +# @dash.callback( # [Output('login-modal', 'is_open'), # Output('alert-login-error', 'is_open'), # Output('alert-login-wrong', 'is_open'), @@ -303,40 +301,40 @@ def register_callbacks(app, cache, cache_timeout): # # raise PreventUpdate - @app.callback( - Output('btn-anim-download', 'href'), +@dash.callback( + Output('btn-anim-download', 'href'), # Output('btn-all-frame-download', 'href'), # Output('btn-all-anim-download', 'href')], - [#Input('btn-anim-download', 'n_clicks'), - Input('model-dropdown', 'value'), - Input('variable-dropdown-forecast', 'value'), - Input('model-date-picker', 'date'), - Input('slider-graph', 'value')], - prevent_initial_call=False, - ) - def download_anim_link(models, variable, date, tstep): - """ Download PNG frame """ - # from tools import get_figure - from tools import download_image_link + [#Input('btn-anim-download', 'n_clicks'), + Input('model-dropdown', 'value'), + Input('variable-dropdown-forecast', 'value'), + Input('model-date-picker', 'date'), + Input('slider-graph', 'value')], + prevent_initial_call=False, +) +def download_anim_link(models, variable, date, tstep): + """ Download PNG frame """ + # from tools import get_figure + from tools import download_image_link # ctx = dash.callback_context # # if ctx.triggered: # button_id = ctx.triggered[0]["prop_id"].split(".")[0] # if button_id == 'btn-anim-download': - if DEBUG: print('GIF', models, variable, date) - try: - curdate = dt.strptime(date, '%Y-%m-%d').strftime('%Y%m%d') - except: - curdate = date - anim = download_image_link(models, variable, curdate, anim=True) - #all_frame = download_image_link(['all',], variable, curdate, tstep=int(tstep/3), anim=False) - #all_anim = download_image_link(['all',], variable, curdate, anim=True) - if DEBUG: print('DOWNLOAD LINK', anim) - return anim #, all_frame, all_anim - - -# @app.callback( + if DEBUG: print('GIF', models, variable, date) + try: + curdate = dt.strptime(date, '%Y-%m-%d').strftime('%Y%m%d') + except: + curdate = date + anim = download_image_link(models, variable, curdate, anim=True) + #all_frame = download_image_link(['all',], variable, curdate, tstep=int(tstep/3), anim=False) + #all_anim = download_image_link(['all',], variable, curdate, anim=True) + if DEBUG: print('DOWNLOAD LINK', anim) + return anim #, all_frame, all_anim + + +# @dash.callback( # Output('anim-download', 'data'), # [Input('btn-anim-download', 'n_clicks')], # [State('model-dropdown', 'value'), @@ -366,66 +364,66 @@ def register_callbacks(app, cache, cache_timeout): # raise PreventUpdate # - @app.callback( - Output('all-frame-download', 'data'), - [Input('btn-all-frame-download', 'n_clicks')], - [State('variable-dropdown-forecast', 'value'), - State('model-date-picker', 'date'), - State('slider-graph', 'value') - ], - prevent_initial_call=True, - ) - def download_all_frame(btn, models, variable, date, tstep): - """ Download PNG frame """ - # from tools import get_figure - from tools import download_image - - ctx = dash.callback_context - - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id == 'btn-all-frame-download': - if DEBUG: print('GIF', btn, models, variable, date) - try: - curdate = dt.strptime(date, '%Y-%m-%d').strftime('%Y%m%d') - except: - curdate = date - data = download_image(models, variable, curdate, tstep=tstep, anim=False) - if DEBUG: print('DATA', type(data), data.keys(), [data[k] for k in data if k != 'content']) - return data - - raise PreventUpdate +@dash.callback( + Output('all-frame-download', 'data'), + [Input('btn-all-frame-download', 'n_clicks')], + [State('variable-dropdown-forecast', 'value'), + State('model-date-picker', 'date'), + State('slider-graph', 'value') + ], + prevent_initial_call=True, +) +def download_all_frame(btn, models, variable, date, tstep): + """ Download PNG frame """ + # from tools import get_figure + from tools import download_image + + ctx = dash.callback_context + + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if button_id == 'btn-all-frame-download': + if DEBUG: print('GIF', btn, models, variable, date) + try: + curdate = dt.strptime(date, '%Y-%m-%d').strftime('%Y%m%d') + except: + curdate = date + data = download_image(models, variable, curdate, tstep=tstep, anim=False) + if DEBUG: print('DATA', type(data), data.keys(), [data[k] for k in data if k != 'content']) + return data + + raise PreventUpdate + +@dash.callback( + Output('all-anim-download', 'data'), + [Input('btn-all-anim-download', 'n_clicks')], + [State('variable-dropdown-forecast', 'value'), + State('model-date-picker', 'date')], + prevent_initial_call=True, +) +def download_all_anim(btn, variable, date): + """ Download PNG frame """ + # from tools import get_figure + from tools import download_image + + ctx = dash.callback_context + + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if button_id == 'btn-all-anim-download': + if DEBUG: print('GIF', btn, variable, date) + try: + curdate = dt.strptime(date, '%Y-%m-%d').strftime('%Y%m%d') + except: + curdate = date + data = download_image(['all'], variable, curdate, anim=True) + if DEBUG: print('DATA', type(data), data.keys(), [data[k] for k in data if k != 'content']) + return data - @app.callback( - Output('all-anim-download', 'data'), - [Input('btn-all-anim-download', 'n_clicks')], - [State('variable-dropdown-forecast', 'value'), - State('model-date-picker', 'date')], - prevent_initial_call=True, - ) - def download_all_anim(btn, variable, date): - """ Download PNG frame """ - # from tools import get_figure - from tools import download_image + raise PreventUpdate - ctx = dash.callback_context - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id == 'btn-all-anim-download': - if DEBUG: print('GIF', btn, variable, date) - try: - curdate = dt.strptime(date, '%Y-%m-%d').strftime('%Y%m%d') - except: - curdate = date - data = download_image(['all'], variable, curdate, anim=True) - if DEBUG: print('DATA', type(data), data.keys(), [data[k] for k in data if k != 'content']) - return data - raise PreventUpdate - - - # app.clientside_callback( # ClientsideFunction( # namespace='clientside', @@ -436,7 +434,7 @@ def register_callbacks(app, cache, cache_timeout): # prevent_initial_call=True, # ) -# @app.callback( +# @dash.callback( # Output('frame-download', 'data'), # [Input('btn-frame-download', 'n_clicks')], # [State('model-dropdown', 'value'), @@ -465,199 +463,202 @@ def register_callbacks(app, cache, cache_timeout): # # return download_image(models, variable, curdate, tstep=int(tstep/FREQ)) - @app.callback( - [Output('was-graph', 'children'), - Output('was-previous', 'data')], - [Input('was-apply', 'n_clicks'), - Input('was-date-picker', 'date'), - Input('was-slider-graph', 'value')], - [State('was-dropdown', 'value'), - State('variable-dropdown-forecast', 'value'), - State('was-previous', 'data'), - State({'tag': 'view-style', 'index': ALL}, 'active'), - State({'tag': 'empty-map', 'index': ALL}, 'zoom'), - State({'tag': 'empty-map', 'index': ALL}, 'center')], - prevent_initial_call=False - ) - @cache.memoize(timeout=cache_timeout) - def update_was_figure(n_clicks, date, day, was, var, previous, view, zoom, center): - """ Update Warning Advisory Systems maps """ - if DEBUG: print('WAS', n_clicks, date, day, was, previous, view, zoom, center) - ctx = dash.callback_context - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id != 'was-apply' and date is None and day is None: - raise PreventUpdate - - from tools import get_was_figure - from tools import get_figure - if not zoom or previous != was: - zoom = WAS[was]['zoom'] - else: - zoom = zoom[0] - if not center or previous != was: - center = WAS[was]['center'] - else: - center = center[0] - - previous = was - - print('WAS', was) - if date is not None: - date = date.split(' ')[0] - try: - date = dt.strptime( - date, "%Y-%m-%d").strftime("%Y%m%d") - except: - pass - if DEBUG: print('SERVER: callback date {}'.format(date)) - else: - date = end_date - - if DEBUG: print("WAS figure " + date, was, day) - if was: - view = list(STYLES.keys())[view.index(True)] - geojson, legend, info = get_was_figure(was, day, selected_date=date) - fig = get_figure(model=None, var=var, layer=[geojson, legend, info], view=view, zoom=zoom, center=center) - return fig, previous - - raise PreventUpdate +@dash.callback( + [Output('was-graph', 'children'), + Output('was-previous', 'data')], + [Input('was-apply', 'n_clicks'), + Input('was-date-picker', 'date'), + Input('was-slider-graph', 'value')], + [State('was-dropdown', 'value'), + State('variable-dropdown-forecast', 'value'), + State('was-previous', 'data'), + State({'tag': 'view-style', 'index': ALL}, 'active'), + State({'tag': 'empty-map', 'index': ALL}, 'zoom'), + State({'tag': 'empty-map', 'index': ALL}, 'center')], + prevent_initial_call=False + ) +@cache.memoize(timeout=cache_timeout) +def update_was_figure(n_clicks, date, day, was, var, previous, view, zoom, center): + """ Update Warning Advisory Systems maps """ + if DEBUG: print('WAS', n_clicks, date, day, was, previous, view, zoom, center) + ctx = dash.callback_context + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if button_id != 'was-apply' and date is None and day is None: + raise PreventUpdate + from tools import get_was_figure + from tools import get_figure + if not zoom or previous != was: + zoom = WAS[was]['zoom'] + else: + zoom = zoom[0] + if not center or previous != was: + center = WAS[was]['center'] + else: + center = center[0] + + previous = was + + print('WAS', was) + if date is not None: + date = date.split(' ')[0] + try: + date = dt.strptime( + date, "%Y-%m-%d").strftime("%Y%m%d") + except: + pass + if DEBUG: print('SERVER: callback date {}'.format(date)) + else: + date = end_date + if DEBUG: print("WAS figure " + date, was, day) + if was: + view = list(STYLES.keys())[view.index(True)] + geojson, legend, info = get_was_figure(was, day, selected_date=date) + fig = get_figure(model=None, var=var, layer=[geojson, legend, info], view=view, zoom=zoom, center=center) + return fig, previous + + raise PreventUpdate + + +@dash.callback( + Output('prob-graph', 'children'), + [Input('prob-apply', 'n_clicks'), + Input('prob-date-picker', 'date'), + Input('prob-slider-graph', 'value')], + [State('prob-dropdown', 'value'), + State('variable-dropdown-forecast', 'value'), + State({'tag': 'view-style', 'index': ALL}, 'active'), + State({'tag': 'empty-map', 'index': ALL}, 'zoom'), + State({'tag': 'empty-map', 'index': ALL}, 'center')], + prevent_initial_call=False +) +@cache.memoize(timeout=cache_timeout) +def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): + """ Update Warning Advisory Systems maps """ + from tools import get_prob_figure + from tools import get_figure + + # if not prob in case user navigates to section via URL + if not prob: + prob = 'prob_0.1' + if not zoom: + zoom = None + else: + zoom = zoom[0] + if not center: + center = None + else: + center = center[0] + if DEBUG: print('PROB', date, day, var, prob, var, view, zoom, center) + ctx = dash.callback_context + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if button_id != 'prob-apply' and var is None and day is None: + raise PreventUpdate - @app.callback( - Output('prob-graph', 'children'), - [Input('prob-apply', 'n_clicks'), - Input('prob-date-picker', 'date'), - Input('prob-slider-graph', 'value')], - [State('prob-dropdown', 'value'), - State('variable-dropdown-forecast', 'value'), - State({'tag': 'view-style', 'index': ALL}, 'active'), - State({'tag': 'empty-map', 'index': ALL}, 'zoom'), - State({'tag': 'empty-map', 'index': ALL}, 'center')], - prevent_initial_call=False - ) - @cache.memoize(timeout=cache_timeout) - def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): - """ Update Warning Advisory Systems maps """ - from tools import get_prob_figure - from tools import get_figure - if not zoom: - zoom = None - else: - zoom = zoom[0] - if not center: - center = None - else: - center = center[0] - if DEBUG: print('PROB', date, day, var, prob, var, view, zoom, center) - ctx = dash.callback_context - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id != 'prob-apply' and var is None and day is None: - raise PreventUpdate + if date is not None: + date = date.split(' ')[0] + try: + date = dt.strptime( + date, "%Y-%m-%d").strftime("%Y%m%d") + except: + pass + if DEBUG: print('SERVER: callback date {}'.format(date)) + else: + date = end_date - if date is not None: - date = date.split(' ')[0] - try: - date = dt.strptime( - date, "%Y-%m-%d").strftime("%Y%m%d") - except: - pass - if DEBUG: print('SERVER: callback date {}'.format(date)) - else: - date = end_date - - if prob: - prob = prob.replace('prob_', '') - view = list(STYLES.keys())[view.index(True)] - geojson, colorbar, info = get_prob_figure(var, prob, day, selected_date=date) - fig = get_figure(model=None, var=var, layer=[geojson, colorbar, info], view=view, zoom=zoom, center=center) - if DEBUG: print("FIG", fig) - return fig + if prob: + prob = prob.replace('prob_', '') + view = list(STYLES.keys())[view.index(True)] + geojson, colorbar, info = get_prob_figure(var, prob, day, selected_date=date) + fig = get_figure(model=None, var=var, layer=[geojson, colorbar, info], view=view, zoom=zoom, center=center) + if DEBUG: print("FIG", fig) + return fig # return html.Div( # fig, # id='prob-graph', # className='graph-with-slider' # ) - raise PreventUpdate - - - @app.callback( - [Output({'tag': 'empty-tile-layer', 'index': ALL}, 'url'), - Output({'tag': 'empty-tile-layer', 'index': ALL}, 'attribution')], - [Input({'tag': 'view-style', 'index': ALL}, 'n_clicks')], - [State({'tag': 'view-style', 'index': ALL}, 'active'), - State({'tag': 'empty-tile-layer', 'index': ALL}, 'url')], - prevent_initial_call=True - ) - @cache.memoize(timeout=cache_timeout) - def update_styles_button(*args): - """ Function updating styles button """ - ctx = dash.callback_context - if ctx.triggered: - button_id = orjson.loads(ctx.triggered[0]["prop_id"].split(".")[0]) - if DEBUG: print("BUTTON ID", str(button_id), type(button_id)) - if button_id['index'] in STYLES: - active = args[-2] - graphs = args[-1] - num_graphs = len(graphs) - # if DEBUG: print("CURRENT ARGS", str(args)) - # if DEBUG: print("NUM GRAPHS", num_graphs) - - res = [False for i in active] - st_idx = list(STYLES.keys()).index(button_id['index']) - if active[st_idx] is False: - res[st_idx] = True - url = [STYLES[button_id['index']]['url'] for x in range(num_graphs)] - attr = [STYLES[button_id['index']]['attribution'] for x in range(num_graphs)] - if DEBUG: - print(res, url, attr) - return url, attr - # return [True if i == button_id['index'] else False for i in active] - - if DEBUG: print('NOTHING TO DO') - raise PreventUpdate - - - @app.callback( - [Output({'tag': 'model-tile-layer', 'index': ALL}, 'url'), - Output({'tag': 'model-tile-layer', 'index': ALL}, 'attribution'), - Output({'tag': 'view-style', 'index': ALL}, 'active')], - [Input({'tag': 'view-style', 'index': ALL}, 'n_clicks')], - [State({'tag': 'view-style', 'index': ALL}, 'active'), - State({'tag': 'model-tile-layer', 'index': ALL}, 'url')], - prevent_initial_call=True - ) - @cache.memoize(timeout=cache_timeout) - def update_models_styles_button(*args): - """ Function updating styles button """ - ctx = dash.callback_context - if ctx.triggered: - button_id = orjson.loads(ctx.triggered[0]["prop_id"].split(".")[0]) - if DEBUG: print("BUTTON ID", str(button_id), type(button_id)) - if button_id['index'] in STYLES: - active = args[-2] - graphs = args[-1] - num_graphs = len(graphs) - # if DEBUG: print("CURRENT ARGS", str(args)) - # if DEBUG: print("NUM GRAPHS", num_graphs) - - res = [False for i in active] - st_idx = list(STYLES.keys()).index(button_id['index']) - if active[st_idx] is False: - res[st_idx] = True - url = [STYLES[button_id['index']]['url'] for x in range(num_graphs)] - attr = [STYLES[button_id['index']]['attribution'] for x in range(num_graphs)] - if DEBUG: - print(res, url, attr) - return url, attr, res - # return [True if i == button_id['index'] else False for i in active] - - if DEBUG: print('NOTHING TO DO') - raise PreventUpdate - - -# @app.callback( + raise PreventUpdate + + +@dash.callback( + [Output({'tag': 'empty-tile-layer', 'index': ALL}, 'url'), + Output({'tag': 'empty-tile-layer', 'index': ALL}, 'attribution')], + [Input({'tag': 'view-style', 'index': ALL}, 'n_clicks')], + [State({'tag': 'view-style', 'index': ALL}, 'active'), + State({'tag': 'empty-tile-layer', 'index': ALL}, 'url')], + prevent_initial_call=True +) +@cache.memoize(timeout=cache_timeout) +def update_styles_button(*args): + """ Function updating styles button """ + ctx = dash.callback_context + if ctx.triggered: + button_id = orjson.loads(ctx.triggered[0]["prop_id"].split(".")[0]) + if DEBUG: print("BUTTON ID", str(button_id), type(button_id)) + if button_id['index'] in STYLES: + active = args[-2] + graphs = args[-1] + num_graphs = len(graphs) + # if DEBUG: print("CURRENT ARGS", str(args)) + # if DEBUG: print("NUM GRAPHS", num_graphs) + + res = [False for i in active] + st_idx = list(STYLES.keys()).index(button_id['index']) + if active[st_idx] is False: + res[st_idx] = True + url = [STYLES[button_id['index']]['url'] for x in range(num_graphs)] + attr = [STYLES[button_id['index']]['attribution'] for x in range(num_graphs)] + if DEBUG: + print(res, url, attr) + return url, attr + # return [True if i == button_id['index'] else False for i in active] + + if DEBUG: print('NOTHING TO DO') + raise PreventUpdate + + +@dash.callback( + [Output({'tag': 'model-tile-layer', 'index': ALL}, 'url'), + Output({'tag': 'model-tile-layer', 'index': ALL}, 'attribution'), + Output({'tag': 'view-style', 'index': ALL}, 'active')], + [Input({'tag': 'view-style', 'index': ALL}, 'n_clicks')], + [State({'tag': 'view-style', 'index': ALL}, 'active'), + State({'tag': 'model-tile-layer', 'index': ALL}, 'url')], + prevent_initial_call=True +) +@cache.memoize(timeout=cache_timeout) +def update_models_styles_button(*args): + """ Function updating styles button """ + ctx = dash.callback_context + if ctx.triggered: + button_id = orjson.loads(ctx.triggered[0]["prop_id"].split(".")[0]) + if DEBUG: print("BUTTON ID", str(button_id), type(button_id)) + if button_id['index'] in STYLES: + active = args[-2] + graphs = args[-1] + num_graphs = len(graphs) + # if DEBUG: print("CURRENT ARGS", str(args)) + # if DEBUG: print("NUM GRAPHS", num_graphs) + + res = [False for i in active] + st_idx = list(STYLES.keys()).index(button_id['index']) + if active[st_idx] is False: + res[st_idx] = True + url = [STYLES[button_id['index']]['url'] for x in range(num_graphs)] + attr = [STYLES[button_id['index']]['attribution'] for x in range(num_graphs)] + if DEBUG: + print(res, url, attr) + return url, attr, res + # return [True if i == button_id['index'] else False for i in active] + + if DEBUG: print('NOTHING TO DO') + raise PreventUpdate + + +# @dash.callback( # [Output({'tag': 'model-tile-layer', 'index': MATCH}, 'url'), # Output({'tag': 'model-tile-layer', 'index': MATCH}, 'attribution')], # [Input('airports', 'n_clicks')] + @@ -728,281 +729,281 @@ def register_callbacks(app, cache, cache_timeout): # raise PreventUpdate - @app.callback( - [Output('model-clicked-coords', 'data'), - Output('current-popups-stored', 'data'), - Output(dict(tag='model-map', index=ALL, n_clicks=ALL), 'children')], - [Input(dict(tag='model-map', index=ALL, n_clicks=ALL), 'click_lat_lng')], - [State(dict(tag='model-map', index=ALL, n_clicks=ALL), 'id'), - State(dict(tag='model-map', index=ALL, n_clicks=ALL), 'children'), - State('model-date-picker', 'date'), - State('slider-graph', 'value'), - State('variable-dropdown-forecast', 'value'), - State('model-clicked-coords', 'data'), - State('current-popups-stored', 'data')], - prevent_initial_call=False - ) - def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups): - from tools import get_single_point - if DEBUG: print("CLICK:", str(click_data)) - if click_data.count(None) == len(click_data): - raise PreventUpdate +@dash.callback( + [Output('model-clicked-coords', 'data'), + Output('current-popups-stored', 'data'), + Output(dict(tag='model-map', index=ALL, n_clicks=ALL), 'children')], + [Input(dict(tag='model-map', index=ALL, n_clicks=ALL), 'click_lat_lng')], + [State(dict(tag='model-map', index=ALL, n_clicks=ALL), 'id'), + State(dict(tag='model-map', index=ALL, n_clicks=ALL), 'children'), + State('model-date-picker', 'date'), + State('slider-graph', 'value'), + State('variable-dropdown-forecast', 'value'), + State('model-clicked-coords', 'data'), + State('current-popups-stored', 'data')], + prevent_initial_call=False +) +def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups): + from tools import get_single_point + if DEBUG: print("CLICK:", str(click_data)) + if click_data.count(None) == len(click_data): + raise PreventUpdate - if DEBUG: print("MAPID:", str(map_ids), type(map_ids)) - # if DEBUG: print("RESLIST:", str(res_list), type(res_list)) + if DEBUG: print("MAPID:", str(map_ids), type(map_ids)) + # if DEBUG: print("RESLIST:", str(res_list), type(res_list)) - ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("CTXT", ctxt, type(ctxt)) - if not ctxt or ctxt is None: - raise PreventUpdate + ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] + if DEBUG: print("CTXT", ctxt, type(ctxt)) + if not ctxt or ctxt is None: + raise PreventUpdate - if popups is None: - popups = {} + if popups is None: + popups = {} - trigger = orjson.loads(ctxt) - if DEBUG: print('TRIGGER', trigger, type(trigger)) + trigger = orjson.loads(ctxt) + if DEBUG: print('TRIGGER', trigger, type(trigger)) - curr_models = [m['index'] for m in map_ids] - res = res_list - if trigger in map_ids: - model = trigger['index'] - mod_idx = map_ids.index(trigger) - click = click_data[mod_idx] - if tstep is None: - tstep = 0 - else: - tstep = int(tstep/FREQ) - - if DEBUG: print("MODEL", model, "CLICK", click, "DATE", date, "STEP", tstep, "MODIDX", mod_idx) - - if click is not None and model is not None: - lat, lon = click - try: - selected_date = dt.strptime(date, '%Y%m%d') - except: - selected_date = dt.strptime(date, '%Y-%m-%d') - - if model in MODELS and MODELS[model]['start'] == 12: - if tstep < 4: - date = (selected_date - timedelta(days=1)).strftime("%Y%m%d") - tstep = 4 + int(tstep) - else: - date = selected_date.strftime("%Y%m%d") - tstep = int(tstep) - 4 - valid_dt = dt.strptime(date, '%Y%m%d') + timedelta(hours=(tstep+4)*FREQ) + curr_models = [m['index'] for m in map_ids] + res = res_list + if trigger in map_ids: + model = trigger['index'] + mod_idx = map_ids.index(trigger) + click = click_data[mod_idx] + if tstep is None: + tstep = 0 + else: + tstep = int(tstep/FREQ) + + if DEBUG: print("MODEL", model, "CLICK", click, "DATE", date, "STEP", tstep, "MODIDX", mod_idx) + + if click is not None and model is not None: + lat, lon = click + try: + selected_date = dt.strptime(date, '%Y%m%d') + except: + selected_date = dt.strptime(date, '%Y-%m-%d') + + if model in MODELS and MODELS[model]['start'] == 12: + if tstep < 4: + date = (selected_date - timedelta(days=1)).strftime("%Y%m%d") + tstep = 4 + int(tstep) else: date = selected_date.strftime("%Y%m%d") - valid_dt = dt.strptime(date, '%Y%m%d') + timedelta(hours=tstep*FREQ) - - if DEBUG: print("MODEL", model, "CLICK", click, "DATE", date, "STEP", tstep) - value = get_single_point(model, date, int(tstep), var, lat, lon) - if DEBUG: print("VALUE:", str(value)) - - marker = dl.Popup( - children=[ - html.Div([ - html.Span(html.P( - '{:.2f}'.format(value*VARS[var]['mul'])), - className='popup-map-value', + tstep = int(tstep) - 4 + valid_dt = dt.strptime(date, '%Y%m%d') + timedelta(hours=(tstep+4)*FREQ) + else: + date = selected_date.strftime("%Y%m%d") + valid_dt = dt.strptime(date, '%Y%m%d') + timedelta(hours=tstep*FREQ) + + if DEBUG: print("MODEL", model, "CLICK", click, "DATE", date, "STEP", tstep) + value = get_single_point(model, date, int(tstep), var, lat, lon) + if DEBUG: print("VALUE:", str(value)) + + marker = dl.Popup( + children=[ + html.Div([ + html.Span(html.P( + '{:.2f}'.format(value*VARS[var]['mul'])), + className='popup-map-value', + ), + html.Span([ + html.B("Lat {:.2f}, Lon {:.2f}".format(lat, lon)), html.Br(), + "DATE {:02d} {} {} {:02d}UTC".format( + valid_dt.day, + dt.strftime(valid_dt, '%b'), + valid_dt.year, + valid_dt.hour ), - html.Span([ - html.B("Lat {:.2f}, Lon {:.2f}".format(lat, lon)), html.Br(), - "DATE {:02d} {} {} {:02d}UTC".format( - valid_dt.day, - dt.strftime(valid_dt, '%b'), - valid_dt.year, - valid_dt.hour + html.Br(), + html.Button("EXPLORE TIMESERIES", + id=dict( + tag='ts-button', + index=model, + random=int(random()*100) ), - html.Br(), - html.Button("EXPLORE TIMESERIES", - id=dict( - tag='ts-button', - index=model, - random=int(random()*100) - ), - n_clicks=0, - className='popup-ts-button' - )], - className='popup-map-body', + n_clicks=0, + className='popup-ts-button' )], - ) - ], - position=[lat, lon], - autoClose=True, - closeOnEscapeKey=True, - closeOnClick=False, - closeButton=True, - className='popup-map-point', - ) - - # if DEBUG: print("||||", res, "\n", res[mod_idx], type(res[mod_idx])) - if DEBUG: print("||||", res[mod_idx][-1], type(res[mod_idx][-1])) - if not res[mod_idx] or res[mod_idx] is None: - if DEBUG: print("*** 1 ***") - res[mod_idx] = marker - elif res[mod_idx][-1] is None: - if DEBUG: print("*** 2 ***") - res[mod_idx][-1] = marker + className='popup-map-body', + )], + ) + ], + position=[lat, lon], + autoClose=True, + closeOnEscapeKey=True, + closeOnClick=False, + closeButton=True, + className='popup-map-point', + ) + + # if DEBUG: print("||||", res, "\n", res[mod_idx], type(res[mod_idx])) + if DEBUG: print("||||", res[mod_idx][-1], type(res[mod_idx][-1])) + if not res[mod_idx] or res[mod_idx] is None: + if DEBUG: print("*** 1 ***") + res[mod_idx] = marker + elif res[mod_idx][-1] is None: + if DEBUG: print("*** 2 ***") + res[mod_idx][-1] = marker # elif res[mod_idx][-1]['type'] == 'Popup': # if DEBUG: print("*** 3 ***") # res[mod_idx].pop(-1) # res[mod_idx].append(marker) - else: - if DEBUG: print("*** 3 ***") - res[mod_idx].append(marker) - if not coords or coords is None: - coords = { model: [lat, lon] } - else: - coords[model] = [lat, lon] - - popups[model] = sum([res[mod_idx][i]['type'] == 'Popup' and 1 or 0 - for i in range(len(res[mod_idx])) if 'type' in - res[mod_idx][i]]) + 1 - - if DEBUG: print("COORDS:", str(coords)) - if DEBUG: print("POPUPS:", str(popups)) - return coords, popups, res - - return {}, {}, dash.no_update - # raise PreventUpdate - - - # retrieve timeseries according to coordinates selected - @app.callback( - [Output('ts-modal', 'children'), - Output('ts-modal', 'is_open'), - Output({ 'tag': 'ts-button', 'index': ALL, 'random': ALL }, 'n_clicks')], - [Input({ 'tag': 'ts-button', 'index': ALL, 'random': ALL }, 'n_clicks')], - [State('model-dropdown', 'value'), - State('model-date-picker', 'date'), - State('variable-dropdown-forecast', 'value'), - State('model-clicked-coords', 'data'), - State('current-popups-stored', 'data')], -# prevent_initial_call=False - ) - @cache.memoize(timeout=cache_timeout) - def show_timeseries(ts_button, mod, date, variable, coords, popups): - """ Renders model comparison timeseries """ - from tools import get_timeseries + else: + if DEBUG: print("*** 3 ***") + res[mod_idx].append(marker) + if not coords or coords is None: + coords = { model: [lat, lon] } + else: + coords[model] = [lat, lon] + + popups[model] = sum([res[mod_idx][i]['type'] == 'Popup' and 1 or 0 + for i in range(len(res[mod_idx])) if 'type' in + res[mod_idx][i]]) + 1 + + if DEBUG: print("COORDS:", str(coords)) + if DEBUG: print("POPUPS:", str(popups)) + return coords, popups, res + + return {}, {}, dash.no_update + # raise PreventUpdate + + +# retrieve timeseries according to coordinates selected +@dash.callback( + [Output('ts-modal', 'children'), + Output('ts-modal', 'is_open'), + Output({ 'tag': 'ts-button', 'index': ALL, 'random': ALL }, 'n_clicks')], + [Input({ 'tag': 'ts-button', 'index': ALL, 'random': ALL }, 'n_clicks')], + [State('model-dropdown', 'value'), + State('model-date-picker', 'date'), + State('variable-dropdown-forecast', 'value'), + State('model-clicked-coords', 'data'), + State('current-popups-stored', 'data')], + prevent_initial_call=True +) +@cache.memoize(timeout=cache_timeout) +def show_timeseries(ts_button, mod, date, variable, coords, popups): + """ Renders model comparison timeseries """ + from tools import get_timeseries + + ctx = dash.callback_context + if ctx.triggered: + button_id = orjson.loads(ctx.triggered[0]["prop_id"].split(".")[0]) + if DEBUG: print("BUTTONID:", mod, str(button_id), str(ts_button)) + if DEBUG: print('SHOW TS COORDS:', coords, type(coords), mod) + ts_popups = 0 + for m in popups: + if DEBUG: print("___", m, "___") + ts_popups += popups[m] + if m == button_id['index']: + break + + if ts_popups == 0: + ts_popups = 1 + + if button_id['tag'] == 'ts-button' and ts_button[ts_popups-1] >= 1: + lat, lon = coords[button_id['index']] + + if lat is None or lon is None: + raise PreventUpdate - ctx = dash.callback_context - if ctx.triggered: - button_id = orjson.loads(ctx.triggered[0]["prop_id"].split(".")[0]) - if DEBUG: print("BUTTONID:", mod, str(button_id), str(ts_button)) - if DEBUG: print('SHOW TS COORDS:', coords, type(coords), mod) - ts_popups = 0 - for m in popups: - if DEBUG: print("___", m, "___") - ts_popups += popups[m] - if m == button_id['index']: - break - - if ts_popups == 0: - ts_popups = 1 - - if button_id['tag'] == 'ts-button' and ts_button[ts_popups-1] >= 1: - lat, lon = coords[button_id['index']] - - if lat is None or lon is None: - raise PreventUpdate - - if DEBUG: print('SHOW TS """""', mod, lat, lon) - figure = get_timeseries(mod, date, variable, lat, lon, forecast=True) - figure.update_layout(MODEBAR_LAYOUT_TS) - ts_body = dbc.ModalBody( - dcc.Graph( - id='timeseries-modal', - figure=figure, - config=MODEBAR_CONFIG_TS - ) + if DEBUG: print('SHOW TS """""', mod, lat, lon) + figure = get_timeseries(mod, date, variable, lat, lon, forecast=True) + figure.update_layout(MODEBAR_LAYOUT_TS) + ts_body = dbc.ModalBody( + dcc.Graph( + id='timeseries-modal', + figure=figure, + config=MODEBAR_CONFIG_TS ) + ) - return ts_body, True, [0 for _ in ts_button] - return dash.no_update, False, [0 for _ in ts_button] + return ts_body, True, [0 for _ in ts_button] return dash.no_update, False, [0 for _ in ts_button] - # raise PreventUpdate - - - # start/stop animation - @app.callback( - [Output('slider-interval', 'disabled'), - Output('slider-interval', 'n_intervals'), - Output('open-timeseries', 'style'), - #Output('div-collection', 'children'), - ], - [Input('btn-play', 'n_clicks'), - Input('btn-stop', 'n_clicks')], - [State('slider-interval', 'disabled'), - State('slider-graph', 'value')], - prevent_initial_call=True - ) - def start_stop_autoslider(n_play, n_stop, disabled, value): - """ Play/Pause map animation """ - ctx = dash.callback_context - if DEBUG: print("VALUE", value) - if not value: - value = 0 - - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id == 'btn-play' and disabled: - ts_style = { 'display': 'none' } - return not disabled, int(value/FREQ), ts_style - elif button_id == 'btn-stop' and not disabled: - ts_style = { 'display': 'block' } - return not disabled, int(value/FREQ), ts_style - - raise PreventUpdate - - - @app.callback( - Output('slider-graph', 'value'), - [Input('slider-interval', 'n_intervals')], - prevent_initial_call=True + return dash.no_update, False, [0 for _ in ts_button] + # raise PreventUpdate + + +# start/stop animation +@dash.callback( + [Output('slider-interval', 'disabled'), + Output('slider-interval', 'n_intervals'), + Output('open-timeseries', 'style'), + #Output('div-collection', 'children'), + ], + [Input('btn-play', 'n_clicks'), + Input('btn-stop', 'n_clicks')], + [State('slider-interval', 'disabled'), + State('slider-graph', 'value')], + prevent_initial_call=True ) - @cache.memoize(timeout=cache_timeout) - def update_slider(n): - """ Update slider value according to the number of intervals """ - if DEBUG: print('SERVER: updating slider-graph ' + str(n)) - if not n: - return - if n >= 24: - tstep = int(round(24*math.modf(n/24)[0], 0)) - else: - tstep = int(n) - if DEBUG: print('SERVER: updating slider-graph ' + str(tstep*FREQ)) - return tstep*FREQ - - - @app.callback( - Output('forecast-tab', 'children'), - [Input('models-apply', 'n_clicks'), - Input('prob-apply', 'n_clicks'), - Input('was-apply', 'n_clicks')], - [State({'tag': 'tab-name', 'index': ALL}, 'id')], - prevent_initial_call=True - ) - @cache.memoize(timeout=cache_timeout) - def update_tab_content(models_clicks, prob_clicks, was_clicks, curtab): - ctx = dash.callback_context - - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id not in ('models-apply', 'prob-apply', 'was-apply'): - raise PreventUpdate +def start_stop_autoslider(n_play, n_stop, disabled, value): + """ Play/Pause map animation """ + ctx = dash.callback_context + if DEBUG: print("VALUE", value) + if not value: + value = 0 + + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if button_id == 'btn-play' and disabled: + ts_style = { 'display': 'none' } + return not disabled, int(value/FREQ), ts_style + elif button_id == 'btn-stop' and not disabled: + ts_style = { 'display': 'block' } + return not disabled, int(value/FREQ), ts_style + + raise PreventUpdate + + +@dash.callback( + Output('slider-graph', 'value'), + [Input('slider-interval', 'n_intervals')], + prevent_initial_call=True +) +@cache.memoize(timeout=cache_timeout) +def update_slider(n): + """ Update slider value according to the number of intervals """ + if DEBUG: print('SERVER: updating slider-graph ' + str(n)) + if not n: + return + if n >= 24: + tstep = int(round(24*math.modf(n/24)[0], 0)) + else: + tstep = int(n) + if DEBUG: print('SERVER: updating slider-graph ' + str(tstep*FREQ)) + return tstep*FREQ + + +@dash.callback( + Output('forecast-tab', 'children'), + [Input('models-apply', 'n_clicks'), + Input('prob-apply', 'n_clicks'), + Input('was-apply', 'n_clicks')], + [State({'tag': 'tab-name', 'index': ALL}, 'id')], + prevent_initial_call=True + ) +@cache.memoize(timeout=cache_timeout) +def update_tab_content(models_clicks, prob_clicks, was_clicks, curtab): + ctx = dash.callback_context - if DEBUG: print("::::::::::::", len(curtab), curtab[0]['index']) - curtab_name = curtab[0]['index'] + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if button_id not in ('models-apply', 'prob-apply', 'was-apply'): + raise PreventUpdate - nexttab_name = button_id.replace('-apply', '') - if DEBUG: print("::::::::::::", curtab_name, nexttab_name) - if curtab_name != nexttab_name: - return tab_forecast(nexttab_name) + if DEBUG: print("::::::::::::", len(curtab), curtab[0]['index']) + curtab_name = curtab[0]['index'] - raise PreventUpdate + nexttab_name = button_id.replace('-apply', '') + if DEBUG: print("::::::::::::", curtab_name, nexttab_name) + if curtab_name != nexttab_name: + return tab_forecast(nexttab_name) raise PreventUpdate + raise PreventUpdate + # app.clientside_callback( # """ @@ -1014,7 +1015,6 @@ def register_callbacks(app, cache, cache_timeout): # Output('graph-collection', 'children'), # Input('clientside-graph-collection', 'data'), # ) - @app.callback( Output('graph-collection', 'children'), @@ -1042,97 +1042,99 @@ def register_callbacks(app, cache, cache_timeout): raise PreventUpdate elif tstep is None and date is None: raise PreventUpdate + elif tstep is None and date is None: + raise PreventUpdate - from tools import get_figure - if DEBUG: print('SERVER: calling figure from picker callback') + from tools import get_figure + if DEBUG: print('SERVER: calling figure from picker callback') - st_time = time.time() + st_time = time.time() - # if DEBUG: print('SERVER: interval ' + str(n)) - if DEBUG: print('SERVER: tstep ' + str(tstep)) + # if DEBUG: print('SERVER: interval ' + str(n)) + if DEBUG: print('SERVER: tstep ' + str(tstep)) - if date is not None: - date = date.split(' ')[0] - try: - date = dt.strptime( - date, "%Y-%m-%d").strftime("%Y%m%d") - except: - pass - if DEBUG: print('SERVER: callback date {}'.format(date)) - else: - date = end_date + if date is not None: + date = date.split(' ')[0] + try: + date = dt.strptime( + date, "%Y-%m-%d").strftime("%Y%m%d") + except: + pass + if DEBUG: print('SERVER: callback date {}'.format(date)) + else: + date = end_date - if model is None: - model = DEFAULT_MODEL + if model is None: + model = DEFAULT_MODEL - if variable is None: - variable = DEFAULT_VAR + if variable is None: + variable = DEFAULT_VAR - if tstep is not None: - tstep = int(tstep/FREQ) - else: - tstep = 0 + if tstep is not None: + tstep = int(tstep/FREQ) + else: + tstep = 0 - if DEBUG: print('SERVER: tstep calc ' + str(tstep)) - if DEBUG: print('#### IDS, ZOOM, CENTER:', ids, zoom, center) + if DEBUG: print('SERVER: tstep calc ' + str(tstep)) + if DEBUG: print('#### IDS, ZOOM, CENTER:', ids, zoom, center) - curr_mods = { i['index']: { 'zoom': z, 'center': c } - for i, z, c in zip(ids, zoom, center) } + curr_mods = { i['index']: { 'zoom': z, 'center': c } + for i, z, c in zip(ids, zoom, center) } - figures = [] + figures = [] - ncols, nrows = calc_matrix(len(model)) + ncols, nrows = calc_matrix(len(model)) - if not zoom: - zoom = [None] - if not center: - center = [None] + if not zoom: + zoom = [None] + if not center: + center = [None] - if len(model) != len(zoom): - if DEBUG: print("##############", len(model), len(zoom), "**********") - zoom = [None for item in model] - center = [None for item in model] + if len(model) != len(zoom): + if DEBUG: print("##############", len(model), len(zoom), "**********") + zoom = [None for item in model] + center = [None for item in model] - if DEBUG: print('#### ZOOM, CENTER:', zoom, center, model, ncols, nrows) - view = list(STYLES.keys())[view.index(True)] + if DEBUG: print('#### ZOOM, CENTER:', zoom, center, model, ncols, nrows) + view = list(STYLES.keys())[view.index(True)] # for mod in model: # mod_zoom = mod in curr_mods and curr_mods[mod]['zoom'] or None # mod_center = mod in curr_mods and curr_mods[mod]['center'] or None - for mod, mod_zoom, mod_center in zip(model, zoom, center): - if DEBUG: print("MOD", mod, "ZOOM", mod_zoom, "CENTER", mod_center, 'VIEW', view) - - figure = get_figure(mod, variable, date, tstep, - static=static, aspect=(nrows, ncols), view=view, - center=mod_center, zoom=mod_zoom) - - figure.style = { - 'height': '{}vh'.format(int(GRAPH_HEIGHT/nrows)), - } - - # add n_clicks to id dict to reset the state of map, for centering - figure.id['n_clicks'] = n_clicks - # if DEBUG: print('FIGURE KEYS 2', figure.id) - if DEBUG: print('STATIC', static, int(GRAPH_HEIGHT)/nrows) - figures.append(figure) - - if DEBUG: - for f in figures: - print("************", f.id, f.style, "**************") - - res = [ - dbc.Row( - [ - dbc.Col( - figures[row+col+(row*(ncols-1))], - width=int(12/ncols), - ) - for col in range(ncols) - if len(figures) > row+col+(row*(ncols-1)) - ], - align="start", - no_gutters=True, - ) for row in range(nrows) - ] - if DEBUG: print(ncols, nrows, len(res), [(type(i), len(i.children)) for i in res]) - if DEBUG: print("**** REQUEST TIME", str(time.time() - st_time)) - return res + for mod, mod_zoom, mod_center in zip(model, zoom, center): + if DEBUG: print("MOD", mod, "ZOOM", mod_zoom, "CENTER", mod_center, 'VIEW', view) + + figure = get_figure(mod, variable, date, tstep, + static=static, aspect=(nrows, ncols), view=view, + center=mod_center, zoom=mod_zoom) + + figure.style = { + 'height': '{}vh'.format(int(GRAPH_HEIGHT/nrows)), + } + + # add n_clicks to id dict to reset the state of map, for centering + figure.id['n_clicks'] = n_clicks + # if DEBUG: print('FIGURE KEYS 2', figure.id) + if DEBUG: print('STATIC', static, int(GRAPH_HEIGHT)/nrows) + figures.append(figure) + + if DEBUG: + for f in figures: + print("************", f.id, f.style, "**************") + + res = [ + dbc.Row( + [ + dbc.Col( + figures[row+col+(row*(ncols-1))], + width=int(12/ncols), + ) + for col in range(ncols) + if len(figures) > row+col+(row*(ncols-1)) + ], + align="start", + no_gutters=True, + ) for row in range(nrows) + ] + if DEBUG: print(ncols, nrows, len(res), [(type(i), len(i.children)) for i in res]) + if DEBUG: print("**** REQUEST TIME", str(time.time() - st_time)) + return res diff --git a/tabs/observations.py b/tabs/observations.py index d182a87c76c7b07bb8175602cd5f194e086dda09..576e4f2f9a7f4389e056ac53c616a740cb305185 100644 --- a/tabs/observations.py +++ b/tabs/observations.py @@ -237,7 +237,16 @@ def tab_observations(window='rgb'): children=windows[window], ) -def sidebar_observations(): +def sidebar_observations(option='rgb'): + """ Build the observations sidebar""" + + #Highlight the selected section + rgb_style = { 'fontWeight': 'bold' } + visibility_style = { 'fontWeight': 'normal' } + if option == 'visibility': + rgb_style={ 'fontWeight': 'normal' } + visibility_style= { 'fontWeight': 'bold' } + return [html.Div([ html.Label("Variable"), dcc.Dropdown( @@ -247,7 +256,7 @@ def sidebar_observations(): # {'label': 'AOD', 'value': 'aod'}, {'label': 'Visibility', 'value': 'visibility'} ], - value='rgb', + value=option, clearable=False, searchable=False, disabled=True @@ -260,7 +269,8 @@ def sidebar_observations(): html.Div([ dbc.Button("EUMETSAT RGB", color="link", - id='rgb' + id='rgb', + style=rgb_style )], className="sidebar-item", ), @@ -275,7 +285,8 @@ def sidebar_observations(): html.Div([ dbc.Button("Visibility", color="link", - id='visibility' + id='visibility', + style=visibility_style )], className="sidebar-item", ), diff --git a/tabs/observations_callbacks.py b/tabs/observations_callbacks.py index bab1c339ab1e1dbfc155b2b91337d4b2ef2c6ba5..2735039c7cbe999746998d8a7f305b295120f3b6 100644 --- a/tabs/observations_callbacks.py +++ b/tabs/observations_callbacks.py @@ -34,44 +34,45 @@ import math start_date = DATES['start_date'] end_date = DATES['end_date'] or dt.now().strftime("%Y%m%d") +from data_handler import cache_timeout -def register_callbacks(app, cache, cache_timeout): - """ Registering callbacks """ +#def register_callbacks(app, cache, cache_timeout): +# """ Registering callbacks """ - @app.callback( - [Output('observations-tab', 'children'), - Output('rgb', 'style'), - # Output('aod', 'style'), - Output('visibility', 'style'), - Output('variable-dropdown-observation', 'value')], - [Input('rgb', 'n_clicks'), - # Input('aod', 'n_clicks'), - Input('visibility', 'n_clicks')], - prevent_initial_call=True - ) - # def render_observations_tab(rgb_button, aod_button, vis_button): - def render_observations_tab(rgb_button, vis_button): - """ Function rendering requested tab """ - ctx = dash.callback_context +@dash.callback( + [Output('observations-tab', 'children'), + Output('rgb', 'style'), + # Output('aod', 'style'), + Output('visibility', 'style'), + Output('variable-dropdown-observation', 'value')], + [Input('rgb', 'n_clicks'), + # Input('aod', 'n_clicks'), + Input('visibility', 'n_clicks')], + prevent_initial_call=True +) +# def render_observations_tab(rgb_button, aod_button, vis_button): +def render_observations_tab(rgb_button, vis_button): + """ Function rendering requested tab """ + ctx = dash.callback_context - bold = { 'font-weight': 'bold' } - norm = { 'font-weight': 'normal' } + bold = { 'font-weight': 'bold' } + norm = { 'font-weight': 'normal' } - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id == "rgb" and rgb_button: - return tab_observations('rgb'), bold, norm, button_id + if button_id == "rgb" and rgb_button: + return tab_observations('rgb'), bold, norm, button_id # elif button_id == "aod" and aod_button: # return tab_observations('aod'), norm, bold, norm, button_id - elif button_id == "visibility" and vis_button: - return tab_observations('visibility'), norm, bold, button_id - else: - raise PreventUpdate + elif button_id == "visibility" and vis_button: + return tab_observations('visibility'), norm, bold, button_id + else: + raise PreventUpdate - return dash.no_update, bold, norm, 'rgb' + return dash.no_update, bold, norm, 'rgb' -# @app.callback( +# @dash.callback( # Output('aod-image', 'src'), # [Input('obs-aod-date-picker', 'date'), # Input('obs-aod-slider-graph', 'value')], @@ -90,7 +91,7 @@ def register_callbacks(app, cache, cache_timeout): # return app.get_asset_url(path) # # # start/stop animation -# @app.callback( +# @dash.callback( # [Output('obs-aod-slider-interval', 'disabled'), # Output('obs-aod-slider-interval', 'n_intervals')], # [Input('btn-obs-aod-play', 'n_clicks'), @@ -115,7 +116,7 @@ def register_callbacks(app, cache, cache_timeout): # raise PreventUpdate # # -# @app.callback( +# @dash.callback( # Output('obs-aod-slider-graph', 'value'), # [Input('obs-aod-slider-interval', 'n_intervals')], # prevent_initial_call=True @@ -133,96 +134,96 @@ def register_callbacks(app, cache, cache_timeout): # return tstep - @app.callback( - [Output('rgb-image', 'src'), - Output('btn-fulldisc', 'active'), - Output('btn-middleeast', 'active')], - [Input('btn-fulldisc', 'n_clicks'), - Input('btn-middleeast', 'n_clicks'), - Input('obs-date-picker', 'date'), - Input('obs-slider-graph', 'value')], - [State('btn-fulldisc', 'active'), - State('btn-middleeast', 'active')], - prevent_initial_call=True - ) - def update_image_src(btn_fulldisc, btn_middleeast, date, tstep, btn_fulldisc_active, btn_middleeast_active): +@dash.callback( +[Output('rgb-image', 'src'), + Output('btn-fulldisc', 'active'), + Output('btn-middleeast', 'active')], +[Input('btn-fulldisc', 'n_clicks'), + Input('btn-middleeast', 'n_clicks'), + Input('obs-date-picker', 'date'), + Input('obs-slider-graph', 'value')], +[State('btn-fulldisc', 'active'), + State('btn-middleeast', 'active')], + prevent_initial_call=True +) +def update_image_src(btn_fulldisc, btn_middleeast, date, tstep, btn_fulldisc_active, btn_middleeast_active): - if DEBUG: - print('BUTTONS', date, tstep, btn_fulldisc_active, btn_middleeast_active) - ctx = dash.callback_context - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - else: - button_id = None - if button_id not in ('btn-fulldisc', 'btn-middleeast'): - if btn_middleeast_active: - button_id = 'btn-middleeast' - elif btn_fulldisc_active: - button_id = 'btn-fulldisc' + if DEBUG: + print('BUTTONS', date, tstep, btn_fulldisc_active, btn_middleeast_active) + ctx = dash.callback_context + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + else: + button_id = None + if button_id not in ('btn-fulldisc', 'btn-middleeast'): + if btn_middleeast_active: + button_id = 'btn-middleeast' + elif btn_fulldisc_active: + button_id = 'btn-fulldisc' - if DEBUG: print('BUTTONS', button_id) + if DEBUG: print('BUTTONS', button_id) - if button_id == 'btn-middleeast': - path_tpl = 'eumetsat/MiddleEast/archive/{date}/MET8_RGBDust_MiddleEast_{date}{tstep:02d}00.gif' - btn_fulldisc_active = False - btn_middleeast_active = True - elif button_id == 'btn-fulldisc': - path_tpl = 'eumetsat/FullDiscHD/archive/{date}/FRAME_OIS_RGB-dust-all_{date}{tstep:02d}00.gif' - btn_fulldisc_active = True - btn_middleeast_active = False + if button_id == 'btn-middleeast': + path_tpl = 'eumetsat/MiddleEast/archive/{date}/MET8_RGBDust_MiddleEast_{date}{tstep:02d}00.gif' + btn_fulldisc_active = False + btn_middleeast_active = True + elif button_id == 'btn-fulldisc': + path_tpl = 'eumetsat/FullDiscHD/archive/{date}/FRAME_OIS_RGB-dust-all_{date}{tstep:02d}00.gif' + btn_fulldisc_active = True + btn_middleeast_active = False - try: - date = dt.strptime(date, '%Y-%m-%d').strftime('%Y%m%d') - except: - pass - path = path_tpl.format(date=date, tstep=tstep) - if DEBUG: print('......', path) - return app.get_asset_url(path), btn_fulldisc_active, btn_middleeast_active + try: + date = dt.strptime(date, '%Y-%m-%d').strftime('%Y%m%d') + except: + pass + path = path_tpl.format(date=date, tstep=tstep) + if DEBUG: print('......', path) + return app.get_asset_url(path), btn_fulldisc_active, btn_middleeast_active - # start/stop animation - @app.callback( - [Output('obs-slider-interval', 'disabled'), - Output('obs-slider-interval', 'n_intervals')], - [Input('btn-obs-play', 'n_clicks'), - Input('btn-obs-stop', 'n_clicks')], - [State('obs-slider-interval', 'disabled'), - State('obs-slider-graph', 'value')], - prevent_initial_call=True - ) - def start_stop_obs_autoslider(n_play, n_stop, disabled, value): - """ Play/Pause map animation """ - ctx = dash.callback_context - if DEBUG: print("VALUE", value) - if not value: - value = 0 - if ctx.triggered: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id == 'btn-obs-play' and disabled: - return not disabled, int(value) - elif button_id == 'btn-obs-stop' and not disabled: - return not disabled, int(value) +# start/stop animation +@dash.callback( + [Output('obs-slider-interval', 'disabled'), + Output('obs-slider-interval', 'n_intervals')], + [Input('btn-obs-play', 'n_clicks'), + Input('btn-obs-stop', 'n_clicks')], + [State('obs-slider-interval', 'disabled'), + State('obs-slider-graph', 'value')], + prevent_initial_call=True +) +def start_stop_obs_autoslider(n_play, n_stop, disabled, value): + """ Play/Pause map animation """ + ctx = dash.callback_context + if DEBUG: print("VALUE", value) + if not value: + value = 0 + if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + if button_id == 'btn-obs-play' and disabled: + return not disabled, int(value) + elif button_id == 'btn-obs-stop' and not disabled: + return not disabled, int(value) - raise PreventUpdate + raise PreventUpdate - @app.callback( - Output('obs-slider-graph', 'value'), - [Input('obs-slider-interval', 'n_intervals')], - prevent_initial_call=True - ) - def update_obs_slider(n): - """ Update slider value according to the number of intervals """ - if DEBUG: print('SERVER: updating slider-graph ' + str(n)) - if not n: - return 0 - if n >= 24: - tstep = int(round(24*math.modf(n/24)[0], 0)) - else: - tstep = int(n) - if DEBUG: print('SERVER: updating slider-graph ' + str(tstep)) - return tstep +@dash.callback( + Output('obs-slider-graph', 'value'), + [Input('obs-slider-interval', 'n_intervals')], + prevent_initial_call=True + ) +def update_obs_slider(n): + """ Update slider value according to the number of intervals """ + if DEBUG: print('SERVER: updating slider-graph ' + str(n)) + if not n: + return 0 + if n >= 24: + tstep = int(round(24*math.modf(n/24)[0], 0)) + else: + tstep = int(n) + if DEBUG: print('SERVER: updating slider-graph ' + str(tstep)) + return tstep -# @app.callback( +# @dash.callback( # [Output({'tag': 'model-tile-layer', 'index': ALL}, 'url'), # Output({'tag': 'model-tile-layer', 'index': ALL}, 'attribution'), # Output({'tag': 'view-style', 'index': ALL}, 'active')], @@ -260,36 +261,36 @@ def register_callbacks(app, cache, cache_timeout): # raise PreventUpdate - @app.callback( - Output('obs-vis-graph', 'children'), - [Input('obs-vis-date-picker', 'date'), - Input('obs-vis-slider-graph', 'value')], - [State('obs-vis-graph', 'zoom'), - State('obs-vis-graph', 'center')], - prevent_initial_call=False - ) - def update_vis_figure(date, tstep, zoom, center): - from tools import get_vis_figure - from tools import get_figure - if DEBUG: print("*************", date, tstep, zoom, center) - if date is not None: - date = date.split(' ')[0] - try: - date = dt.strptime( - date, "%Y-%m-%d").strftime("%Y%m%d") - except: - pass - else: - date = end_date +@dash.callback( + Output('obs-vis-graph', 'children'), + [Input('obs-vis-date-picker', 'date'), + Input('obs-vis-slider-graph', 'value')], + [State('obs-vis-graph', 'zoom'), + State('obs-vis-graph', 'center')], + prevent_initial_call=False +) +def update_vis_figure(date, tstep, zoom, center): + from tools import get_vis_figure + from tools import get_figure + if DEBUG: print("*************", date, tstep, zoom, center) + if date is not None: + date = date.split(' ')[0] + try: + date = dt.strptime( + date, "%Y-%m-%d").strftime("%Y%m%d") + except: + pass + else: + date = end_date - # view = list(STYLES.keys())[view.index(True)] - if DEBUG: print('SERVER: VIS callback date {}, tstep {}'.format(date, tstep)) - df, points_layer = get_vis_figure(tstep=tstep, selected_date=date) - if DEBUG: print("POINTS LAYER", points_layer) - fig = get_figure(model=None, var=None, layer=points_layer, zoom=zoom, center=center) - return html.Div( - fig, - id='obs-vis-graph', - className='graph-with-slider' - ) - # return get_graph(index='vis-graph', figure=get_vis_figure(tstep=tstep, selected_date=date)) + # view = list(STYLES.keys())[view.index(True)] + if DEBUG: print('SERVER: VIS callback date {}, tstep {}'.format(date, tstep)) + df, points_layer = get_vis_figure(tstep=tstep, selected_date=date) + if DEBUG: print("POINTS LAYER", points_layer) + fig = get_figure(model=None, var=None, layer=points_layer, zoom=zoom, center=center) + return html.Div( + fig, + id='obs-vis-graph', + className='graph-with-slider' + ) + # return get_graph(index='vis-graph', figure=get_vis_figure(tstep=tstep, selected_date=date)) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..fc25146abed294de2328fb88881bbc813610affc --- /dev/null +++ b/tests/test_evaluation.py @@ -0,0 +1,15 @@ +import pytest +import pandas as pd +import importlib +import numpy as np +from contextvars import copy_context +import dash +from dash._callback_context import context_value +from dash._utils import AttributeDict +code = importlib.import_module('tabs.evaluation') + + +#=================test sidebar evaluation =========================== +def test_sidebar_evaluation(): + assert "Button(children='Visual comparison', id='nrt-evaluation', color='link', style={'fontWeight': 'bold'}" in str(code.sidebar_evaluation('nrt')) + assert "children='Statistics', id='scores-evaluation', color='link', style={'fontWeight': 'bold'})], className='sidebar-item')]" in str(code.sidebar_evaluation('scores')) diff --git a/tests/test_evaluation_callbacks.py b/tests/test_evaluation_callbacks.py new file mode 100644 index 0000000000000000000000000000000000000000..c8ab7bcfa28ed5162738e022df339f398b42037a --- /dev/null +++ b/tests/test_evaluation_callbacks.py @@ -0,0 +1,143 @@ +import pytest +import pandas as pd +import importlib +import numpy as np +from contextvars import copy_context +import dash +from dash._callback_context import context_value +from dash._utils import AttributeDict +code = importlib.import_module('tabs.evaluation_callbacks') + +#add equality checker for objects +def __eq__(self, other): + if isinstance(self, other.__class__): + return self.a == other.a and self.b == other.b + return False + +#============ TEST extend_l============================================ +def test_extend_l(): + assert code.extend_l([[1],[2],[3]]) == [1,2,3] + +#============ TEST render_evaluation_tab================================= +def test_render_evaluation_tab(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "nrt-evaluation.n_clicks"},{"prop_id": "scores-evaluation.n_clicks"}]})) + return code.render_evaluation_tab(0, 0) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output[0] == dash.no_update + assert output[1:] == ({ 'fontWeight': 'bold' }, { 'fontWeight': 'normal' }) + + +#============ TEST _no_data_modis======================================== +def test_no_data_modis(): + assert code._no_modis_data() == ([{'name': 'NO DATA', 'id': ''}], [], {'display': 'block'}) + +#============ TEST update_time_selection=================================== +def test_update_time_selection(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "obs-timescale-dropdown.value"},{"prop_id": "obs-network-dropdown.value"}]})) + return code.update_time_selection('seasonal', 'modis') + + ctx = copy_context() + output = ctx.run(run_callback) + assert "'Spring 2018', 'value': '201803-201805'}], 'Select season')" in str(output) + + +#============ TEST modis_scores_tables_retrieve======================== +def test_modis_scores_tables_retrieve_no_n_click(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "scores-apply.n_clicks"}, + {"prop_id": "obs-models-dropdown.value"}, + {"prop_id": "obs-statistics-dropdown.value"}, + {"prop_id": "obs-network-dropdown.value"}, + {"prop_id": "obs-timescale-dropdown.value"}, + {"prop_id": "obs-selection-dropdown.value"}]})) + return code.modis_scores_tables_retrieve(0, ['median'],['bias'],'modis','monthly','202208' ) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output[0] == dash.no_update + assert output[1] == dash.no_update + assert output[2] == {'display':'none'} + +def test_modis_scores_tables_retrieve_median_202206(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "scores-apply.n_clicks"}, + {"prop_id": "obs-models-dropdown.value"}, + {"prop_id": "obs-statistics-dropdown.value"}, + {"prop_id": "obs-network-dropdown.value"}, + {"prop_id": "obs-timescale-dropdown.value"}, + {"prop_id": "obs-selection-dropdown.value"}]})) + return code.modis_scores_tables_retrieve(1, ['median'],['bias'],'modis','monthly','202206' ) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output == ([{'id': 'model', 'name': ''}, {'id': 'bias', 'name': 'BIAS'}], [{'bias': '-0.09', 'model': 'MULTI-MODEL'}], {'display': 'block'}) + +def test_modis_scores_tables_retrieve_no_data(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "scores-apply.n_clicks"}, + {"prop_id": "obs-models-dropdown.value"}, + {"prop_id": "obs-statistics-dropdown.value"}, + {"prop_id": "obs-network-dropdown.value"}, + {"prop_id": "obs-timescale-dropdown.value"}, + {"prop_id": "obs-selection-dropdown.value"}]})) + return code.modis_scores_tables_retrieve(1, ['median'],['bias'],'modis','monthly','212208' ) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output == ([{'id': '', 'name': 'NO DATA'}], [], {'display': 'block'}) + + +#============ TEST aeronet_scores_tables_retrieve======================== +def test_aeronet_scores_tables_retrieve_no_n_click(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "scores-apply.n_clicks"}, + {"prop_id": "obs-models-dropdown.value"}, + {"prop_id": "obs-statistics-dropdown.value"}, + {"prop_id": "obs-network-dropdown.value"}, + {"prop_id": "obs-timescale-dropdown.value"}, + {"prop_id": "obs-selection-dropdown.value"}]})) + return code.aeronet_scores_tables_retrieve(0, None, None, None, None, None, ['median'], ['bias'], 'aeronet', 'monthly', '202208', [], [], None, [], [], None, [], [], None, [], [], None,[], [], None) + ctx = copy_context() + output = ctx.run(run_callback) + assert output[0] == dash.no_update + assert output[1] == dash.no_update + assert output[2] == {'display':'none'} + +def test_aeronet_scores_tables_retrieve_median_202206(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "scores-apply.n_clicks"}, + {"prop_id": "obs-models-dropdown.value"}, + {"prop_id": "obs-statistics-dropdown.value"}, + {"prop_id": "obs-network-dropdown.value"}, + {"prop_id": "obs-timescale-dropdown.value"}, + {"prop_id": "obs-selection-dropdown.value"}]})) + return code.aeronet_scores_tables_retrieve(1, None, None, None, None, None, ['median'], ['bias'], 'aeronet', 'monthly', '202208', [], [], None, [], [], None, [], [], None, [], [], None,[], [], None) + ctx = copy_context() + output = ctx.run(run_callback) + assert output[0] == [{'id': 'station', 'name': ['BIAS', '']}, {'id': 'median', 'name': ['BIAS', 'MULTI-MODEL']}] + assert output[1] == [{'median': '-0.17', 'station': 'Europe'}, {'median': '-0.11', 'station': 'Mediterranean'}, {'median': '-0.31', 'station': 'MiddleEast'}, {'median': '-0.03', 'station': 'NAfrica'}, {'median': '-0.11', 'station': 'Total'}] + + +def test_aeronet_scores_tables_retrieve_no_data(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "scores-apply.n_clicks"}, + {"prop_id": "obs-models-dropdown.value"}, + {"prop_id": "obs-statistics-dropdown.value"}, + {"prop_id": "obs-network-dropdown.value"}, + {"prop_id": "obs-timescale-dropdown.value"}, + {"prop_id": "obs-selection-dropdown.value"}]})) + return code.aeronet_scores_tables_retrieve(1, None, None, None, None, None, ['median'], ['bias'], 'aeronet', 'monthly', '212208', [], [], None, [], [], None, [], [], None, [], [], None,[], [], None) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output[0] == [{'id': 'station', 'name': ['NO DATA']},{'id': 'station', 'name': ['NO DATA']}, {'id': 'station', 'name': ['NO DATA']}, {'id': 'station', 'name': ['NO DATA']}, {'id': 'station', 'name': ['NO DATA']}] diff --git a/tests/test_nc2scores_aeronet.py b/tests/test_nc2scores_aeronet.py new file mode 100644 index 0000000000000000000000000000000000000000..02a21078cb81ce193d30d4c20bb515efb488f504 --- /dev/null +++ b/tests/test_nc2scores_aeronet.py @@ -0,0 +1,21 @@ +import pytest +import pandas as pd +import importlib +import numpy as np +code = importlib.import_module('preproc.nc2scores_aeronet') + +def test_dropna_and_setindex(): + d = {'time': [2,1,3],'col1':[np.nan, 22, 53]} + d = pd.DataFrame(d) + df = d.dropna().set_index('time') + res = code.dropna_and_setindex(d, 'Output string',column_name='col1') + assert res.shape == df.shape + +def test_verify_df_size_no_grpname(): + df = pd.DataFrame() + assert code.verify_df_size(df) == ('Total', {'BIAS': '-', 'CORR': '-', 'RMSE': '-', 'FRGE': '-', 'TOTN': '0'}) + +def test_verify_df_size_grpname(): + df = pd.DataFrame(columns=['test']) + assert code.verify_df_size(df, 'test') == (None, {'BIAS': '-', 'CORR': '-', 'RMSE': '-', 'FRGE': '-', 'TOTN': '0'}) + diff --git a/tests/test_observations.py b/tests/test_observations.py new file mode 100644 index 0000000000000000000000000000000000000000..c3cf5360deb5467c5224f2ccfe8f54af83be6c31 --- /dev/null +++ b/tests/test_observations.py @@ -0,0 +1,15 @@ +import pytest +import pandas as pd +import importlib +import numpy as np +from contextvars import copy_context +import dash +from dash._callback_context import context_value +from dash._utils import AttributeDict +code = importlib.import_module('tabs.observations') + + +#=================test sidebar observations =========================== +def test_sidebar_observations(): + assert "[Button(children='EUMETSAT RGB', id='rgb', color='link', style={'fontWeight': 'bold'})]" in str(code.sidebar_observations('rgb')) + assert "Button(children='Visibility', id='visibility', color='link', style={'fontWeight': 'bold'})" in str(code.sidebar_observations('visibility')) diff --git a/tests/test_router.py b/tests/test_router.py new file mode 100644 index 0000000000000000000000000000000000000000..d47859590772e94a4ebd9575009c423c765f3b50 --- /dev/null +++ b/tests/test_router.py @@ -0,0 +1,76 @@ +import pytest +import importlib +code = importlib.import_module('router') +from data_handler import ROUTE_DEFAULTS + +#============ TEST get_input_aliases================================= +def test_get_input_aliases(): + input = {'tab': ['forecast'], 'var': ['aod'], 'model': ['median', 'monarch', 'cams'], 'for_option': ['models'], 'eval_option': ['nrt'], 'obs_option': ['rgb'], 'date': ['20230110']} + assert code.get_input_aliases(input) == {'tab': ['forecast-tab'], 'var': ['OD550_DUST'], 'model': ['median', 'monarch', 'cams'], 'for_option': ['models'], 'eval_option': ['nrt'], 'obs_option': ['rgb'], 'date': ['20230110']} + + route_selections = {'model': ['multi-model']} + result = code.get_input_aliases(route_selections) + assert result['model'] == ['median'] + + route_selections = {'var': ['aod']} + result = code.get_input_aliases(route_selections) + assert result['var'] == ['OD550_DUST'] + +#============ TEST eval_section_query================================= +def test_eval_section_query(): + input = {'tab': ['forecast-tab'], 'var': ['aod'], 'section': ['was'], 'model': ['median', 'monarch', 'cams'], 'for_option': ['models'], 'eval_option': ['nrt'], 'obs_option': ['rgb'], 'date': ['20230110']} + assert code.eval_section_query(input) == {'tab': ['forecast-tab'], 'var': ['aod'], 'section': ['was'], 'model': ['median', 'monarch', 'cams'], 'for_option': ['was'], 'eval_option': ['nrt'], 'obs_option': ['rgb'], 'date': ['20230110']} + + queries = {'tab': ['forecast-tab'], 'section': ['models']} + result = code.eval_section_query(queries) + assert result['for_option'] == ['models'] + + queries = {'tab': ['evaluation-tab'], 'section': ['visibility']} + result = code.eval_section_query(queries) + assert result['eval_option'] == ['visibility'] + +#============ TEST get_url_queries================================= +def test_get_url_queries(): + url = '/dashboard/?var=aod&model=cams&date=20230110' + assert code.get_url_queries(url, ROUTE_DEFAULTS) == {'date': ['20230110'], 'eval_option': ['nrt'], 'for_option': ['models'], 'model': ['cams'], 'obs_option': ['rgb'], 'tab': ['forecast-tab'], 'var': ['OD550_DUST'], 'country':['burkinafaso'], 'download': [None]} + + url = "http://localhost/?tab=forecast§ion=models&model=multi-model&var=concentration&country=chad" + result = code.get_url_queries(url) + expected = {'tab': ['forecast-tab'], + 'var': ['SCONC_DUST'], + 'model': ['median'], + 'for_option': ['models'], + 'eval_option': ['nrt'], + 'obs_option': ['rgb'], + 'country': ['chad'], + 'download': [None], + 'date': ['20220831'], + 'section': ['models'] + } + assert result == expected + +#============ TEST render_sidebar================================= +def test_render_sidebar(): + #first test forecast tab + assert "className='collapsible-cards')], className='sidebar-bottom')]" in str(code.render_sidebar('forecast-tab')) + #test evaluation + assert "Div(children=[Button(children='Statistics', id='scores-evaluation', color='link'" in str(code.render_sidebar('evaluation-tab')) + #test observations + assert "id='observations-variable', className='sidebar-first-item')" in str(code.render_sidebar('observations-tab')) +#============ TEST router================================= +def test_router(): + #verify forecast with selections returns correctly + url ='http://bscesdust02.bsc.es:7778/dashboard/?var=aod&model=median&model=monarch&model=cams&date=20230110' + assert "id='app-tabs', value='forecast-tab')]" in str(code.router(url)) + + #verify evaluation with section + url ='http://bscesdust02.bsc.es:7778/dashboard/?tab=evaluation§ion=statistics' + assert "id='app-tabs', value='evaluation-tab')]" in str(code.router(url)) + + #verify observations with section + url ='http://bscesdust02.bsc.es:7778/dashboard/?tab=observations§ion=visibility' + assert "id='app-tabs', value='observations-tab')]" in str(code.router(url)) + + #verify incorrect url lands on default + url ="children=[H2(children='404 Error', id='error_title" + assert "id='app-tabs', value='forecast-tab')]" in str(code.router(url))