diff --git a/transmonee_dashboard/src/transmonee_dashboard/index.py b/transmonee_dashboard/src/transmonee_dashboard/index.py index 7f1871a7..9eb010d1 100755 --- a/transmonee_dashboard/src/transmonee_dashboard/index.py +++ b/transmonee_dashboard/src/transmonee_dashboard/index.py @@ -3,48 +3,32 @@ from .app import app from .utils import DashRouter, DashNavBar from .pages import ( - child_education, - child_protection, - child_health, - child_poverty, - child_rights, - child_participation, home, - overview, - resources, - country_profiles, - data_query, - adolescent, - disability, - gender, - ecd, - risks, - climate, + child_education ) from .components import fa - # Ordered iterable of routes: tuples of (route, layout), where 'route' is a # string corresponding to path of the route (will be prefixed with Dash's # 'routes_pathname_prefix' and 'layout' is a Dash Component. urls = ( ("", home.get_layout), - ("overview", overview.get_layout), + # ("overview", overview.get_layout), ("child-education", child_education.get_layout), - ("child-protection", child_protection.get_layout), - ("child-health", child_health.get_layout), - ("child-poverty", child_poverty.get_layout), - ("child-rights", child_rights.get_layout), - ("child-participation", child_participation.get_layout), - ("profiles", country_profiles.get_layout), - ("data_query", data_query.get_layout), - ("resources", resources.get_layout), - ("adolescent", adolescent.get_layout), - ("disability", disability.get_layout), - ("gender", gender.get_layout), - ("ecd", ecd.get_layout), - ("risks", risks.get_layout), - ("climate", climate.get_layout), + # ("child-protection", child_protection.get_layout), + # ("child-health", child_health.get_layout), + # ("child-poverty", child_poverty.get_layout), + # ("child-rights", child_rights.get_layout), + # ("child-participation", child_participation.get_layout), + # ("profiles", country_profiles.get_layout), + # ("data_query", data_query.get_layout), + # ("resources", resources.get_layout), + # ("adolescent", adolescent.get_layout), + # ("disability", disability.get_layout), + # ("gender", gender.get_layout), + # ("ecd", ecd.get_layout), + # ("risks", risks.get_layout), + # ("climate", climate.get_layout), ) # Ordered iterable of navbar items: tuples of `(route, display)`, where `route` @@ -53,57 +37,64 @@ # keyword argument for a Dash component (ie a Dash Component or a string). nav_items = ( ("", html.Div([fa("fas fa-home"), "Home"]), []), - ("overview", html.Div([fa("fas fa-info-circle"), "Overview"]), []), ( "sectors", - "Domains", - [ - ("child-education", html.Div([fa("fas fa-book"), "Education"])), - ( - "child-protection", - html.Div([fa("fas fa-child"), "Family Environment and Protection"]), - ), - ( - "child-health", - html.Div([fa("fas fa-heartbeat"), "Health and Nutrition"]), - ), - ( - "child-poverty", - html.Div([fa("fas fa-hand-holding-usd"), "Poverty"]), - ), - ( - "child-rights", - html.Div( - [ - fa("fas fa-balance-scale"), - "Child Rights Landscape", - ] - ), - ), - ( - "child-participation", - html.Div([fa("fas fa-users"), "Participation"]), - ), - ], - ), - ( - "cross-sectors", - "Cross-Sectoral Issues", - [ - ("adolescent", html.Div([fa("fas fa-user-friends"), "Adolescent"])), - ("disability", html.Div([fa("fas fa-blind"), "Disability"])), - ("gender", html.Div([fa("fas fa-venus-mars"), "Gender"])), - ("ecd", html.Div([fa("fas fa-baby"), "ECD"])), - ( - "risks", - html.Div([fa("fas fa-exclamation-triangle"), "Risks and Humanitarian"]), - ), - ("climate", html.Div([fa("fas fa-sun"), "Climate Change"])), - ], - ), - ("profiles", html.Div([fa("fas fa-globe"), "Country Snapshots"]), []), - ("data_query", html.Div([fa("fas fa-table"), "Query Data"]), []), - ("resources", html.Div([fa("fas fa-database"), "Resources"]), []), + "Domains", [ + ("child-education", html.Div([fa("fas fa-book"), "Education"])) + ] + ) + + # ("overview", html.Div([fa("fas fa-info-circle"), "Overview"]), []), + # ( + # "sectors", + # "Domains", + # [ + # ("child-education", html.Div([fa("fas fa-book"), "Education"])), + # ( + # "child-protection", + # html.Div([fa("fas fa-child"), "Family Environment and Protection"]), + # ), + # ( + # "child-health", + # html.Div([fa("fas fa-heartbeat"), "Health and Nutrition"]), + # ), + # ( + # "child-poverty", + # html.Div([fa("fas fa-hand-holding-usd"), "Poverty"]), + # ), + # ( + # "child-rights", + # html.Div( + # [ + # fa("fas fa-balance-scale"), + # "Child Rights Landscape", + # ] + # ), + # ), + # ( + # "child-participation", + # html.Div([fa("fas fa-users"), "Participation"]), + # ), + # ], + # ), + # ( + # "cross-sectors", + # "Cross-Sectoral Issues", + # [ + # ("adolescent", html.Div([fa("fas fa-user-friends"), "Adolescent"])), + # ("disability", html.Div([fa("fas fa-blind"), "Disability"])), + # ("gender", html.Div([fa("fas fa-venus-mars"), "Gender"])), + # ("ecd", html.Div([fa("fas fa-baby"), "ECD"])), + # ( + # "risks", + # html.Div([fa("fas fa-exclamation-triangle"), "Risks and Humanitarian"]), + # ), + # ("climate", html.Div([fa("fas fa-sun"), "Climate Change"])), + # ], + # ), + # ("profiles", html.Div([fa("fas fa-globe"), "Country Snapshots"]), []), + # ("data_query", html.Div([fa("fas fa-table"), "Query Data"]), []), + # ("resources", html.Div([fa("fas fa-database"), "Resources"]), []), ) nav_items_full_names = { diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/__init__.py b/transmonee_dashboard/src/transmonee_dashboard/pages/__init__.py old mode 100755 new mode 100644 index 46cd8726..2276412c --- a/transmonee_dashboard/src/transmonee_dashboard/pages/__init__.py +++ b/transmonee_dashboard/src/transmonee_dashboard/pages/__init__.py @@ -1,1074 +1,25 @@ -import json -import logging -import pathlib -import collections -from io import BytesIO -from re import L -import urllib - import dash_html_components as html -import numpy as np +import datetime +import collections import pandas as pd -import requests -from requests.exceptions import HTTPError - -import pandasdmx as sdmx - - -# TODO: Move all of these to env/setting vars from production -sdmx_url = "https://sdmx.data.unicef.org/ws/public/sdmxapi/rest/data/ECARO,TRANSMONEE,1.0/.{}....?format=csv&startPeriod={}&endPeriod={}" - -geo_json_file = ( - pathlib.Path(__file__).parent.parent.absolute() / "assets/countries.geo.json" -) -with open(geo_json_file) as shapes_file: - geo_json_countries = json.load(shapes_file) - -with open( - pathlib.Path(__file__).parent.parent.absolute() / "assets/indicator_config.json" -) as config_file: - indicators_config = json.load(config_file) - -unicef = sdmx.Request("UNICEF") - -metadata = unicef.dataflow("TRANSMONEE", provider="ECARO", version="1.0") -dsd = metadata.structure["DSD_ECARO_TRANSMONEE"] - -indicator_names = { - code.id: code.name.en - for code in dsd.dimensions.get("INDICATOR").local_representation.enumerated -} -# lbassil: get the age groups code list as it is not in the DSD -cl_age = unicef.codelist("CL_AGE", version="1.0") -age_groups = sdmx.to_pandas(cl_age) -dict_age_groups = age_groups["codelist"]["CL_AGE"].reset_index() -age_groups_names = { - age["CL_AGE"]: age["name"] - for index, age in dict_age_groups.iterrows() - if age["CL_AGE"] != "_T" -} - -units_names = { - unit.id: str(unit.name) - for unit in dsd.attributes.get("UNIT_MEASURE").local_representation.enumerated -} - -# lbassil: get the names of the residence dimensions -residence_names = { - residence.id: str(residence.name) - for residence in dsd.dimensions.get("RESIDENCE").local_representation.enumerated -} - -# lbassil: get the names of the wealth quintiles dimensions -wealth_names = { - wealth.id: str(wealth.name) - for wealth in dsd.dimensions.get("WEALTH_QUINTILE").local_representation.enumerated -} - -gender_names = {"F": "Female", "M": "Male", "_T": "Total"} - -dimension_names = { - "SEX": "Sex_name", - "AGE": "Age_name", - "RESIDENCE": "Residence_name", - "WEALTH_QUINTILE": "Wealth_name", -} - -adolescent_codes = [ - "DM_ASYL_FRST", - "DM_ASYL_UASC", - "HT_ADOL_UNMETMED_FEAR", - "HT_ADOL_UNMETMED_HOPING", - "HT_ADOL_UNMETMED_NOKNOW", - "HT_ADOL_UNMETMED_NOTIME", - "HT_ADOL_UNMETMED_NOUNMET", - "HT_ADOL_UNMETMED_TOOEFW", - "HT_ADOL_UNMETMED_TOOEXP", - "HT_ADOL_UNMETMED_TOOFAR", - "HT_ADOL_UNMETMED_WAITING", - "HT_ADOL_UNMETMED_OTH", - "HT_CDRT_SELF_HARM", - "HT_SH_HIV_INCD", - "HVA_EPI_LHIV_0-19", - "HVA_EPI_LHIV_15-24", - "JJ_PRISIONERS_RT", - "MNCH_CSEC", - "MNCH_PNCMOM", - "PT_ADLT_PS_NEC", - "PT_CHLD_1-14_PS-PSY-V_CGVR", - "PT_CHLD_5-17_LBR_ECON", - "PT_CHLD_5-17_LBR_ECON-HC", - "PT_CHLD_CARED_BY_FOSTER", - "PT_CHLD_ENTEREDFOSTER", - "PT_CHLD_INRESIDENTIAL", - "PT_F_15-49_W-BTNG", - "PT_F_GE15_PS-SX-EM_V_PTNR_12MNTH", - "PT_M_15-49_W-BTNG", - "PT_ST_13-15_BUL_30-DYS", - "PV_AROPE", - "PV_AROPRT", - "PV_SD_MDP_MUHC", - "PV_SEV_MAT_DPRT", - "PV_SI_POV_EMP1", -] - -adolescent_age_groups = [ - "_T", - "Y14T17", - "Y0T13", - "Y1T4", - "Y0", - "Y0T14", - "Y14T15", - "Y16T17", - "Y16T19", - "Y16T24", - "Y10T14", - "Y15T19", - "Y15T24", - "Y10T19", - "Y0T17", - "Y15T49", - "Y20T24", - "Y18T49", - "Y18T29", - "Y0T24", - "Y25T39", - "Y40T59", - "Y_GE50", - "Y_GE15", - "Y1T14", - "Y2T14", - "Y5T14", - "Y5T17", - "Y7T17", - "Y10T17", - "Y13T15", - "Y15", - "Y11T15", - "Y12T17", - "Y0T15", - "Y0T4", - "M0", -] - -# Add all indicators found in the data dictionary to get their data to the query data page -data_query_codes = [ - "DM_BRTS", - # "DM_POP_TOT", - # "DM_AVG_POP_TOT", - # "DM_POP_PROP", - "DM_DPR_AGE", - "DM_DPR_CHD", - "DM_DPR_OLD", - # "DM_IMG", - # "DM_EMG", - # "DM_NEXTRT_MG", - "DM_MRG_AGE", - "DM_DIV", - "DM_CRDIVRT", - "DM_CHLD_DIV", - "DM_CHLDRT_DIV", - "DM_POP_TOT_AGE", - "FT_WHS_PBR", - "MT_SP_DYN_CDRT_IN", - "DM_LIFE_EXP", - "HT_SH_HAP_HBSAG", - "HT_SH_TBS_INCD", - "HT_SH_SUD_ALCOL", - "HT_DIST79DTP3_P", - "IM_MCV2", - "HT_DIST79MCV2_P", - "HT_SDG_PM25", - "ECD_CHLD_36-59M_ADLT_SRC", - "HT_SH_SUD_TREAT", - "EDUNF_STU_L01_TOT", - "EDU_SDG_GER_L01", - "EDUNF_STU_L02_TOT", - "EDUNF_FEP_L02", - "EDUNF_NARA_L1_UNDER1", - "EDUNF_FEP_L1", - "EDUNF_FEP_L2", - "EDUNF_FEP_L3", - "EDUNF_STU_L3_GEN", - "EDUNF_STU_L3_VOC", - "EDUNF_STU_L3_GEN_PUB", - "EDUNF_STU_L3_GEN_PRV", - "EDUNF_STU_L3_VOC_PUB", - "EDUNF_STU_L3_VOC_PRV", - "EDUNF_GER_L1AND2", - "EDU_TIMSS_MAT4", - "EDU_TIMSS_SCI4", - "EDU_TIMSS_MAT8", - "EDU_TIMSS_SCI8", - "EDU_PIRLS_REA", - "EDUNF_REPP_L1", - "EDUNF_REPP_L2", - "EDUNF_FRP_L1", - "EDUNF_SR_L1", - "EDUNF_SR_L2", - "EDUNF_STU_L4_TOT", - "EDUNF_STU_L4_PUB", - "EDUNF_STU_L4_PRV", - "EDUNF_PRP_L4", - "EDUNF_FEP_L4", - "EDUNF_STU_L5T8_TOT", - "EDUNF_STU_L5T8_PUB", - "EDUNF_STU_L5T8_PRV", - "EDUNF_PRP_L5T8", - "EDUNF_FEP_L5T8", - "EDUNF_GER_GPI_L02", - "EDUNF_GER_GPI_L1", - "EDUNF_GER_GPI_L2", - "EDUNF_GER_GPI_L3", - "EDUNF_GER_GPI_L2AND3", - "EDUNF_PTR_L1", - "EDUNF_PTR_L2", - "EDUNF_PTR_L2AND3", - "EDUNF_PTR_L3", - "EDU_SDG_PTTR_L02", - "EDU_SDG_PTTR_L1", - "EDU_SDG_PTTR_L2", - "EDU_SDG_PTTR_L3", - "EDU_SDG_PQTR_L02", - "EDU_SDG_PQTR_L1", - "EDU_SDG_PQTR_L2", - "EDU_SDG_PQTR_L3", - "ECD_CHLD_U5_BKS-HM", - "ECD_CHLD_U5_PLYTH-HM", - "EDUNF_EA_L2T8", - "EDU_PISA_MAT2", - "EDU_PISA_MAT3", - "EDU_PISA_MAT4", - "EDU_PISA_MAT5", - "EDU_PISA_MAT6", - "EDU_PISA_REA2", - "EDU_PISA_REA3", - "EDU_PISA_REA4", - "EDU_PISA_REA5", - "EDU_PISA_REA6", - "EDU_PISA_SCI2", - "EDU_PISA_SCI3", - "EDU_PISA_SCI4", - "EDU_PISA_SCI5", - "EDU_PISA_SCI6", - "EDU_CHLD_DISAB_L02", - "EDU_CHLD_DISAB_L1", - "EDU_CHLD_DISAB_L2", - "EDU_CHLD_DISAB_L3", - "EDUNF_SAP_L02", - "EDUNF_SAP_L1", - "EDUNF_SAP_L2", - "EDUNF_SAP_L3", - "EDUNF_SAP_L2_GLAST", - "EDUNF_FRP_L2AND3", - "EDUNF_GER_GPI_L01", - "EDUNF_OFST_L1T3", - "EDUNF_PRP_L2AND3", - "EDUNF_STU_L2AND3_PRV", - "EDUNF_COMP_YR", - "EDUNF_COMP_YR_L02T3", - "EDUNF_PTR_L02", - "EDUNF_GECER_L01", - "EDUNF_GECER_L02", - "ED_ANAR_L1", - "ED_ANAR_L2", - "ED_ANAR_L3", - "EDU_SE_ACS_INTNT_L1", - "EDU_SE_ACS_INTNT_L2", - "EDU_SE_ACS_INTNT_L3", - "EDU_SE_ACS_CMPTR_L1", - "EDU_SE_ACS_CMPTR_L2", - "EDU_SE_ACS_CMPTR_L3", - "EDU_SE_ACS_ELECT_L1", - "EDU_SE_ACS_ELECT_L2", - "EDU_SE_ACS_ELECT_L3", - "EDU_SE_TOT_GPI_L1_REA", - "EDU_SE_TOT_GPI_L2_REA", - "EDU_SE_TOT_GPI_L1_MAT", - "EDU_SE_TOT_GPI_L2_MAT", - "EDU_SE_TOT_GPI_FS_LIT", - "EDU_SE_TOT_GPI_FS_NUM", - "EDU_SE_AGP_CPRA_L1", - "EDU_SE_AGP_CPRA_L2", - "EDU_SE_AGP_CPRA_L3", - "EDU_SE_GPI_PART", - "EDU_SE_GPI_PTNPRE", - "EDU_SE_GPI_TCAQ_L02", - "EDU_SE_GPI_TCAQ_L1", - "EDU_SE_GPI_TCAQ_L2", - "EDU_SE_GPI_TCAQ_L3", - "EDU_SE_NAP_ACHI_L1_REA", - "EDU_SE_NAP_ACHI_L2_REA", - "EDU_SE_NAP_ACHI_L1_MAT", - "EDU_SE_NAP_ACHI_L2_MAT", - "EDU_SE_IMP_FPOF_LIT", - "EDU_SE_IMP_FPOF_NUM", - "EDU_SE_LGP_ACHI_L1_REA", - "EDU_SE_LGP_ACHI_L2_REA", - "EDU_SE_LGP_ACHI_L1_MAT", - "EDU_SE_LGP_ACHI_L2_MAT", - "EDU_SE_ALP_CPLR_L1", - "EDU_SE_ALP_CPLR_L2", - "EDU_SE_ALP_CPLR_L3", - "EDU_SE_TOT_SESPI_L1_REA", - "EDU_SE_TOT_SESPI_L2_REA", - "EDU_SE_TOT_SESPI_L1_MAT", - "EDU_SE_TOT_SESPI_L2_MAT", - "EDU_SE_TOT_SESPI_FS_LIT", - "EDU_SE_TOT_SESPI_FS_NUM", - "EDU_SE_TOT_RUPI_L1_REA", - "EDU_SE_TOT_RUPI_L2_REA", - "EDU_SE_TOT_RUPI_L1_MAT", - "EDU_SE_TOT_RUPI_L2_MAT", - "EDU_SE_AWP_CPRA_L1", - "EDU_SE_AWP_CPRA_L2", - "EDU_SE_AWP_CPRA_L3", - "EDU_SE_GPI_ICTS_ATCH", - "EDU_SE_GPI_ICTS_CPT", - "EDU_SE_GPI_ICTS_CDV", - "EDU_SE_GPI_ICTS_SSHT", - "EDU_SE_GPI_ICTS_PRGM", - "EDU_SE_GPI_ICTS_PST", - "EDU_SE_GPI_ICTS_SFWR", - "EDU_SE_GPI_ICTS_TRFF", - "EDU_SE_GPI_ICTS_CMFL", - "EDUNF_STEM_GRAD_RT", - "DM_TOT_POP_PROSP", - "DM_SP_POP_BRTH_MF", - "DM_ADOL_YOUTH_POP", - "DM_REPD_AGE_POP", - "GN_MTNTY_LV_BNFTS", - "GN_PTNTY_LV_BNFTS", - "EC_GDI", - "EC_HCI_OVRL", - "EC_MIN_WAGE", - "EC_IQ_CPA_GNDR_XQ", - "EC_SIGI", - "EC_YOUTH_UNE_RT", - "EC_EAP_RT", - "EC_GNI_PCAP_PPP", - "EC_FB_BNK_ACCSS", - "SL_DOM_TSPD", - "SG_GEN_PARL", - "CR_VC_VOV_GDSD", - "PT_ADLS_10-14_LBR_HC", - "ECD_CHLD_U5_LFT-ALN", - "MNCH_MATERNAL_DEATHS", - "MNCH_SH_MMR_RISK", - "MNCH_SH_MMR_RISK_ZS", - "MNCH_INSTDEL", - "MNCH_BIRTH18", - "HT_NCD_BMI_18A", - "HVA_EPI_INF_ANN_15-24", - "CR_CCRI_VUL_HT", - "CR_CCRI_VUL_EDU", - "CR_CCRI_VUL_WASH", - "CR_CCRI_VUL_SP", - "CR_CCRI_VUL_ES", - "CR_CCRI", - "CR_CCRI_EXP_WS", - "CR_CCRI_EXP_RF", - "CR_CCRI_EXP_CF", - "CR_CCRI_EXP_TC", - "CR_CCRI_EXP_VBD", - "CR_CCRI_EXP_HEAT", - "CR_CCRI_EXP_AP", - "CR_CCRI_EXP_SWP", - "CR_CCRI_EXP_CESS", - "CR_UN_CHLD_RIGHTS", - "CR_UN_CHLD_SALE", - "CR_UN_RIGHTS_DISAB", -] - -years = list(range(2010, 2022)) - -# a key:value dictionary of countries where the 'key' is the country name as displayed in the selection -# tree whereas the 'value' is the country name as returned by the sdmx list: https://sdmx.data.unicef.org/ws/public/sdmxapi/rest/codelist/UNICEF/CL_COUNTRY/1.0 -countries_iso3_dict = { - "Albania": "ALB", - "Andorra": "AND", - "Armenia": "ARM", - "Austria": "AUT", - "Azerbaijan": "AZE", - "Belarus": "BLR", - "Belgium": "BEL", - "Bosnia and Herzegovina": "BIH", - "Bulgaria": "BGR", - "Croatia": "HRV", - "Cyprus": "CYP", - "Czech Republic": "CZE", - "Denmark": "DNK", - "Estonia": "EST", - "Finland": "FIN", - "France": "FRA", - "Georgia": "GEO", - "Germany": "DEU", - "Greece": "GRC", - "Holy See": "VAT", - "Hungary": "HUN", - "Iceland": "ISL", - "Ireland": "IRL", - "Italy": "ITA", - "Kazakhstan": "KAZ", - "Kosovo (UN SC resolution 1244)": "XKX", # UNDP defines it as KOS - "Kyrgyzstan": "KGZ", - "Latvia": "LVA", - "Liechtenstein": "LIE", - "Lithuania": "LTU", - "Luxembourg": "LUX", - "Malta": "MLT", - "Monaco": "MCO", - "Montenegro": "MNE", - "Netherlands": "NLD", - "North Macedonia": "MKD", - "Norway": "NOR", - "Poland": "POL", - "Portugal": "PRT", - "Republic of Moldova": "MDA", - "Romania": "ROU", - "Russian Federation": "RUS", - "San Marino": "SMR", - "Serbia": "SRB", - "Slovakia": "SVK", - "Slovenia": "SVN", - "Spain": "ESP", - "Sweden": "SWE", - "Switzerland": "CHE", - "Tajikistan": "TJK", - "Turkey": "TUR", - "Turkmenistan": "TKM", - "Ukraine": "UKR", - "United Kingdom": "GBR", - "Uzbekistan": "UZB", -} - -# create a list of country names in the same order as the countries_iso3_dict -countries = list(countries_iso3_dict.keys()) - -unicef_country_prog = [ - "Albania", - "Armenia", - "Azerbaijan", - "Belarus", - "Bosnia and Herzegovina", - "Bulgaria", - "Croatia", - "Georgia", - "Greece", - "Kazakhstan", - "Kosovo (UN SC resolution 1244)", - "Kyrgyzstan", - "Montenegro", - "North Macedonia", - "Republic of Moldova", - "Romania", - "Serbia", - "Tajikistan", - "Turkey", - "Turkmenistan", - "Ukraine", - "Uzbekistan", -] - -country_selections = [ - { - "label": "Eastern Europe and Central Asia", - "value": [ - {"label": "Caucasus", "value": ["Armenia", "Azerbaijan", "Georgia"]}, - { - "label": "Western Balkans", - "value": [ - "Albania", - "Bosnia and Herzegovina", - "Croatia", - "Kosovo (UN SC resolution 1244)", - "North Macedonia", - "Montenegro", - "Serbia", - ], - }, - { - "label": "Central Asia", - "value": [ - "Kazakhstan", - "Kyrgyzstan", - "Tajikistan", - "Turkmenistan", - "Uzbekistan", - ], - }, - { - "label": "Eastern Europe", - "value": [ - "Bulgaria", - "Belarus", - "Republic of Moldova", - "Romania", - "Russian Federation", - "Turkey", - "Ukraine", - ], - }, - ], - }, - { - "label": "Western Europe", - "value": [ - "Andorra", - "Austria", - "Belgium", - "Cyprus", - "Czech Republic", - "Denmark", - "Estonia", - "Finland", - "France", - "Germany", - "Greece", - "Holy See", - "Hungary", - "Iceland", - "Ireland", - "Italy", - "Latvia", - "Liechtenstein", - "Lithuania", - "Luxembourg", - "Malta", - "Monaco", - "Netherlands", - "Norway", - "Poland", - "Portugal", - "San Marino", - "Slovakia", - "Slovenia", - "Spain", - "Sweden", - "Switzerland", - "United Kingdom", - ], - }, - { - "label": "By EU Engagement", - "value": [ - { - "label": "Central Asia", - "value": [ - "Kazakhstan", - "Kyrgyzstan", - "Tajikistan", - "Turkmenistan", - "Uzbekistan", - ], - }, - { - "label": "Eastern Partnership", - "value": [ - "Armenia", - "Azerbaijan", - "Belarus", - "Georgia", - "Republic of Moldova", - "Ukraine", - ], - }, - { - "label": "EFTA", - "value": ["Iceland", "Liechtenstein", "Norway", "Switzerland"], - }, - { - "label": "EU Member States", - "value": [ - "Andorra", - "Austria", - "Belgium", - "Bulgaria", - "Croatia", - "Cyprus", - "Czech Republic", - "Denmark", - "Estonia", - "Finland", - "France", - "Germany", - "Greece", - "Hungary", - "Ireland", - "Italy", - "Latvia", - "Lithuania", - "Luxembourg", - "Malta", - "Netherlands", - "Poland", - "Portugal", - "Romania", - "Slovakia", - "Slovenia", - "Spain", - "Sweden", - ], - }, - { - "label": "Other", - "value": [ - "Andorra", - "Monaco", - "Holy See", - "San Marino", - ], - }, - { - "label": "Pre-accession countries", - "value": [ - "Albania", - "Bosnia and Herzegovina", - "Kosovo (UN SC resolution 1244)", - "North Macedonia", - "Montenegro", - "Serbia", - "Turkey", - ], - }, - { - "label": "Russian Federation", - "value": ["Russian Federation"], - }, - { - "label": "United Kingdom (left EU on January 31, 2020)", - "value": ["United Kingdom"], - }, - ], - }, -] - -data_sources = { - "CDDEM": "CountDown 2030", - "CCRI": "Children's Climate Risk Index", - "UN Treaties": "UN Treaties", - "ESTAT": "Euro Stat", - "Helix": " Health Entrepreneurship and LIfestyle Xchange", - "ILO": "International Labour Organization", - "WHO": "World Health Organization", - "Immunization Monitoring (WHO)": "Immunization Monitoring (WHO)", - "WB": "World Bank", - "OECD": "Organisation for Economic Co-operation and Development", - "SDG": "Sustainable Development Goals", - "UIS": "UNESCO Institute for Statistics", - "UNDP": "United Nations Development Programme", - "TMEE": "Transformative Monitoring for Enhanced Equity", -} - -topics_subtopics = { - "All": ["All"], - "Education, Leisure, and Culture": [ - {"Participation": "Education access and participation"}, - {"Quality": "Learning quality and skills"}, - {"System": "Education system"}, - ], - "Family Environment and Protection": [ - {"Violence": "Violence against Children and Women"}, - {"Care": "Children without parental care"}, - {"Justice": "Justice for Children"}, - {"Marriage": "Child marriage and other harmful practices"}, - {"Labour": "Child labour and other forms of exploitation"}, - ], - "Health and Nutrition": [ - {"HS": "Health System"}, - {"MNCH": "Maternal, newborn and child health"}, - {"Immunization": "Immunization"}, - {"Nutrition": "Nutrition"}, - {"Adolescent": "Adolescent physical, mental, and reproductive health"}, - {"HIVAIDS": "HIV/AIDS"}, - {"Wash": "Water, sanitation and hygiene"}, - ], - "Poverty and Social Protection": [ - {"Poverty": "Child Poverty and Material Deprivation"}, - {"Protection": "Social protection system"}, - ], - "Child Rights Landscape and Governance": [ - {"Demography": "Demographics"}, - {"Economy": "Political Economy"}, - {"Migration": "Migration and Displacement"}, - {"Access": "Access to Justice"}, - {"Data": "Data on Children"}, - {"Spending": "Public spending on Children"}, - ], - "Participation and Civil Rights": [ - {"Registration": "Birth registration and identity"}, - {"Information": "Information, Internet and Protection of privacy"}, - {"Leisure": "Education, Leisure, and Culture"}, - ], -} - -dict_topics_subtopics = { - "Education, Leisure, and Culture": [ - "Education access and participation", - "Learning quality and skills", - "Education System", - ], - "Family Environment and Protection": [ - "Violence against Children and Women", - "Children without parental care", - "Justice for Children", - "Child marriage and other harmful practices", - "Child labour and other forms of exploitation", - ], - "Health and Nutrition": [ - "Health System", - "Maternal, newborn and child health", - "Immunization", - "Nutrition", - "Adolescent physical, mental, and reproductive health", - "HIV/AIDS", - "Water, sanitation and hygiene", - ], - "Poverty and Social Protection": [ - "Child Poverty and Material Deprivation", - "Social protection system", - ], - "Child Rights Landscape and Governance": [ - "Demographics", - "Political Economy", - "Migration and Displacement", - "Access to Justice", - "Data on Children", - "Public spending on Children", - ], - "Participation and Civil Rights": [ - "Birth registration and identity", - "Information, Internet and Protection of privacy", - "Education, Leisure, and Culture", - ], -} - - -def get_search_countries(add_all): - all_countries = {"label": "All", "value": "All"} - countries_list = [ - { - "label": key, - "value": countries_iso3_dict[key], - } - for key in countries_iso3_dict.keys() - ] - if add_all: - countries_list.insert(0, all_countries) - return countries_list - - -def get_sector(subtopic): - for key in dict_topics_subtopics.keys(): - if subtopic.strip() in dict_topics_subtopics.get(key): - return key - return "" - - -# function to check if the config of a certain indicator are only about its dtype -def only_dtype(config): - return list(config.keys()) == ["DTYPE"] - - -def get_filtered_dataset( - indicators: list, - years: list, - country_codes: list, - breakdown: str = "TOTAL", # send default breakdown as Total - dimensions: dict = {}, - latest_data: bool = True, -) -> pd.DataFrame: - - # TODO: This is temporary, need to move to config - # Add all dimensions by default to the keys - keys = { - "REF_AREA": country_codes, - "INDICATOR": indicators, - "SEX": [], - "AGE": [], - "RESIDENCE": [], - "WEALTH_QUINTILE": [], - } - - # get the first indicator of the list... we have more than one indicator in the cards - indicator_config = ( - indicators_config[indicators[0]] if indicators[0] in indicators_config else {} - ) - # check if the indicator has special config, update the keys from the config - if indicator_config and not only_dtype(indicator_config): - # TODO: need to confirm that a TOTAL is always available when a config is available for the indicator - card_keys = indicator_config[breakdown] - if ( - dimensions - ): # if we are sending cards related filters, update the keys with the set values - card_keys.update(dimensions) - keys.update(card_keys) # update the keys with the sent values - - try: - data = unicef.data( - "TRANSMONEE", - provider="ECARO", - key=keys, - params=dict( - startPeriod=years[0], - endPeriod=years[-1], - lastNObservations=1 if latest_data else 0, - ), - dsd=dsd, - ) - logging.debug(f"URL: {data.response.url} CACHED: {data.response.from_cache}") - except HTTPError as e: - logging.exception(f"URL: {e.response}", e) - # TODO: Maybe do something better here - return pd.DataFrame() - - # lbassil: add sorting by Year to display the years in proper order on the x-axis - dtype = ( - eval(indicator_config["DTYPE"]) if "DTYPE" in indicator_config else np.float64 - ) - data = ( - data.to_pandas(attributes="o", rtype="rows", dtype=dtype) - .sort_values(by=["TIME_PERIOD"]) - .reset_index() - ) - data.rename(columns={"value": "OBS_VALUE", "INDICATOR": "CODE"}, inplace=True) - # replace Yes by 1 and No by 0 - data.OBS_VALUE.replace({"Yes": "1", "No": "0", "<": "", ">": ""}, inplace=True) - - # convert to numeric and round - data["OBS_VALUE"] = pd.to_numeric(data.OBS_VALUE, errors="coerce") - data.dropna(subset=["OBS_VALUE"], inplace=True) - data = data.round({"OBS_VALUE": 2}) - # converting TIME_PERIOD to numeric: we should get integers by default - data["TIME_PERIOD"] = pd.to_numeric(data.TIME_PERIOD) - - # lbassil: add the code to fill the country names - countries_val_list = list(countries_iso3_dict.values()) - - def create_labels(row): - row["Country_name"] = countries[countries_val_list.index(row["REF_AREA"])] - row["Unit_name"] = str(units_names.get(str(row["UNIT_MEASURE"]), "")) - row["Sex_name"] = str(gender_names.get(str(row["SEX"]), "")) - row["Residence_name"] = str(residence_names.get(str(row["RESIDENCE"]), "")) - row["Wealth_name"] = str(wealth_names.get(str(row["WEALTH_QUINTILE"]), "")) - row["Age_name"] = str(age_groups_names.get(str(row["AGE"]), "")) - return row - - data = data.apply(create_labels, axis="columns") - - return data - - -# create two dicts, one for display tree and one with the index of all possible selections -selection_index = collections.OrderedDict({"0": countries}) -selection_tree = dict(title="Select All", key="0", children=[]) -for num1, group in enumerate(country_selections): - parent = dict(title=group["label"], key=f"0-{num1}", children=[]) - group_countries = [] - - for num2, region in enumerate(group["value"]): - child_region = dict( - title=region["label"] if "label" in region else region, - key=f"0-{num1}-{num2}", - children=[], - ) - parent.get("children").append(child_region) - if "value" in region: - selection_index[f"0-{num1}-{num2}"] = ( - region["value"] - if isinstance(region["value"], list) - else [region["value"]] - ) - for num3, country in enumerate(region["value"]): - child_country = dict(title=country, key=f"0-{num1}-{num2}-{num3}") - if len(region["value"]) > 1: - # only create child nodes for more then one child - child_region.get("children").append(child_country) - selection_index[f"0-{num1}-{num2}-{num3}"] = [country] - group_countries.append(country) - else: - selection_index[f"0-{num1}-{num2}"] = [region] - group_countries.append(region) - - selection_index[f"0-{num1}"] = group_countries - selection_tree.get("children").append(parent) - -programme_country_indexes = [ - next( - key - for key, value in selection_index.items() - if value[0] == item and len(value) == 1 - ) - for item in unicef_country_prog -] - -data = pd.DataFrame() -data_query_inds = set(data_query_codes) - -# column data types coerced -col_types = { - "COVERAGE_TIME": str, - "OBS_FOOTNOTE": str, - "OBS_VALUE": str, - "Frequency": str, - "Unit multiplier": str, - "OBS_STATUS": str, - "Observation Status": str, - "TIME_PERIOD": int, -} - -# avoid a loop to query SDMX -try: - data_query_sdmx = pd.read_csv( - sdmx_url.format("+".join(data_query_inds), years[0], years[-1]), - dtype=col_types, - storage_options={"Accept-Encoding": "gzip"}, - low_memory=False, - ) -except urllib.error.HTTPError as e: - raise e - -data = data.append(data_query_sdmx) -# no need to create column CODE, just rename indicator -data.rename(columns={"INDICATOR": "CODE"}, inplace=True) - -# replace Yes by 1 and No by 0 -data.OBS_VALUE.replace({"Yes": "1", "No": "0"}, inplace=True) - - -# check and drop non-numeric observations, eg: SDMX accepts > 95 as an OBS_VALUE -filter_non_num = pd.to_numeric(data.OBS_VALUE, errors="coerce").isnull() -if filter_non_num.any(): - not_num_code_val = data[["CODE", "OBS_VALUE"]][filter_non_num] - f"Non-numeric observations in {not_num_code_val.CODE.unique()}\ndiscarded: {not_num_code_val.OBS_VALUE.unique()}" - data.drop(data[filter_non_num].index, inplace=True) - -# convert to numeric -data["OBS_VALUE"] = pd.to_numeric(data.OBS_VALUE) -data = data.round({"OBS_VALUE": 2}) -# print(data.shape) - -# TODO: calculations for children age population -indicators = data["Indicator"].unique() - -# extract the indicators that have gender/sex disaggregation -gender_indicators = data.groupby("CODE").agg({"SEX": "nunique"}).reset_index() -# Keep only indicators with gender/sex disaggregation -gender_indicators = gender_indicators[gender_indicators["SEX"] > 1] - -# path to excel data dictionary in repo -github_url = "https://github.com/UNICEFECAR/data-etl/raw/proto_API/tmee/data_in/data_dictionary/indicator_dictionary_TM_v8.xlsx" -data_dict_content = requests.get(github_url).content -# Reading the downloaded content and turning it into a pandas dataframe and read Snapshot sheet from excel data-dictionary -snapshot_df = pd.read_excel(BytesIO(data_dict_content), sheet_name="Snapshot") -snapshot_df.dropna(subset=["Source_name"], inplace=True) -snapshot_df["Source"] = snapshot_df["Source_name"].apply(lambda x: x.split(":")[0]) -# read indicators table from excel data-dictionary -df_topics_subtopics = pd.read_excel(BytesIO(data_dict_content), sheet_name="Indicator") -df_topics_subtopics.dropna(subset=["Issue"], inplace=True) -df_sources = pd.merge(df_topics_subtopics, snapshot_df, how="outer", on=["Code"]) -# assign source = TMEE to all indicators without a source since they all come from excel data collection files -df_sources.fillna("TMEE", inplace=True) -# Concatenate sectors/subtopics dictionary value lists (mapping str lower) -sitan_subtopics = list(map(str.lower, sum(dict_topics_subtopics.values(), []))) +import urllib +from ..sdmx_utils import utils -df_sources.rename( - columns={ - "Name_x": "Indicator", - "Issue": "Subdomain", - }, - inplace=True, -) -# filter the sources to keep only sitan related sectors and sub-topics -df_sources["Subdomain"] = df_sources["Subdomain"].str.strip() -df_sources["Domain"] = df_sources["Subdomain"].apply( - lambda x: get_sector(x) if not pd.isna(x) else "" -) -df_sources["Source_Full"] = df_sources["Source"].apply( - lambda x: data_sources[x] if not pd.isna(x) else "" -) +years = list(range(2010, datetime.datetime.now().year)) -df_sources = df_sources[df_sources["Subdomain"].str.lower().isin(sitan_subtopics)] -# read source table from excel data-dictionary and merge -source_table_df = pd.read_excel(BytesIO(data_dict_content), sheet_name="Source") -df_sources = df_sources.merge( - source_table_df[["Source_Id", "Source_Link"]], - on="Source_Id", - how="left", - sort=False, -) -# assign source link for TMEE, url: UNICEF_RDM/indicator_code -tmee_source_link = df_sources.Source_Link.isnull() -unicef_rdm_url = "https://data.unicef.org/indicator-profile/{helix_code}/" -df_sources.loc[tmee_source_link, "Source_Link"] = df_sources[ - tmee_source_link -].Code.apply(lambda x: unicef_rdm_url.format(helix_code=x)) -df_sources_groups = df_sources.groupby("Source") -df_sources_summary_groups = df_sources.groupby("Source_Full") -# Extract the indicators' potential unique disaggregations. -# Group by indicator code and keep only unique aggregations for the 4 possible dimensions: -# Sex, Age, Residence and Wealth. -indicators_disagg = ( - data.groupby("CODE") - .agg( - { - "AGE": "nunique", - "SEX": "nunique", - "RESIDENCE": "nunique", - "WEALTH_QUINTILE": "nunique", - } - ) - .reset_index() -) -# Filter the dimensions with count greater than 1 which means Total is there (default) in addition to other possible values. -indicators_disagg_no_total = indicators_disagg[ - (indicators_disagg["AGE"] > 1) - | (indicators_disagg["SEX"] > 1) - | (indicators_disagg["RESIDENCE"] > 1) - | (indicators_disagg["WEALTH_QUINTILE"] > 1) -] +sdmx_base_url = "https://sdmx.data.unicef.org/ws/public/sdmxapi/rest/" +sdmx_cl_url = sdmx_base_url + "codelist/{}/{}/{}/" -# include the indicators with Total only to show in the data query -indicators_disagg_with_total = indicators_disagg[ - (indicators_disagg["AGE"] >= 1) - | (indicators_disagg["SEX"] >= 1) - | (indicators_disagg["RESIDENCE"] >= 1) - | (indicators_disagg["WEALTH_QUINTILE"] >= 1) -] -# Get the data for all the indicators having disaggregated data by any of the 4 dimensions. -indicators_disagg_details = data[ - data["CODE"].isin(indicators_disagg_with_total["CODE"]) -] +url_ref_areas = sdmx_cl_url.format("BRAZIL_CO", "CL_BRAZIL_REF_AREAS", "latest") +cl_ref_areas = utils.get_codelist(sdmx_cl_url.format("BRAZIL_CO", "CL_BRAZIL_REF_AREAS", "latest")) -# Filter the dataframe to be used in the data query to keep indicators code and the possible disaggregations. -indicators_disagg_details = indicators_disagg_details[ - ["CODE", "Age", "Sex", "Residence", "Wealth Quintile"] -] -indicators_disagg_details = indicators_disagg_details.drop_duplicates() -# extract the indicators that have gender/sex disaggregation -age_indicators_counts = data.groupby("CODE").agg({"AGE": "nunique"}).reset_index() -# Keep only indicators with gender/sex disaggregation -age_indicators_counts = age_indicators_counts[age_indicators_counts["AGE"] > 1] -# age_indicators_counts.to_csv("age_indicators_counts.csv", index=True) -age_indicators = pd.merge(data, age_indicators_counts, on=["CODE"]) -age_indicators = age_indicators[["CODE", "Indicator", "Age"]] -age_indicators = age_indicators.drop_duplicates() -age_indicators = age_indicators.sort_values(by=["CODE", "Age"]) -# age_indicators.to_csv("age_indicators.csv", index=False) +sel_ref_areas = [x["id"] for x in cl_ref_areas] +selection_tree = dict(title="Select All", key="0", children=[{"title":x["name"], "key":x["id"]} for x in cl_ref_areas]) -# extract the indicators that have gender/sex disaggregation -age_indicators_counts = data.groupby("CODE").agg({"AGE": "nunique"}).reset_index() -# Keep only indicators with gender/sex disaggregation -age_indicators_counts = age_indicators_counts[age_indicators_counts["AGE"] > 1] -# age_indicators_counts.to_csv("age_indicators_counts.csv", index=True) -age_indicators = pd.merge(data, age_indicators_counts, on=["CODE"]) -age_indicators = age_indicators[["CODE", "Indicator", "Age"]] -age_indicators = age_indicators.drop_duplicates() -age_indicators = age_indicators.sort_values(by=["CODE", "Age"]) -# age_indicators.to_csv("age_indicators.csv", index=False) +# data = pd.DataFrame() +# data_query_inds = set(["REPROV_EFFINAL_MUNICIPAL"]) def page_not_found(pathname): - return html.P("No page '{}'".format(pathname)) + return html.P("No page '{}'".format(pathname)) \ No newline at end of file diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/__init__OLD.py b/transmonee_dashboard/src/transmonee_dashboard/pages/__init__OLD.py new file mode 100644 index 00000000..49bf0b40 --- /dev/null +++ b/transmonee_dashboard/src/transmonee_dashboard/pages/__init__OLD.py @@ -0,0 +1,123 @@ +import dash_html_components as html +import datetime +import collections +import pandas as pd +import urllib +from ..sdmx_utils import utils + +years = list(range(2010, datetime.datetime.now().year)) + +sdmx_base_url = "https://sdmx.data.unicef.org/ws/public/sdmxapi/rest/" +sdmx_cl_url = sdmx_base_url + "codelist/{}/{}/{}/" + +url_countries = sdmx_cl_url.format("BRAZIL_CO", "CL_BRAZIL_REF_AREAS", "latest") +cl_countries = utils.get_codelist(sdmx_cl_url.format("BRAZIL_CO", "CL_BRAZIL_REF_AREAS", "latest")) + +# do we need this? Can we reshape the dictionary in get_search_countries? +countries_iso3_dict = {c["name"]: c["id"] for c in cl_countries} + + +def get_search_countries(add_all): + all_countries = {"label": "All", "value": "All"} + countries_list = [ + { + "label": key, + "value": countries_iso3_dict[key], + } + for key in countries_iso3_dict.keys() + ] + if add_all: + countries_list.insert(0, all_countries) + return countries_list + + +# create a list of country names in the same order as the countries_iso3_dict +countries = list(countries_iso3_dict.keys()) + +# Hierarchical cl? +# country_selections = [ +# { +# "label": "Eastern Europe and Central Asia", +# "value": [x["name"] for x in cl_countries] +# } +# ] + +# print(country_selections) + +selection_index = collections.OrderedDict({"0": countries}) +selection_tree = dict(title="Select All", key="0", children=[{"title":x["name"], "key":x["id"]} for x in cl_countries]) + +# {"title": "Subchild1", "key": "0-0-1"}, + +#max 3 levels? Can we make it recursive? +# for num1, group in enumerate(country_selections): +# parent = dict(title=group["label"], key=f"0-{num1}", children=[]) +# group_countries = [] +# +# for num2, region in enumerate(group["value"]): +# child_region = dict( +# title=region["label"] if "label" in region else region, +# key=f"0-{num1}-{num2}", +# children=[], +# ) +# parent.get("children").append(child_region) +# if "value" in region: +# selection_index[f"0-{num1}-{num2}"] = ( +# region["value"] +# if isinstance(region["value"], list) +# else [region["value"]] +# ) +# for num3, country in enumerate(region["value"]): +# child_country = dict(title=country, key=f"0-{num1}-{num2}-{num3}") +# if len(region["value"]) > 1: +# # only create child nodes for more then one child +# child_region.get("children").append(child_country) +# selection_index[f"0-{num1}-{num2}-{num3}"] = [country] +# group_countries.append(country) +# else: +# selection_index[f"0-{num1}-{num2}"] = [region] +# group_countries.append(region) +# +# selection_index[f"0-{num1}"] = group_countries +# selection_tree.get("children").append(parent) + +data = pd.DataFrame() +data_query_inds = set(["REPROV_EFFINAL_MUNICIPAL"]) + +# column data types coerced +''' +col_types = { + "COVERAGE_TIME": str, + "OBS_FOOTNOTE": str, + "OBS_VALUE": str, + "Frequency": str, + "Unit multiplier": str, + "OBS_STATUS": str, + "Observation Status": str, + "TIME_PERIOD": int, +} +''' + +# avoid a loop to query SDMX +sdmx_url = "https://sdmx.data.unicef.org/ws/public/sdmxapi/rest/data/BRAZIL_CO,BRAZIL_CO,1.0/.{}..?format=csv&startPeriod={}&endPeriod={}" +to_call = sdmx_url.format("+".join(data_query_inds), years[0], years[-1]) + +try: + data_query_sdmx = pd.read_csv(to_call, + # dtype=col_types, + storage_options={"Accept-Encoding": "gzip"}, + low_memory=False, + ) +except urllib.error.HTTPError as e: + raise e + +data = data.append(data_query_sdmx) +# no need to create column CODE, just rename indicator +data.rename(columns={"INDICATOR": "CODE"}, inplace=True) + +print(data.head()) + +indicators = data["Indicator"].unique() + +def page_not_found(pathname): + return html.P("No page '{}'".format(pathname)) diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/base_page.py b/transmonee_dashboard/src/transmonee_dashboard/pages/base_page.py index 4f7d9bfc..c7e4b492 100644 --- a/transmonee_dashboard/src/transmonee_dashboard/pages/base_page.py +++ b/transmonee_dashboard/src/transmonee_dashboard/pages/base_page.py @@ -1,5 +1,3 @@ -import textwrap - import dash import dash_bootstrap_components as dbc import dash_core_components as dcc @@ -14,21 +12,12 @@ from ..app import app from ..components import fa + from . import ( - countries, - countries_iso3_dict, - df_sources, - dimension_names, - geo_json_countries, - get_filtered_dataset, - indicator_names, - indicators_config, - programme_country_indexes, - selection_index, - selection_tree, - unicef_country_prog, years, - get_search_countries, + cl_ref_areas, + sel_ref_areas, + selection_tree ) # set defaults @@ -47,14 +36,6 @@ "danger", ] AREA_KEYS = ["MAIN", "AREA_1", "AREA_2", "AREA_3", "AREA_4", "AREA_5", "AREA_6"] -DEFAULT_LABELS = { - "Country_name": "Country", - "TIME_PERIOD": "Year", - "Sex_name": "Sex", - "Residence_name": "Residence", - "Age_name": "Age", - "Wealth_name": "Wealth Quintile", -} CARD_TEXT_STYLE = {"textAlign": "center", "color": "#0074D9"} EMPTY_CHART = { @@ -74,121 +55,13 @@ } -def make_area(area_name): - area_id = {"type": "area", "index": area_name} - popover_id = {"type": "area_sources", "index": area_name} - historical_data_style = {"display": "none"} - exclude_outliers_style = {"paddingLeft": 20, "display": "block"} - breakdowns_style = {"display": "block"} - - # lbassil: still differentiating main area id from other areas ids because the call backs are still not unified - if area_name == "MAIN": - area_id = f"{area_name.lower()}_area" - popover_id = f"{area_name.lower()}_area_sources" - historical_data_style = {"display": "block"} - exclude_outliers_style = {"display": "none"} - breakdowns_style = {"display": "none"} - - # lbassil: unifying both main and figure area generations by tweaking the ids and styles - area = dbc.Card( - [ - dbc.CardHeader( - id={"type": "area_title", "index": area_name}, - style={"fontWeight": "bold"}, - ), - dbc.CardBody( - [ - dcc.Dropdown( - id={"type": "area_options", "index": area_name}, - className="dcc_control", - ), - html.Br(), - dbc.Checklist( - options=[ - { - "label": "Show historical data", - "value": 1, - } - ], - value=[], - id={ - "type": "historical_data_toggle", - "index": area_name, - }, - switch=True, - style=historical_data_style, - ), - html.Br(), - dbc.RadioItems( - id={"type": "area_types", "index": area_name}, - inline=True, - ), - dcc.Loading([dcc.Graph(id=area_id)]), - dbc.Checklist( - options=[ - { - "label": "Exclude outliers ", - "value": 1, - } - ], - value=[1], - id={ - "type": "exclude_outliers_toggle", - "index": area_name, - }, - switch=True, - style=exclude_outliers_style, - ), - html.Br(), - dbc.RadioItems( - id={"type": "area_breakdowns", "index": area_name}, - inline=True, - style=breakdowns_style, - ), - html.Div( - fa("fas fa-info-circle"), - id=f"{area_name.lower()}_area_info", - className="float-right", - ), - dbc.Popover( - [ - dbc.PopoverHeader("Sources"), - dbc.PopoverBody(id=popover_id), - ], - id="hover", - target=f"{area_name.lower()}_area_info", - trigger="hover", - ), - ] - ), - ], - id={"type": "area_parent", "index": area_name}, - ) - return area - - def get_base_layout(**kwargs): indicators_dict = kwargs.get("indicators") main_title = kwargs.get("main_title") - is_country_profile = kwargs.get("is_country_profile") - country_dropdown_style = {"display": "none"} + themes_row_style = {"verticalAlign": "center", "display": "flex"} + country_dropdown_style = {"display": "none"} countries_filter_style = {"display": "block"} - programme_toggle_style = {"display": "block"} - main_area_style = {"display": "block"} - - if is_country_profile: - country_dropdown_style = { - "minWidth": 400, - "maxWidth": 600, - "paddingRight": 4, - "verticalAlign": "center", - "display": "visible", - } - themes_row_style = {"display": "none"} - countries_filter_style = {"display": "none"} - programme_toggle_style = {"display": "none"} - main_area_style = {"display": "none"} return html.Div( [ @@ -217,880 +90,222 @@ def get_base_layout(**kwargs): ], ), ], - ) - ], - ), - dbc.Row( - children=[ - dbc.Col( - [ - dbc.Row( - [ - dbc.ButtonGroup( - id="themes", - ), - ], - id="theme-row", - # width=4, - className="my-2", - # no_gutters=True, - justify="center", - style=themes_row_style, - ), - dbc.Row( + ), + + dbc.Row( + children=[ + dbc.Col( [ - dbc.DropdownMenu( - label=f"Years: {years[0]} - {years[-1]}", - id="collapse-years-button", - className="m-2", - color="info", - # block=True, - children=[ - dbc.Card( - dcc.RangeSlider( - id="year_slider", - min=0, - max=len(years) - 1, - step=None, - marks={ - index: str(year) - for index, year in enumerate( - years - ) - }, - value=[0, len(years) - 1], - ), - style={ - "maxHeight": "250px", - "minWidth": "500px", - }, - className="overflow-auto", - body=True, - ), - ], - ), - dcc.Dropdown( - id="country_profile_selector", - options=get_search_countries(False), - value="ALB", - multi=False, - placeholder="Select a country...", - className="m-2", - style=country_dropdown_style, - ), - dbc.DropdownMenu( - label=f"Countries: {len(countries)}", - id="collapse-countries-button", - className="m-2", - color="info", - style=countries_filter_style, - children=[ - dbc.Card( - dash_treeview_antd.TreeView( - id="country_selector", - multiple=True, - checkable=True, - checked=["0"], - # selected=[], - expanded=["0"], - data=selection_tree, - ), - style={ - "maxHeight": "250px", - # "maxWidth": "300px", - }, - className="overflow-auto", - body=True, + dbc.Row( + [ + dbc.ButtonGroup( + id="themes", ), ], + id="theme-row", + # width=4, + className="my-2", + # no_gutters=True, + justify="center", + style=themes_row_style, ), - dbc.FormGroup( + dbc.Row( [ - dbc.Checkbox( - id="programme-toggle", - className="custom-control-input", + dbc.DropdownMenu( + label=f"Years: {years[0]} - {years[-1]}", + id="collapse-years-button", + className="m-2", + color="info", + # block=True, + children=[ + dbc.Card( + dcc.RangeSlider( + id="year_slider", + min=0, + max=len(years) - 1, + step=None, + marks={ + index: str(year) + for index, year in enumerate( + years + ) + }, + value=[0, len(years) - 1], + ), + style={ + "maxHeight": "250px", + "minWidth": "500px", + }, + className="overflow-auto", + body=True, + ), + ], ), - dbc.Label( - "UNICEF Country Programmes", - html_for="programme-toggle", - className="custom-control-label", - color="primary", + + # dcc.Dropdown( + # id="country_profile_selector", + # options=get_search_countries(False), + # value="ALB", + # multi=False, + # placeholder="Select a country...", + # className="m-2", + # style=country_dropdown_style, + # ), + dbc.DropdownMenu( + label=f"Countries: {len(sel_ref_areas)}", + id="collapse-countries-button", + className="m-2", + color="info", + style=countries_filter_style, + children=[ + dbc.Card( + dash_treeview_antd.TreeView( + id="country_selector", + multiple=True, + checkable=True, + checked=["0"], + # selected=[], + expanded=["0"], + data=selection_tree, + ), + style={ + "maxHeight": "250px", + # "maxWidth": "300px", + }, + className="overflow-auto", + body=True, + ), + ], ), + # dbc.FormGroup( + # [ + # dbc.Checkbox( + # id="programme-toggle", + # className="custom-control-input", + # ), + # dbc.Label( + # "UNICEF Country Programmes", + # html_for="programme-toggle", + # className="custom-control-label", + # color="primary", + # ), + # ], + # className="custom-control custom-switch m-2", + # check=True, + # inline=True, + # style=programme_toggle_style, + # ), ], - className="custom-control custom-switch m-2", - check=True, - inline=True, - style=programme_toggle_style, + id="filter-row", + no_gutters=True, + justify="center", ), - ], - id="filter-row", - no_gutters=True, - justify="center", + ] ), - ] - ), - ], - # sticky="top", - className="sticky-top bg-light", - ), - dbc.Row( - [ - dbc.CardDeck( - id="cards_row", - className="mt-3", - ), - ], - justify="center", - ), - html.Br(), - dbc.CardDeck( - [make_area(area) for area in ["MAIN"]], - style=main_area_style, - ), - html.Br(), - dbc.CardDeck( - [make_area(area) for area in ["AREA_1", "AREA_2"]], - ), - html.Br(), - dbc.CardDeck( - [make_area(area) for area in ["AREA_3", "AREA_4"]], - ), - html.Br(), - dbc.CardDeck( - [make_area(area) for area in ["AREA_5", "AREA_6"]], - ), - html.Br(), - ], - ) - - -def make_card( - card_id, - name, - suffix, - indicator_sources, - source_link, - indicator_header, - numerator_pairs, -): - card = dbc.Card( - [ - dbc.CardBody( - [ - html.H1( - indicator_header, - className="display-4", - style={ - # "fontSize": 50, - "textAlign": "center", - "color": "#1cabe2", - }, - ), - html.H4(suffix, className="card-title"), - html.P(name, className="lead"), - html.Div( - fa("fas fa-info-circle"), - id=f"{card_id}_info", - # className="float-right", - style={ - "position": "absolute", - "bottom": "10px", - "right": "10px", - }, - ), - ], - style={ - # "fontSize": 50, - "textAlign": "center", - }, - ), - dbc.Popover( - [ - dbc.PopoverHeader( - html.A( - html.P(f"Sources: {indicator_sources}"), - href=source_link, - target="_blank", - ) + ], + # sticky="top", + className="sticky-top bg-light", ), - dbc.PopoverBody( - dcc.Markdown(get_card_popover_body(numerator_pairs)) + dbc.Row( + [ + dbc.CardDeck( + id="cards_row", + className="mt-3", + ), + ], + justify="center", ), + html.Br(), ], - id="hover", - target=f"{card_id}_info", - trigger="hover", ), - ], - color="primary", - outline=True, - id=card_id, + ] ) - return card - -def get_card_popover_body(sources): - """This function is used to generate the list of countries that are part of the card's - displayed result; it displays the countries as a list, each on a separate line - - Args: - sources (_type_): _description_ - - Returns: - _type_: _description_ - """ - countries = [] - # lbassil: added this condition to stop the exception when sources is empty - if len(sources) > 0: - for index, source_info in sources.sort_values(by="OBS_VALUE").iterrows(): - countries.append(f"- {index[0]}, {source_info[0]} ({index[1]})") - card_countries = "\n".join(countries) - return card_countries - else: - return "NA" - - -# TODO: Move to client side call back @app.callback( - Output("collapse-years", "is_open"), - Output("collapse-countries", "is_open"), - Output("collapse-engagements", "is_open"), - [ - Input("collapse-years-button", "n_clicks"), - Input("collapse-countries-button", "n_clicks"), - Input("collapse-engagements-button", "n_clicks"), - ], - [ - State("collapse-years-button", "is_open"), - State("collapse-countries-button", "is_open"), - State("collapse-engagements-button", "is_open"), - ], -) -def toggle_collapse(n1, n2, n3, is_open1, is_open2, is_open3): - ctx = dash.callback_context - - if not ctx.triggered: - return False, False, False - else: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - - if button_id == "collapse-years-button" and n1: - return not is_open1, False, False - elif button_id == "collapse-countries-button" and n2: - return False, not is_open2, False - elif button_id == "collapse-engagements-button" and n3: - return False, False, not is_open3 - return False, False, False - - -@app.callback( - Output({"type": "area_parent", "index": MATCH}, "hidden"), - Input("theme", "hash"), - [ - State("indicators", "data"), - State({"type": "area_parent", "index": MATCH}, "id"), - ], + Output() ) -def display_areas(theme, indicators_dict, id): - area = id["index"] - theme = theme[1:].upper() if theme else next(iter(indicators_dict.keys())) - return area not in indicators_dict[theme] @app.callback( Output("store", "data"), Output("country_selector", "checked"), - Output("programme-toggle", "checked"), Output("collapse-years-button", "label"), Output("collapse-countries-button", "label"), [ Input("theme", "hash"), Input("year_slider", "value"), Input("country_selector", "checked"), - Input("programme-toggle", "checked"), - Input("country_profile_selector", "value"), ], State("indicators", "data"), ) def apply_filters( - theme, - years_slider, - country_selector, - programme_toggle, - selected_country, - indicators, + theme, + years_slider, + country_selector, + indicators, ): ctx = dash.callback_context selected = ctx.triggered[0]["prop_id"].split(".")[0] - countries_selected = set() + print("selected") + print(selected) + print("selected end") + # countries_selected = set() current_theme = theme[1:].upper() if theme else next(iter(indicators.keys())) - # check if it is the country profile page - is_country_profile = current_theme == "COUNTRYPROFILE" - # check if the user clicked on the generate button in the country profile page - if is_country_profile: - key_list = list(countries_iso3_dict.keys()) - val_list = list(countries_iso3_dict.values()) - # get the name of the selected country in the dropdown to filter the data accordingly - countries_selected = ( - [key_list[val_list.index(selected_country)]] if selected_country else [] - ) - elif programme_toggle and selected == "programme-toggle": - countries_selected = unicef_country_prog - country_selector = programme_country_indexes - # Add the condition to know when the user unchecks the UNICEF country programs! - elif not country_selector or ( - not programme_toggle and selected == "programme-toggle" - ): - countries_selected = countries - # Add this to check all the items in the selection tree - country_selector = ["0"] - else: - for index in country_selector: - countries_selected.update(selection_index[index]) - if countries_selected == countries: - # if all countries are all selected then stop - break - countries_selected = list(countries_selected) - country_text = f"{len(countries_selected)} Selected" + + # if selected=="country_selector": + # if + + print("country_selector") + print(country_selector) + print("country_selector END") + + # for index in country_selector: + # + # # print(index) + # print(index) + # print(country_selector[index]) + # countries_selected.update(selection_index[index]) + # if countries_selected == countries: + # if all countries are all selected then stop + # break + + + # countries_selected = list(countries_selected) + country_text = f"{len(country_selector)} Selected" # need to include the last selected year as it was exluded in the previous method - selected_years = years[years_slider[0] : years_slider[1] + 1] + selected_years = years[years_slider[0]: years_slider[1] + 1] # selected_years = years[slice(*years_slider)] # Use the dictionary to return the values of the selected countries based on the SDMX ISO3 codes - countries_selected_codes = [ - countries_iso3_dict[country] for country in countries_selected - ] + # countries_selected_codes = [ + # countries_iso3_dict[country] for country in countries_selected + # ] + + # print("countries_selected") + # print(countries_selected) + print("country_text") + print(country_text) + print("selected_years") + print(selected_years) + print("countries_selected_codes") + # print(countries_selected_codes) selections = dict( theme=current_theme, indicators_dict=indicators, years=selected_years, - countries=countries_selected_codes, - is_adolescent=("ADOLESCENT" in indicators), + # countries=countries_selected_codes, + countries=[] + # is_adolescent=("ADOLESCENT" in indicators), ) + print("SELECTION") + print(selections) + return ( selections, country_selector, - countries_selected == unicef_country_prog, + # countries_selected == unicef_country_prog, f"Years: {selected_years[0]} - {selected_years[-1]}", "Countries: {}".format(country_text), ) - - -def indicator_card( - selections, - card_id, - name, - numerator, - suffix, - denominator=None, - absolute=False, - average=False, - min_max=False, - sex_code=None, - age_group=None, -): - indicators = numerator.split(",") - - # TODO: Change to use albertos config - # lbassil: had to change this to cater for 2 dimensions set to the indicator card like age and sex - breakdown = "TOTAL" - # define the empty dimensions dict to be filled based on the card data filters - dimensions = {} - if age_group is not None: - dimensions["AGE"] = [age_group] - if sex_code is not None: - dimensions["SEX"] = [sex_code] - - filtered_data = get_filtered_dataset( - indicators, - selections["years"], - selections["countries"], - breakdown, - dimensions, - latest_data=True, - ) - - df_indicator_sources = df_sources[df_sources["Code"].isin(indicators)] - unique_indicator_sources = df_indicator_sources["Source_Full"].unique() - indicator_sources = ( - "; ".join(list(unique_indicator_sources)) - if len(unique_indicator_sources) > 0 - else "" - ) - source_link = ( - df_indicator_sources["Source_Link"].unique()[0] - if len(unique_indicator_sources) > 0 - else "" - ) - # lbassil: add this check because we are getting an exception where there is no data; i.e. no totals for all dimensions mostly age for the selected indicator - if filtered_data.empty: - indicator_header = "No data" - indicator_sources = "NA" - numerator_pairs = [] - return make_card( - card_id, - name, - indicator_header, - indicator_sources, - source_link, - numerator_pairs, - ) - - # select last value for each country - indicator_values = ( - filtered_data.groupby( - [ - "Country_name", - "TIME_PERIOD", - ] - ).agg({"OBS_VALUE": "sum", "CODE": "count"}) - ).reset_index() - - numerator_pairs = ( - indicator_values[indicator_values.CODE == len(indicators)] - .groupby("Country_name", as_index=False) - .last() - .set_index(["Country_name", "TIME_PERIOD"]) - ) - - if suffix.lower() == "countries": - # this is a hack to accomodate small cases (to discuss with James) - if "FREE" in numerator: - # trick to filter number of years of free education - indicator_sum = (numerator_pairs.OBS_VALUE >= 1).to_numpy().sum() - sources = numerator_pairs.index.tolist() - numerator_pairs = numerator_pairs[numerator_pairs.OBS_VALUE >= 1] - else: - # trick to accomodate cards for admin exams (AND for boolean indicators) - # filter exams according to number of indicators - indicator_sum = ( - (numerator_pairs.OBS_VALUE == len(indicators)).to_numpy().sum() - ) - sources = numerator_pairs.index.tolist() - - else: - indicator_sum = numerator_pairs["OBS_VALUE"].to_numpy().sum() - sources = numerator_pairs.index.tolist() - if average and len(sources) > 1: - indicator_sum = indicator_sum / len(sources) - - # define indicator header text: the resultant number except for the min-max range - if min_max and len(sources) > 1: - indicator_min = "{:,.1f}".format(numerator_pairs["OBS_VALUE"].min()) - indicator_max = "{:,.1f}".format(numerator_pairs["OBS_VALUE"].max()) - indicator_header = f"[{indicator_min} - {indicator_max}]" - else: - indicator_header = "{:,.0f}".format(indicator_sum) - - return make_card( - card_id, - name, - suffix, - indicator_sources, - source_link, - indicator_header, - numerator_pairs, - ) - - -@app.callback( - Output("cards_row", "children"), - [ - Input("store", "data"), - ], - [State("cards_row", "children"), State("indicators", "data")], -) -def show_cards(selections, current_cards, indicators_dict): - cards = [ - indicator_card( - selections, - f"card-{num}", - card["name"], - card["indicator"], - card["suffix"], - card.get("denominator"), - card.get("absolute"), - card.get("average"), - card.get("min_max"), - card.get("sex"), - card.get("age"), - ) - for num, card in enumerate(indicators_dict[selections["theme"]]["CARDS"]) - ] - return cards - - -@app.callback( - Output("subtitle", "children"), - Output("themes", "children"), - [ - Input("store", "data"), - Input("country_profile_selector", "value"), - ], - State("indicators", "data"), -) -def show_themes(selections, selected_country, indicators_dict): - # check if it is the country profile page - is_country_profile = selections["theme"] == "COUNTRYPROFILE" - ctx = dash.callback_context - ctrl_id = ctx.triggered[0]["prop_id"].split(".")[0] - # check if the countries dropdown is causing this callback in order to set the sub-title to the country's name - if is_country_profile or ctrl_id == "countries": - key_list = list(countries_iso3_dict.keys()) - val_list = list(countries_iso3_dict.values()) - subtitle = ( - key_list[val_list.index(selected_country)] - if selected_country - else "Country Name" - ) - return subtitle, [] - - subtitle = indicators_dict[selections["theme"]].get("NAME") - url_hash = "#{}".format((next(iter(selections.items())))[1].lower()) - # hide the buttons when only one option is available - if len(indicators_dict.items()) == 1: - return subtitle, [] - buttons = [ - dbc.Button( - value["NAME"], - id=key, - color=colours[num], - className="theme mx-1", - href=f"#{key.lower()}", - active=url_hash == f"#{key.lower()}", - ) - for num, (key, value) in enumerate(indicators_dict.items()) - ] - return subtitle, buttons - - -@app.callback( - Output({"type": "area_title", "index": MATCH}, "children"), - Output({"type": "area_options", "index": MATCH}, "options"), - Output({"type": "area_types", "index": MATCH}, "options"), - Output({"type": "area_options", "index": MATCH}, "value"), - Output({"type": "area_types", "index": MATCH}, "value"), - Input("store", "data"), - [ - State("indicators", "data"), - State({"type": "area_options", "index": MATCH}, "id"), - ], -) -def set_options(theme, indicators_dict, id): - - area = id["index"] - - area_options = area_types = [] - if area in indicators_dict[theme["theme"]]: - indicators = indicators_dict[theme["theme"]][area].get("indicators") - area_indicators = indicators.keys() if indicators is dict else indicators - area_options = [ - { - "label": indicator_names[code], - "value": code, - } - for code in area_indicators - ] - - area_types = [ - { - "label": name.capitalize(), - "value": name, - } - for name in indicators_dict[theme["theme"]][area].get("graphs", {}).keys() - ] - - name = ( - indicators_dict[theme["theme"]][area].get("name") - if area in indicators_dict[theme["theme"]] - else "" - ) - default_option = ( - indicators_dict[theme["theme"]][area].get("default") - if area in indicators_dict[theme["theme"]] - else "" - ) - default_graph = ( - indicators_dict[theme["theme"]][area].get("default_graph") - if area in indicators_dict[theme["theme"]] - else "" - ) - - return name, area_options, area_types, default_option, default_graph - - -@app.callback( - Output({"type": "area_breakdowns", "index": MATCH}, "options"), - [ - Input({"type": "area_options", "index": MATCH}, "value"), - Input({"type": "area_types", "index": MATCH}, "value"), - ], - [ - State({"type": "area_breakdowns", "index": MATCH}, "id"), - ], -) -def breakdown_options(indicator, fig_type, id): - - options = [{"label": "Total", "value": "TOTAL"}] - # lbassil: change the disaggregation to use the names of the dimensions instead of the codes - all_breakdowns = [ - {"label": "Sex", "value": "SEX"}, - {"label": "Age", "value": "AGE"}, - {"label": "Residence", "value": "RESIDENCE"}, - {"label": "Wealth Quintile", "value": "WEALTH_QUINTILE"}, - ] - dimensions = indicators_config.get(indicator, {}).keys() - # keep only TOTAL for line charts - if dimensions and fig_type != "line": - for breakdown in all_breakdowns: - if breakdown["value"] in dimensions: - options.append(breakdown) - return options - - -@app.callback( - Output({"type": "area_breakdowns", "index": MATCH}, "value"), - [ - Input("store", "data"), - Input({"type": "area_breakdowns", "index": MATCH}, "options"), - Input({"type": "area_types", "index": MATCH}, "value"), - ], - [ - State("indicators", "data"), - State({"type": "area_breakdowns", "index": MATCH}, "id"), - ], -) -def set_default_compare( - selections, compare_options, selected_type, indicators_dict, id -): - area = id["index"] - # lbassil: add this condition to stop the exception for the main area - if area in indicators_dict[selections["theme"]] and area != "MAIN": - default = indicators_dict[selections["theme"]][area]["default_graph"] - fig_type = selected_type if selected_type else default - config = indicators_dict[selections["theme"]][area]["graphs"][fig_type] - default_compare = config.get("compare") - - return ( - "TOTAL" - if fig_type == "line" or default_compare is None - else default_compare - if default_compare in compare_options - else compare_options[1]["value"] - if len(compare_options) > 1 - else compare_options[0]["value"] - ) - return "TOTAL" - - -@app.callback( - Output("main_area", "figure"), - Output("main_area_sources", "children"), - [ - Input({"type": "area_options", "index": "MAIN"}, "value"), - Input({"type": "historical_data_toggle", "index": "MAIN"}, "value"), - Input("store", "data"), - ], - [ - State("indicators", "data"), - ], -) -def main_figure(indicator, show_historical_data, selections, indicators_dict): - latest_data = not show_historical_data - options = indicators_dict[selections["theme"]]["MAIN"]["options"] - - data = get_filtered_dataset( - [indicator], - selections["years"], - selections["countries"], - latest_data=latest_data, - ) - - # check if the dataframe is empty meaning no data to display as per the user's selection - if data.empty: - return EMPTY_CHART, "" - - # lbassil: replace UNIT_MEASURE by Unit_name to use the name of the unit instead of the code - name = ( - data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()[0] - if len(data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()) > 0 - else "" - ) - df_indicator_sources = df_sources[df_sources["Code"] == indicator] - unique_indicator_sources = df_indicator_sources["Source_Full"].unique() - source = ( - "; ".join(list(unique_indicator_sources)) - if len(unique_indicator_sources) > 0 - else "" - ) - source_link = ( - df_indicator_sources["Source_Link"].unique()[0] - if len(unique_indicator_sources) > 0 - else "" - ) - - options["labels"] = DEFAULT_LABELS.copy() - options["labels"]["OBS_VALUE"] = name - options["labels"]["text"] = "OBS_VALUE" - options["geojson"] = geo_json_countries - if latest_data: - # remove the animation frame and show all countries at once - options.pop("animation_frame") - # add the year to show on hover - options["hover_name"] = "TIME_PERIOD" - - main_figure = px.choropleth_mapbox(data, **options) - main_figure.update_layout(margin={"r": 0, "t": 1, "l": 2, "b": 1}) - - # check if this area's config has an animation frame and hence a slider - if len(main_figure.layout["sliders"]) > 0: - # set last frame as the active one; i.e. select the max year as the default displayed year - main_figure.layout["sliders"][0]["active"] = len(main_figure.frames) - 1 - # assign the data of the last year to the map; without this line the data will show the first year; - main_figure = go.Figure( - data=main_figure["frames"][-1]["data"], - frames=main_figure["frames"], - layout=main_figure.layout, - ) - return main_figure, html.A(html.P(source), href=source_link, target="_blank") - - -@app.callback( - Output({"type": "area", "index": MATCH}, "figure"), - Output({"type": "area_sources", "index": MATCH}, "children"), - [ - Input("store", "data"), - Input({"type": "area_options", "index": MATCH}, "value"), - Input({"type": "area_breakdowns", "index": MATCH}, "value"), - Input({"type": "area_types", "index": MATCH}, "value"), - Input({"type": "exclude_outliers_toggle", "index": MATCH}, "value"), - ], - [ - State("indicators", "data"), - State({"type": "area_options", "index": MATCH}, "id"), - ], -) -def area_figure( - selections, - indicator, - compare, - selected_type, - exclude_outliers, - indicators_dict, - id, -): - # only run if indicator not empty - if not indicator: - return {}, {} - # check if it is the country profile page - is_country_profile = selections["theme"] == "COUNTRYPROFILE" - - area = id["index"] - indicators = indicators_dict[selections["theme"]][area]["indicators"] - default_graph = indicators_dict[selections["theme"]][area].get( - "default_graph", "line" - ) - fig_type = selected_type if selected_type else default_graph - fig_config = indicators_dict[selections["theme"]][area]["graphs"][fig_type] - options = fig_config.get("options") - traces = fig_config.get("trace_options") - dimension = False if fig_type == "line" or compare == "TOTAL" else compare - - indicator_name = str(indicator_names.get(indicator, "")) - # do we need `indicator_settings` below? - indicator_settings = ( - indicators.get(indicator, {}) if type(indicators) is dict else {} - ) - data = get_filtered_dataset( - [indicator], - selections["years"], - selections["countries"], - compare, - latest_data=False if fig_type == "line" or is_country_profile else True, - ).sort_values("OBS_VALUE", ascending=False) - # check if the dataframe is empty meaning no data to display as per the user's selection - if data.empty: - return EMPTY_CHART, "" - - # check if the exclude outliers checkbox is checked - if exclude_outliers: - # filter the data to the remove the outliers - # (df < df.quantile(0.1)).any() (df > df.quantile(0.9)).any() - data["z_scores"] = np.abs(zscore(data["OBS_VALUE"])) # calculate z-scores of df - # filter the data entries to remove the outliers - data = data[(data["z_scores"] < 3) | (data["z_scores"].isnull())] - - # lbassil: was UNIT_MEASURE - name = ( - data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()[0] - if len(data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()) > 0 - else "" - ) - df_indicator_sources = df_sources[df_sources["Code"] == indicator] - unique_indicator_sources = df_indicator_sources["Source_Full"].unique() - source = ( - "; ".join(list(unique_indicator_sources)) - if len(unique_indicator_sources) > 0 - else "" - ) - source_link = ( - df_indicator_sources["Source_Link"].unique()[0] - if len(unique_indicator_sources) > 0 - else "" - ) - - options["labels"] = DEFAULT_LABELS.copy() - options["labels"]["OBS_VALUE"] = name - - # set the chart title, wrap the text when the indicator name is too long - chart_title = textwrap.wrap( - indicator_name, - width=74, - ) - chart_title = "
".join(chart_title) - - # set the layout to center the chart title and change its font size and color - layout = go.Layout( - title=chart_title, - title_x=0.5, - font=dict(family="Arial", size=12), - legend=dict(x=0.9, y=0.5), - ) - - # Add this code to avoid having decimal year on the x-axis for time series charts - if fig_type == "line" or is_country_profile: - data.sort_values(by=["TIME_PERIOD"], inplace=True) - layout["xaxis"] = dict( - tickmode="linear", - tick0=selections["years"][0], - dtick=1, - categoryorder="total ascending", - ) - - if dimension: - # lbassil: use the dimension name instead of the code - dimension_name = str(dimension_names.get(dimension, "")) - options["color"] = dimension_name - if compare == "WEALTH_QUINTILE": - wealth_dict = { - "Lowest": 0, - "Second": 1, - "Middle": 2, - "Fourth": 3, - "Highest": 4, - } - data.sort_values( - by=[dimension], key=lambda x: x.map(wealth_dict), inplace=True - ) - else: - # sort by the compare value to have the legend in the right ascending order - data.sort_values(by=[dimension], inplace=True) - - fig = getattr(px, fig_type)(data, **options) - fig.update_layout(layout) - if traces: - fig.update_traces(**traces) - - return fig, html.A(html.P(source), href=source_link, target="_blank") diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/base_page_old.py b/transmonee_dashboard/src/transmonee_dashboard/pages/base_page_old.py new file mode 100644 index 00000000..5611eb02 --- /dev/null +++ b/transmonee_dashboard/src/transmonee_dashboard/pages/base_page_old.py @@ -0,0 +1,1393 @@ +import textwrap + +import dash +import dash_bootstrap_components as dbc +import dash_core_components as dcc +import dash_html_components as html +import dash_treeview_antd +import numpy as np +import plotly.express as px +import plotly.graph_objects as go +import plotly.io as pio +from dash.dependencies import MATCH, ClientsideFunction, Input, Output, State +from scipy.stats import zscore + +from ..app import app +from ..components import fa + +''' +from . import ( + countries, + countries_iso3_dict, + df_sources, + dimension_names, + geo_json_countries, + get_filtered_dataset, + indicator_names, + indicators_config, + programme_country_indexes, + selection_index, + selection_tree, + unicef_country_prog, + years, + get_search_countries, +) +''' + +from . import ( + years, + countries, + get_search_countries, + selection_tree, + countries_iso3_dict +) + +# set defaults +pio.templates.default = "plotly_white" +px.defaults.color_continuous_scale = px.colors.sequential.BuGn +px.defaults.color_discrete_sequence = px.colors.qualitative.Dark24 + +colours = [ + "primary", + "success", + "warning", + "danger", + "secondary", + "info", + "success", + "danger", +] +AREA_KEYS = ["MAIN", "AREA_1", "AREA_2", "AREA_3", "AREA_4", "AREA_5", "AREA_6"] +DEFAULT_LABELS = { + "Country_name": "Country", + "TIME_PERIOD": "Year", + "Sex_name": "Sex", + "Residence_name": "Residence", + "Age_name": "Age", + "Wealth_name": "Wealth Quintile", +} +CARD_TEXT_STYLE = {"textAlign": "center", "color": "#0074D9"} + +EMPTY_CHART = { + "layout": { + "xaxis": {"visible": False}, + "yaxis": {"visible": False}, + "annotations": [ + { + "text": "No data is available for the selected filters", + "xref": "paper", + "yref": "paper", + "showarrow": False, + "font": {"size": 28}, + } + ], + } +} + + +def make_area(area_name): + area_id = {"type": "area", "index": area_name} + popover_id = {"type": "area_sources", "index": area_name} + historical_data_style = {"display": "none"} + exclude_outliers_style = {"paddingLeft": 20, "display": "block"} + breakdowns_style = {"display": "block"} + + # lbassil: still differentiating main area id from other areas ids because the call backs are still not unified + if area_name == "MAIN": + area_id = f"{area_name.lower()}_area" + popover_id = f"{area_name.lower()}_area_sources" + historical_data_style = {"display": "block"} + exclude_outliers_style = {"display": "none"} + breakdowns_style = {"display": "none"} + + # lbassil: unifying both main and figure area generations by tweaking the ids and styles + area = dbc.Card( + [ + dbc.CardHeader( + id={"type": "area_title", "index": area_name}, + style={"fontWeight": "bold"}, + ), + dbc.CardBody( + [ + dcc.Dropdown( + id={"type": "area_options", "index": area_name}, + className="dcc_control", + ), + html.Br(), + dbc.Checklist( + options=[ + { + "label": "Show historical data", + "value": 1, + } + ], + value=[], + id={ + "type": "historical_data_toggle", + "index": area_name, + }, + switch=True, + style=historical_data_style, + ), + html.Br(), + dbc.RadioItems( + id={"type": "area_types", "index": area_name}, + inline=True, + ), + dcc.Loading([dcc.Graph(id=area_id)]), + dbc.Checklist( + options=[ + { + "label": "Exclude outliers ", + "value": 1, + } + ], + value=[1], + id={ + "type": "exclude_outliers_toggle", + "index": area_name, + }, + switch=True, + style=exclude_outliers_style, + ), + html.Br(), + dbc.RadioItems( + id={"type": "area_breakdowns", "index": area_name}, + inline=True, + style=breakdowns_style, + ), + html.Div( + fa("fas fa-info-circle"), + id=f"{area_name.lower()}_area_info", + className="float-right", + ), + dbc.Popover( + [ + dbc.PopoverHeader("Sources"), + dbc.PopoverBody(id=popover_id), + ], + id="hover", + target=f"{area_name.lower()}_area_info", + trigger="hover", + ), + ] + ), + ], + id={"type": "area_parent", "index": area_name}, + ) + return area + + +def get_base_layout(**kwargs): + main_title = kwargs.get("main_title") + themes_row_style = {"verticalAlign": "center", "display": "flex"} + country_dropdown_style = {"display": "none"} + countries_filter_style = {"display": "block"} + + return html.Div( + [ + html.Div( + className="heading", + style={"padding": 36}, + children=[ + html.Div( + className="heading-content", + children=[ + html.Div( + className="heading-panel", + style={"padding": 20}, + children=[ + html.H1( + main_title, + id="main_title", + className="heading-title", + ), + html.P( + id="subtitle", + className="heading-subtitle", + ), + ], + ), + ], + ), + + dbc.Row( + children=[ + dbc.Col( + [ + dbc.Row( + [ + dbc.ButtonGroup( + id="themes", + ), + ], + id="theme-row", + # width=4, + className="my-2", + # no_gutters=True, + justify="center", + style=themes_row_style, + ), + dbc.Row( + [ + dbc.DropdownMenu( + label=f"Years: {years[0]} - {years[-1]}", + id="collapse-years-button", + className="m-2", + color="info", + # block=True, + children=[ + dbc.Card( + dcc.RangeSlider( + id="year_slider", + min=0, + max=len(years) - 1, + step=None, + marks={ + index: str(year) + for index, year in enumerate( + years + ) + }, + value=[0, len(years) - 1], + ), + style={ + "maxHeight": "250px", + "minWidth": "500px", + }, + className="overflow-auto", + body=True, + ), + ], + ), + + dcc.Dropdown( + id="country_profile_selector", + options=get_search_countries(False), + value="ALB", + multi=False, + placeholder="Select a country...", + className="m-2", + style=country_dropdown_style, + ), + dbc.DropdownMenu( + label=f"Countries: {len(countries)}", + id="collapse-countries-button", + className="m-2", + color="info", + style=countries_filter_style, + children=[ + dbc.Card( + dash_treeview_antd.TreeView( + id="country_selector", + multiple=True, + checkable=True, + checked=["0"], + # selected=[], + expanded=["0"], + data=selection_tree, + ), + style={ + "maxHeight": "250px", + # "maxWidth": "300px", + }, + className="overflow-auto", + body=True, + ), + ], + ), + # dbc.FormGroup( + # [ + # dbc.Checkbox( + # id="programme-toggle", + # className="custom-control-input", + # ), + # dbc.Label( + # "UNICEF Country Programmes", + # html_for="programme-toggle", + # className="custom-control-label", + # color="primary", + # ), + # ], + # className="custom-control custom-switch m-2", + # check=True, + # inline=True, + # style=programme_toggle_style, + # ), + ], + id="filter-row", + no_gutters=True, + justify="center", + ), + ] + ), + ], + # sticky="top", + className="sticky-top bg-light", + ), + dbc.Row( + [ + dbc.CardDeck( + id="cards_row", + className="mt-3", + ), + ], + justify="center", + ), + html.Br(), + ], + ), + ] + ) + + +def get_base_layout_old(**kwargs): + indicators_dict = kwargs.get("indicators") + main_title = kwargs.get("main_title") + is_country_profile = kwargs.get("is_country_profile") + country_dropdown_style = {"display": "none"} + themes_row_style = {"verticalAlign": "center", "display": "flex"} + countries_filter_style = {"display": "block"} + programme_toggle_style = {"display": "block"} + main_area_style = {"display": "block"} + + if is_country_profile: + country_dropdown_style = { + "minWidth": 400, + "maxWidth": 600, + "paddingRight": 4, + "verticalAlign": "center", + "display": "visible", + } + themes_row_style = {"display": "none"} + countries_filter_style = {"display": "none"} + programme_toggle_style = {"display": "none"} + main_area_style = {"display": "none"} + + return html.Div( + [ + dcc.Store(id="indicators", data=indicators_dict), + dcc.Location(id="theme"), + html.Div( + className="heading", + style={"padding": 36}, + children=[ + html.Div( + className="heading-content", + children=[ + html.Div( + className="heading-panel", + style={"padding": 20}, + children=[ + html.H1( + main_title, + id="main_title", + className="heading-title", + ), + html.P( + id="subtitle", + className="heading-subtitle", + ), + ], + ), + ], + ) + ], + ), + dbc.Row( + children=[ + dbc.Col( + [ + dbc.Row( + [ + dbc.ButtonGroup( + id="themes", + ), + ], + id="theme-row", + # width=4, + className="my-2", + # no_gutters=True, + justify="center", + style=themes_row_style, + ), + dbc.Row( + [ + dbc.DropdownMenu( + label=f"Years: {years[0]} - {years[-1]}", + id="collapse-years-button", + className="m-2", + color="info", + # block=True, + children=[ + dbc.Card( + dcc.RangeSlider( + id="year_slider", + min=0, + max=len(years) - 1, + step=None, + marks={ + index: str(year) + for index, year in enumerate( + years + ) + }, + value=[0, len(years) - 1], + ), + style={ + "maxHeight": "250px", + "minWidth": "500px", + }, + className="overflow-auto", + body=True, + ), + ], + ), + dcc.Dropdown( + id="country_profile_selector", + options=get_search_countries(False), + value="ALB", + multi=False, + placeholder="Select a country...", + className="m-2", + style=country_dropdown_style, + ), + dbc.DropdownMenu( + label=f"Countries: {len(countries)}", + id="collapse-countries-button", + className="m-2", + color="info", + style=countries_filter_style, + children=[ + dbc.Card( + dash_treeview_antd.TreeView( + id="country_selector", + multiple=True, + checkable=True, + checked=["0"], + # selected=[], + expanded=["0"], + data=selection_tree, + ), + style={ + "maxHeight": "250px", + # "maxWidth": "300px", + }, + className="overflow-auto", + body=True, + ), + ], + ), + dbc.FormGroup( + [ + dbc.Checkbox( + id="programme-toggle", + className="custom-control-input", + ), + dbc.Label( + "UNICEF Country Programmes", + html_for="programme-toggle", + className="custom-control-label", + color="primary", + ), + ], + className="custom-control custom-switch m-2", + check=True, + inline=True, + style=programme_toggle_style, + ), + ], + id="filter-row", + no_gutters=True, + justify="center", + ), + ] + ), + ], + # sticky="top", + className="sticky-top bg-light", + ), + dbc.Row( + [ + dbc.CardDeck( + id="cards_row", + className="mt-3", + ), + ], + justify="center", + ), + html.Br(), + dbc.CardDeck( + [make_area(area) for area in ["MAIN"]], + style=main_area_style, + ), + html.Br(), + dbc.CardDeck( + [make_area(area) for area in ["AREA_1", "AREA_2"]], + ), + html.Br(), + dbc.CardDeck( + [make_area(area) for area in ["AREA_3", "AREA_4"]], + ), + html.Br(), + dbc.CardDeck( + [make_area(area) for area in ["AREA_5", "AREA_6"]], + ), + html.Br(), + ], + ) + + +def make_card( + card_id, + name, + suffix, + indicator_sources, + source_link, + indicator_header, + numerator_pairs, +): + card = dbc.Card( + [ + dbc.CardBody( + [ + html.H1( + indicator_header, + className="display-4", + style={ + # "fontSize": 50, + "textAlign": "center", + "color": "#1cabe2", + }, + ), + html.H4(suffix, className="card-title"), + html.P(name, className="lead"), + html.Div( + fa("fas fa-info-circle"), + id=f"{card_id}_info", + # className="float-right", + style={ + "position": "absolute", + "bottom": "10px", + "right": "10px", + }, + ), + ], + style={ + # "fontSize": 50, + "textAlign": "center", + }, + ), + dbc.Popover( + [ + dbc.PopoverHeader( + html.A( + html.P(f"Sources: {indicator_sources}"), + href=source_link, + target="_blank", + ) + ), + dbc.PopoverBody( + dcc.Markdown(get_card_popover_body(numerator_pairs)) + ), + ], + id="hover", + target=f"{card_id}_info", + trigger="hover", + ), + ], + color="primary", + outline=True, + id=card_id, + ) + return card + + +def indicator_card( + selections, + card_id, + name, + numerator, + suffix, + denominator=None, + absolute=False, + average=False, + min_max=False, + sex_code=None, + age_group=None, +): + indicators = numerator.split(",") + + # TODO: Change to use albertos config + # lbassil: had to change this to cater for 2 dimensions set to the indicator card like age and sex + breakdown = "TOTAL" + # define the empty dimensions dict to be filled based on the card data filters + dimensions = {} + if age_group is not None: + dimensions["AGE"] = [age_group] + if sex_code is not None: + dimensions["SEX"] = [sex_code] + + filtered_data = get_filtered_dataset( + indicators, + selections["years"], + selections["countries"], + breakdown, + dimensions, + latest_data=True, + ) + + df_indicator_sources = df_sources[df_sources["Code"].isin(indicators)] + unique_indicator_sources = df_indicator_sources["Source_Full"].unique() + indicator_sources = ( + "; ".join(list(unique_indicator_sources)) + if len(unique_indicator_sources) > 0 + else "" + ) + source_link = ( + df_indicator_sources["Source_Link"].unique()[0] + if len(unique_indicator_sources) > 0 + else "" + ) + # lbassil: add this check because we are getting an exception where there is no data; i.e. no totals for all dimensions mostly age for the selected indicator + if filtered_data.empty: + indicator_header = "No data" + indicator_sources = "NA" + numerator_pairs = [] + return make_card( + card_id, + name, + indicator_header, + indicator_sources, + source_link, + numerator_pairs, + ) + + # select last value for each country + indicator_values = ( + filtered_data.groupby( + [ + "Country_name", + "TIME_PERIOD", + ] + ).agg({"OBS_VALUE": "sum", "CODE": "count"}) + ).reset_index() + + numerator_pairs = ( + indicator_values[indicator_values.CODE == len(indicators)] + .groupby("Country_name", as_index=False) + .last() + .set_index(["Country_name", "TIME_PERIOD"]) + ) + + if suffix.lower() == "countries": + # this is a hack to accomodate small cases (to discuss with James) + if "FREE" in numerator: + # trick to filter number of years of free education + indicator_sum = (numerator_pairs.OBS_VALUE >= 1).to_numpy().sum() + sources = numerator_pairs.index.tolist() + numerator_pairs = numerator_pairs[numerator_pairs.OBS_VALUE >= 1] + else: + # trick to accomodate cards for admin exams (AND for boolean indicators) + # filter exams according to number of indicators + indicator_sum = ( + (numerator_pairs.OBS_VALUE == len(indicators)).to_numpy().sum() + ) + sources = numerator_pairs.index.tolist() + + else: + indicator_sum = numerator_pairs["OBS_VALUE"].to_numpy().sum() + sources = numerator_pairs.index.tolist() + if average and len(sources) > 1: + indicator_sum = indicator_sum / len(sources) + + # define indicator header text: the resultant number except for the min-max range + if min_max and len(sources) > 1: + indicator_min = "{:,.1f}".format(numerator_pairs["OBS_VALUE"].min()) + indicator_max = "{:,.1f}".format(numerator_pairs["OBS_VALUE"].max()) + indicator_header = f"[{indicator_min} - {indicator_max}]" + else: + indicator_header = "{:,.0f}".format(indicator_sum) + + return make_card( + card_id, + name, + suffix, + indicator_sources, + source_link, + indicator_header, + numerator_pairs, + ) + + + +def get_card_popover_body(sources): + """This function is used to generate the list of countries that are part of the card's + displayed result; it displays the countries as a list, each on a separate line + + Args: + sources (_type_): _description_ + + Returns: + _type_: _description_ + """ + countries = [] + # lbassil: added this condition to stop the exception when sources is empty + if len(sources) > 0: + for index, source_info in sources.sort_values(by="OBS_VALUE").iterrows(): + countries.append(f"- {index[0]}, {source_info[0]} ({index[1]})") + card_countries = "\n".join(countries) + return card_countries + else: + return "NA" + + + + + + + +# TODO: Move to client side call back +@app.callback( + Output("collapse-years", "is_open"), + Output("collapse-countries", "is_open"), + Output("collapse-engagements", "is_open"), + [ + Input("collapse-years-button", "n_clicks"), + Input("collapse-countries-button", "n_clicks"), + Input("collapse-engagements-button", "n_clicks"), + ], + [ + State("collapse-years-button", "is_open"), + State("collapse-countries-button", "is_open"), + State("collapse-engagements-button", "is_open"), + ], +) +def toggle_collapse(n1, n2, n3, is_open1, is_open2, is_open3): + ctx = dash.callback_context + + if not ctx.triggered: + return False, False, False + else: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + + if button_id == "collapse-years-button" and n1: + return not is_open1, False, False + elif button_id == "collapse-countries-button" and n2: + return False, not is_open2, False + elif button_id == "collapse-engagements-button" and n3: + return False, False, not is_open3 + return False, False, False + +''' +@app.callback( + Output({"type": "area_parent", "index": MATCH}, "hidden"), + Input("theme", "hash"), + [ + State("indicators", "data"), + State({"type": "area_parent", "index": MATCH}, "id"), + ], +) +def display_areas(theme, indicators_dict, id): + area = id["index"] + theme = theme[1:].upper() if theme else next(iter(indicators_dict.keys())) + return area not in indicators_dict[theme] + + +@app.callback( + Output("store", "data"), + Output("country_selector", "checked"), + Output("programme-toggle", "checked"), + Output("collapse-years-button", "label"), + Output("collapse-countries-button", "label"), + [ + Input("theme", "hash"), + Input("year_slider", "value"), + Input("country_selector", "checked"), + Input("programme-toggle", "checked"), + Input("country_profile_selector", "value"), + ], + State("indicators", "data"), +) +def apply_filters( + theme, + years_slider, + country_selector, + programme_toggle, + selected_country, + indicators, +): + ctx = dash.callback_context + selected = ctx.triggered[0]["prop_id"].split(".")[0] + countries_selected = set() + current_theme = theme[1:].upper() if theme else next(iter(indicators.keys())) + # check if it is the country profile page + is_country_profile = current_theme == "COUNTRYPROFILE" + # check if the user clicked on the generate button in the country profile page + if is_country_profile: + key_list = list(countries_iso3_dict.keys()) + val_list = list(countries_iso3_dict.values()) + # get the name of the selected country in the dropdown to filter the data accordingly + countries_selected = ( + [key_list[val_list.index(selected_country)]] if selected_country else [] + ) + elif programme_toggle and selected == "programme-toggle": + countries_selected = unicef_country_prog + country_selector = programme_country_indexes + # Add the condition to know when the user unchecks the UNICEF country programs! + elif not country_selector or ( + not programme_toggle and selected == "programme-toggle" + ): + countries_selected = countries + # Add this to check all the items in the selection tree + country_selector = ["0"] + else: + for index in country_selector: + countries_selected.update(selection_index[index]) + if countries_selected == countries: + # if all countries are all selected then stop + break + + countries_selected = list(countries_selected) + country_text = f"{len(countries_selected)} Selected" + # need to include the last selected year as it was exluded in the previous method + selected_years = years[years_slider[0]: years_slider[1] + 1] + # selected_years = years[slice(*years_slider)] + + # Use the dictionary to return the values of the selected countries based on the SDMX ISO3 codes + countries_selected_codes = [ + countries_iso3_dict[country] for country in countries_selected + ] + selections = dict( + theme=current_theme, + indicators_dict=indicators, + years=selected_years, + countries=countries_selected_codes, + is_adolescent=("ADOLESCENT" in indicators), + ) + + return ( + selections, + country_selector, + countries_selected == unicef_country_prog, + f"Years: {selected_years[0]} - {selected_years[-1]}", + "Countries: {}".format(country_text), + ) + + +def indicator_card( + selections, + card_id, + name, + numerator, + suffix, + denominator=None, + absolute=False, + average=False, + min_max=False, + sex_code=None, + age_group=None, +): + indicators = numerator.split(",") + + # TODO: Change to use albertos config + # lbassil: had to change this to cater for 2 dimensions set to the indicator card like age and sex + breakdown = "TOTAL" + # define the empty dimensions dict to be filled based on the card data filters + dimensions = {} + if age_group is not None: + dimensions["AGE"] = [age_group] + if sex_code is not None: + dimensions["SEX"] = [sex_code] + + filtered_data = get_filtered_dataset( + indicators, + selections["years"], + selections["countries"], + breakdown, + dimensions, + latest_data=True, + ) + + df_indicator_sources = df_sources[df_sources["Code"].isin(indicators)] + unique_indicator_sources = df_indicator_sources["Source_Full"].unique() + indicator_sources = ( + "; ".join(list(unique_indicator_sources)) + if len(unique_indicator_sources) > 0 + else "" + ) + source_link = ( + df_indicator_sources["Source_Link"].unique()[0] + if len(unique_indicator_sources) > 0 + else "" + ) + # lbassil: add this check because we are getting an exception where there is no data; i.e. no totals for all dimensions mostly age for the selected indicator + if filtered_data.empty: + indicator_header = "No data" + indicator_sources = "NA" + numerator_pairs = [] + return make_card( + card_id, + name, + indicator_header, + indicator_sources, + source_link, + numerator_pairs, + ) + + # select last value for each country + indicator_values = ( + filtered_data.groupby( + [ + "Country_name", + "TIME_PERIOD", + ] + ).agg({"OBS_VALUE": "sum", "CODE": "count"}) + ).reset_index() + + numerator_pairs = ( + indicator_values[indicator_values.CODE == len(indicators)] + .groupby("Country_name", as_index=False) + .last() + .set_index(["Country_name", "TIME_PERIOD"]) + ) + + if suffix.lower() == "countries": + # this is a hack to accomodate small cases (to discuss with James) + if "FREE" in numerator: + # trick to filter number of years of free education + indicator_sum = (numerator_pairs.OBS_VALUE >= 1).to_numpy().sum() + sources = numerator_pairs.index.tolist() + numerator_pairs = numerator_pairs[numerator_pairs.OBS_VALUE >= 1] + else: + # trick to accomodate cards for admin exams (AND for boolean indicators) + # filter exams according to number of indicators + indicator_sum = ( + (numerator_pairs.OBS_VALUE == len(indicators)).to_numpy().sum() + ) + sources = numerator_pairs.index.tolist() + + else: + indicator_sum = numerator_pairs["OBS_VALUE"].to_numpy().sum() + sources = numerator_pairs.index.tolist() + if average and len(sources) > 1: + indicator_sum = indicator_sum / len(sources) + + # define indicator header text: the resultant number except for the min-max range + if min_max and len(sources) > 1: + indicator_min = "{:,.1f}".format(numerator_pairs["OBS_VALUE"].min()) + indicator_max = "{:,.1f}".format(numerator_pairs["OBS_VALUE"].max()) + indicator_header = f"[{indicator_min} - {indicator_max}]" + else: + indicator_header = "{:,.0f}".format(indicator_sum) + + return make_card( + card_id, + name, + suffix, + indicator_sources, + source_link, + indicator_header, + numerator_pairs, + ) +''' + +@app.callback( + Output("cards_row", "children"), + [ + Input("store", "data"), + ], + [State("cards_row", "children"), State("indicators", "data")], +) +def show_cards(selections, current_cards, indicators_dict): + cards = [ + indicator_card( + selections, + f"card-{num}", + card["name"], + card["indicator"], + card["suffix"], + card.get("denominator"), + card.get("absolute"), + card.get("average"), + card.get("min_max"), + card.get("sex"), + card.get("age"), + ) + for num, card in enumerate(indicators_dict[selections["theme"]]["CARDS"]) + ] + return cards + + +@app.callback( + Output("subtitle", "children"), + Output("themes", "children"), + [ + Input("store", "data"), + Input("country_profile_selector", "value"), + ], + State("indicators", "data"), +) +''' +def show_themes(selections, selected_country, indicators_dict): + # check if it is the country profile page + is_country_profile = selections["theme"] == "COUNTRYPROFILE" + ctx = dash.callback_context + ctrl_id = ctx.triggered[0]["prop_id"].split(".")[0] + # check if the countries dropdown is causing this callback in order to set the sub-title to the country's name + if is_country_profile or ctrl_id == "countries": + key_list = list(countries_iso3_dict.keys()) + val_list = list(countries_iso3_dict.values()) + subtitle = ( + key_list[val_list.index(selected_country)] + if selected_country + else "Country Name" + ) + return subtitle, [] + + subtitle = indicators_dict[selections["theme"]].get("NAME") + url_hash = "#{}".format((next(iter(selections.items())))[1].lower()) + # hide the buttons when only one option is available + if len(indicators_dict.items()) == 1: + return subtitle, [] + buttons = [ + dbc.Button( + value["NAME"], + id=key, + color=colours[num], + className="theme mx-1", + href=f"#{key.lower()}", + active=url_hash == f"#{key.lower()}", + ) + for num, (key, value) in enumerate(indicators_dict.items()) + ] + return subtitle, buttons + + +@app.callback( + Output({"type": "area_title", "index": MATCH}, "children"), + Output({"type": "area_options", "index": MATCH}, "options"), + Output({"type": "area_types", "index": MATCH}, "options"), + Output({"type": "area_options", "index": MATCH}, "value"), + Output({"type": "area_types", "index": MATCH}, "value"), + Input("store", "data"), + [ + State("indicators", "data"), + State({"type": "area_options", "index": MATCH}, "id"), + ], +) +def set_options(theme, indicators_dict, id): + area = id["index"] + + area_options = area_types = [] + if area in indicators_dict[theme["theme"]]: + indicators = indicators_dict[theme["theme"]][area].get("indicators") + area_indicators = indicators.keys() if indicators is dict else indicators + area_options = [ + { + "label": indicator_names[code], + "value": code, + } + for code in area_indicators + ] + + area_types = [ + { + "label": name.capitalize(), + "value": name, + } + for name in indicators_dict[theme["theme"]][area].get("graphs", {}).keys() + ] + + name = ( + indicators_dict[theme["theme"]][area].get("name") + if area in indicators_dict[theme["theme"]] + else "" + ) + default_option = ( + indicators_dict[theme["theme"]][area].get("default") + if area in indicators_dict[theme["theme"]] + else "" + ) + default_graph = ( + indicators_dict[theme["theme"]][area].get("default_graph") + if area in indicators_dict[theme["theme"]] + else "" + ) + + return name, area_options, area_types, default_option, default_graph + + +@app.callback( + Output({"type": "area_breakdowns", "index": MATCH}, "options"), + [ + Input({"type": "area_options", "index": MATCH}, "value"), + Input({"type": "area_types", "index": MATCH}, "value"), + ], + [ + State({"type": "area_breakdowns", "index": MATCH}, "id"), + ], +) +def breakdown_options(indicator, fig_type, id): + options = [{"label": "Total", "value": "TOTAL"}] + # lbassil: change the disaggregation to use the names of the dimensions instead of the codes + all_breakdowns = [ + {"label": "Sex", "value": "SEX"}, + {"label": "Age", "value": "AGE"}, + {"label": "Residence", "value": "RESIDENCE"}, + {"label": "Wealth Quintile", "value": "WEALTH_QUINTILE"}, + ] + dimensions = indicators_config.get(indicator, {}).keys() + # keep only TOTAL for line charts + if dimensions and fig_type != "line": + for breakdown in all_breakdowns: + if breakdown["value"] in dimensions: + options.append(breakdown) + return options + + +@app.callback( + Output({"type": "area_breakdowns", "index": MATCH}, "value"), + [ + Input("store", "data"), + Input({"type": "area_breakdowns", "index": MATCH}, "options"), + Input({"type": "area_types", "index": MATCH}, "value"), + ], + [ + State("indicators", "data"), + State({"type": "area_breakdowns", "index": MATCH}, "id"), + ], +) +def set_default_compare( + selections, compare_options, selected_type, indicators_dict, id +): + area = id["index"] + # lbassil: add this condition to stop the exception for the main area + if area in indicators_dict[selections["theme"]] and area != "MAIN": + default = indicators_dict[selections["theme"]][area]["default_graph"] + fig_type = selected_type if selected_type else default + config = indicators_dict[selections["theme"]][area]["graphs"][fig_type] + default_compare = config.get("compare") + + return ( + "TOTAL" + if fig_type == "line" or default_compare is None + else default_compare + if default_compare in compare_options + else compare_options[1]["value"] + if len(compare_options) > 1 + else compare_options[0]["value"] + ) + return "TOTAL" + + +@app.callback( + Output("main_area", "figure"), + Output("main_area_sources", "children"), + [ + Input({"type": "area_options", "index": "MAIN"}, "value"), + Input({"type": "historical_data_toggle", "index": "MAIN"}, "value"), + Input("store", "data"), + ], + [ + State("indicators", "data"), + ], +) +def main_figure(indicator, show_historical_data, selections, indicators_dict): + latest_data = not show_historical_data + options = indicators_dict[selections["theme"]]["MAIN"]["options"] + + data = get_filtered_dataset( + [indicator], + selections["years"], + selections["countries"], + latest_data=latest_data, + ) + + # check if the dataframe is empty meaning no data to display as per the user's selection + if data.empty: + return EMPTY_CHART, "" + + # lbassil: replace UNIT_MEASURE by Unit_name to use the name of the unit instead of the code + name = ( + data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()[0] + if len(data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()) > 0 + else "" + ) + df_indicator_sources = df_sources[df_sources["Code"] == indicator] + unique_indicator_sources = df_indicator_sources["Source_Full"].unique() + source = ( + "; ".join(list(unique_indicator_sources)) + if len(unique_indicator_sources) > 0 + else "" + ) + source_link = ( + df_indicator_sources["Source_Link"].unique()[0] + if len(unique_indicator_sources) > 0 + else "" + ) + + options["labels"] = DEFAULT_LABELS.copy() + options["labels"]["OBS_VALUE"] = name + options["labels"]["text"] = "OBS_VALUE" + options["geojson"] = geo_json_countries + if latest_data: + # remove the animation frame and show all countries at once + options.pop("animation_frame") + # add the year to show on hover + options["hover_name"] = "TIME_PERIOD" + + main_figure = px.choropleth_mapbox(data, **options) + main_figure.update_layout(margin={"r": 0, "t": 1, "l": 2, "b": 1}) + + # check if this area's config has an animation frame and hence a slider + if len(main_figure.layout["sliders"]) > 0: + # set last frame as the active one; i.e. select the max year as the default displayed year + main_figure.layout["sliders"][0]["active"] = len(main_figure.frames) - 1 + # assign the data of the last year to the map; without this line the data will show the first year; + main_figure = go.Figure( + data=main_figure["frames"][-1]["data"], + frames=main_figure["frames"], + layout=main_figure.layout, + ) + return main_figure, html.A(html.P(source), href=source_link, target="_blank") + + +@app.callback( + Output({"type": "area", "index": MATCH}, "figure"), + Output({"type": "area_sources", "index": MATCH}, "children"), + [ + Input("store", "data"), + Input({"type": "area_options", "index": MATCH}, "value"), + Input({"type": "area_breakdowns", "index": MATCH}, "value"), + Input({"type": "area_types", "index": MATCH}, "value"), + Input({"type": "exclude_outliers_toggle", "index": MATCH}, "value"), + ], + [ + State("indicators", "data"), + State({"type": "area_options", "index": MATCH}, "id"), + ], +) +def area_figure( + selections, + indicator, + compare, + selected_type, + exclude_outliers, + indicators_dict, + id, +): + # only run if indicator not empty + if not indicator: + return {}, {} + # check if it is the country profile page + is_country_profile = selections["theme"] == "COUNTRYPROFILE" + + area = id["index"] + indicators = indicators_dict[selections["theme"]][area]["indicators"] + default_graph = indicators_dict[selections["theme"]][area].get( + "default_graph", "line" + ) + fig_type = selected_type if selected_type else default_graph + fig_config = indicators_dict[selections["theme"]][area]["graphs"][fig_type] + options = fig_config.get("options") + traces = fig_config.get("trace_options") + dimension = False if fig_type == "line" or compare == "TOTAL" else compare + + indicator_name = str(indicator_names.get(indicator, "")) + # do we need `indicator_settings` below? + indicator_settings = ( + indicators.get(indicator, {}) if type(indicators) is dict else {} + ) + data = get_filtered_dataset( + [indicator], + selections["years"], + selections["countries"], + compare, + latest_data=False if fig_type == "line" or is_country_profile else True, + ).sort_values("OBS_VALUE", ascending=False) + # check if the dataframe is empty meaning no data to display as per the user's selection + if data.empty: + return EMPTY_CHART, "" + + # check if the exclude outliers checkbox is checked + if exclude_outliers: + # filter the data to the remove the outliers + # (df < df.quantile(0.1)).any() (df > df.quantile(0.9)).any() + data["z_scores"] = np.abs(zscore(data["OBS_VALUE"])) # calculate z-scores of df + # filter the data entries to remove the outliers + data = data[(data["z_scores"] < 3) | (data["z_scores"].isnull())] + + # lbassil: was UNIT_MEASURE + name = ( + data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()[0] + if len(data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()) > 0 + else "" + ) + df_indicator_sources = df_sources[df_sources["Code"] == indicator] + unique_indicator_sources = df_indicator_sources["Source_Full"].unique() + source = ( + "; ".join(list(unique_indicator_sources)) + if len(unique_indicator_sources) > 0 + else "" + ) + source_link = ( + df_indicator_sources["Source_Link"].unique()[0] + if len(unique_indicator_sources) > 0 + else "" + ) + + options["labels"] = DEFAULT_LABELS.copy() + options["labels"]["OBS_VALUE"] = name + + # set the chart title, wrap the text when the indicator name is too long + chart_title = textwrap.wrap( + indicator_name, + width=74, + ) + chart_title = "
".join(chart_title) + + # set the layout to center the chart title and change its font size and color + layout = go.Layout( + title=chart_title, + title_x=0.5, + font=dict(family="Arial", size=12), + legend=dict(x=0.9, y=0.5), + ) + + # Add this code to avoid having decimal year on the x-axis for time series charts + if fig_type == "line" or is_country_profile: + data.sort_values(by=["TIME_PERIOD"], inplace=True) + layout["xaxis"] = dict( + tickmode="linear", + tick0=selections["years"][0], + dtick=1, + categoryorder="total ascending", + ) + + if dimension: + # lbassil: use the dimension name instead of the code + dimension_name = str(dimension_names.get(dimension, "")) + options["color"] = dimension_name + if compare == "WEALTH_QUINTILE": + wealth_dict = { + "Lowest": 0, + "Second": 1, + "Middle": 2, + "Fourth": 3, + "Highest": 4, + } + data.sort_values( + by=[dimension], key=lambda x: x.map(wealth_dict), inplace=True + ) + else: + # sort by the compare value to have the legend in the right ascending order + data.sort_values(by=[dimension], inplace=True) + + fig = getattr(px, fig_type)(data, **options) + fig.update_layout(layout) + if traces: + fig.update_traces(**traces) + + return fig, html.A(html.P(source), href=source_link, target="_blank") +''' \ No newline at end of file diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/base_page_old2.py b/transmonee_dashboard/src/transmonee_dashboard/pages/base_page_old2.py new file mode 100644 index 00000000..08636c72 --- /dev/null +++ b/transmonee_dashboard/src/transmonee_dashboard/pages/base_page_old2.py @@ -0,0 +1,617 @@ +import textwrap + +import dash +import dash_bootstrap_components as dbc +import dash_core_components as dcc +import dash_html_components as html +import dash_treeview_antd +import numpy as np +import plotly.express as px +import plotly.graph_objects as go +import plotly.io as pio +from dash.dependencies import MATCH, ClientsideFunction, Input, Output, State +from scipy.stats import zscore + +from ..app import app +from ..components import fa + +''' +from . import ( + countries, + countries_iso3_dict, + df_sources, + dimension_names, + geo_json_countries, + get_filtered_dataset, + indicator_names, + indicators_config, + programme_country_indexes, + selection_index, + selection_tree, + unicef_country_prog, + years, + get_search_countries, +) +''' + +from . import ( + years, + countries, + get_search_countries, + selection_tree, + selection_index, + countries_iso3_dict +) + +# set defaults +pio.templates.default = "plotly_white" +px.defaults.color_continuous_scale = px.colors.sequential.BuGn +px.defaults.color_discrete_sequence = px.colors.qualitative.Dark24 + +colours = [ + "primary", + "success", + "warning", + "danger", + "secondary", + "info", + "success", + "danger", +] +AREA_KEYS = ["MAIN", "AREA_1", "AREA_2", "AREA_3", "AREA_4", "AREA_5", "AREA_6"] +DEFAULT_LABELS = { + "Country_name": "Country", + "TIME_PERIOD": "Year", + "Sex_name": "Sex", + "Residence_name": "Residence", + "Age_name": "Age", + "Wealth_name": "Wealth Quintile", +} +CARD_TEXT_STYLE = {"textAlign": "center", "color": "#0074D9"} + +EMPTY_CHART = { + "layout": { + "xaxis": {"visible": False}, + "yaxis": {"visible": False}, + "annotations": [ + { + "text": "No data is available for the selected filters", + "xref": "paper", + "yref": "paper", + "showarrow": False, + "font": {"size": 28}, + } + ], + } +} + + +def get_base_layout(**kwargs): + indicators_dict = kwargs.get("indicators") + main_title = kwargs.get("main_title") + themes_row_style = {"verticalAlign": "center", "display": "flex"} + country_dropdown_style = {"display": "none"} + countries_filter_style = {"display": "block"} + + return html.Div( + [ + dcc.Store(id="indicators", data=indicators_dict), + dcc.Location(id="theme"), + html.Div( + className="heading", + style={"padding": 36}, + children=[ + html.Div( + className="heading-content", + children=[ + html.Div( + className="heading-panel", + style={"padding": 20}, + children=[ + html.H1( + main_title, + id="main_title", + className="heading-title", + ), + html.P( + id="subtitle", + className="heading-subtitle", + ), + ], + ), + ], + ), + + dbc.Row( + children=[ + dbc.Col( + [ + dbc.Row( + [ + dbc.ButtonGroup( + id="themes", + ), + ], + id="theme-row", + # width=4, + className="my-2", + # no_gutters=True, + justify="center", + style=themes_row_style, + ), + dbc.Row( + [ + dbc.DropdownMenu( + label=f"Years: {years[0]} - {years[-1]}", + id="collapse-years-button", + className="m-2", + color="info", + # block=True, + children=[ + dbc.Card( + dcc.RangeSlider( + id="year_slider", + min=0, + max=len(years) - 1, + step=None, + marks={ + index: str(year) + for index, year in enumerate( + years + ) + }, + value=[0, len(years) - 1], + ), + style={ + "maxHeight": "250px", + "minWidth": "500px", + }, + className="overflow-auto", + body=True, + ), + ], + ), + + dcc.Dropdown( + id="country_profile_selector", + options=get_search_countries(False), + value="ALB", + multi=False, + placeholder="Select a country...", + className="m-2", + style=country_dropdown_style, + ), + dbc.DropdownMenu( + label=f"Countries: {len(countries)}", + id="collapse-countries-button", + className="m-2", + color="info", + style=countries_filter_style, + children=[ + dbc.Card( + dash_treeview_antd.TreeView( + id="country_selector", + multiple=True, + checkable=True, + checked=["0"], + # selected=[], + expanded=["0"], + data=selection_tree, + ), + style={ + "maxHeight": "250px", + # "maxWidth": "300px", + }, + className="overflow-auto", + body=True, + ), + ], + ), + # dbc.FormGroup( + # [ + # dbc.Checkbox( + # id="programme-toggle", + # className="custom-control-input", + # ), + # dbc.Label( + # "UNICEF Country Programmes", + # html_for="programme-toggle", + # className="custom-control-label", + # color="primary", + # ), + # ], + # className="custom-control custom-switch m-2", + # check=True, + # inline=True, + # style=programme_toggle_style, + # ), + ], + id="filter-row", + no_gutters=True, + justify="center", + ), + ] + ), + ], + # sticky="top", + className="sticky-top bg-light", + ), + dbc.Row( + [ + dbc.CardDeck( + id="cards_row", + className="mt-3", + ), + ], + justify="center", + ), + html.Br(), + ], + ), + ] + ) + + +def get_card_popover_body(sources): + """This function is used to generate the list of countries that are part of the card's + displayed result; it displays the countries as a list, each on a separate line + + Args: + sources (_type_): _description_ + + Returns: + _type_: _description_ + """ + countries = [] + # lbassil: added this condition to stop the exception when sources is empty + if len(sources) > 0: + for index, source_info in sources.sort_values(by="OBS_VALUE").iterrows(): + countries.append(f"- {index[0]}, {source_info[0]} ({index[1]})") + card_countries = "\n".join(countries) + return card_countries + else: + return "NA" + + +def make_card( + card_id, + name, + suffix, + indicator_sources, + source_link, + indicator_header, + numerator_pairs, +): + card = dbc.Card( + [ + dbc.CardBody( + [ + html.H1( + indicator_header, + className="display-4", + style={ + # "fontSize": 50, + "textAlign": "center", + "color": "#1cabe2", + }, + ), + html.H4(suffix, className="card-title"), + html.P(name, className="lead"), + html.Div( + fa("fas fa-info-circle"), + id=f"{card_id}_info", + # className="float-right", + style={ + "position": "absolute", + "bottom": "10px", + "right": "10px", + }, + ), + ], + style={ + # "fontSize": 50, + "textAlign": "center", + }, + ), + dbc.Popover( + [ + dbc.PopoverHeader( + html.A( + html.P(f"Sources: {indicator_sources}"), + href=source_link, + target="_blank", + ) + ), + dbc.PopoverBody( + dcc.Markdown(get_card_popover_body(numerator_pairs)) + ), + ], + id="hover", + target=f"{card_id}_info", + trigger="hover", + ), + ], + color="primary", + outline=True, + id=card_id, + ) + return card + + +@app.callback( + Output("store", "data"), + Output("country_selector", "checked"), + # Output("programme-toggle", "checked"), + Output("collapse-years-button", "label"), + Output("collapse-countries-button", "label"), + [ + Input("theme", "hash"), + Input("year_slider", "value"), + Input("country_selector", "checked"), + # Input("programme-toggle", "checked"), + # Input("country_profile_selector", "value"), + ], + State("indicators", "data"), +) +def apply_filters( + theme, + years_slider, + country_selector, + # programme_toggle, + # selected_country, + indicators, +): + print("apply_filters called") + ctx = dash.callback_context + selected = ctx.triggered[0]["prop_id"].split(".")[0] + countries_selected = set() + current_theme = theme[1:].upper() if theme else next(iter(indicators.keys())) + # check if it is the country profile page + is_country_profile = current_theme == "COUNTRYPROFILE" + # check if the user clicked on the generate button in the country profile page + ''' + if is_country_profile: + key_list = list(countries_iso3_dict.keys()) + val_list = list(countries_iso3_dict.values()) + # get the name of the selected country in the dropdown to filter the data accordingly + countries_selected = ( + [key_list[val_list.index(selected_country)]] if selected_country else [] + ) + elif programme_toggle and selected == "programme-toggle": + countries_selected = unicef_country_prog + country_selector = programme_country_indexes + # Add the condition to know when the user unchecks the UNICEF country programs! + elif not country_selector or ( + not programme_toggle and selected == "programme-toggle" + ): + countries_selected = countries + # Add this to check all the items in the selection tree + country_selector = ["0"] + else: + for index in country_selector: + countries_selected.update(selection_index[index]) + if countries_selected == countries: + # if all countries are all selected then stop + break + ''' + for index in country_selector: + countries_selected.update(selection_index[index]) + if countries_selected == countries: + # if all countries are all selected then stop + break + countries_selected = list(countries_selected) + country_text = f"{len(countries_selected)} Selected" + # need to include the last selected year as it was exluded in the previous method + selected_years = years[years_slider[0]: years_slider[1] + 1] + # selected_years = years[slice(*years_slider)] + + # Use the dictionary to return the values of the selected countries based on the SDMX ISO3 codes + countries_selected_codes = [ + countries_iso3_dict[country] for country in countries_selected + ] + + print("countries_selected") + print(countries_selected) + print("country_text") + print(country_text) + print("selected_years") + print(selected_years) + print("countries_selected_codes") + print(countries_selected_codes) + selections = dict( + theme=current_theme, + indicators_dict=indicators, + years=selected_years, + countries=countries_selected_codes, + is_adolescent=("ADOLESCENT" in indicators), + ) + + print("SELECTION") + print(selections) + + return ( + selections, + country_selector, + # countries_selected == unicef_country_prog, + f"Years: {selected_years[0]} - {selected_years[-1]}", + "Countries: {}".format(country_text), + ) + + +# TODO: Move to client side call back +@app.callback( + Output("collapse-years", "is_open"), + Output("collapse-countries", "is_open"), + Output("collapse-engagements", "is_open"), + [ + Input("collapse-years-button", "n_clicks"), + Input("collapse-countries-button", "n_clicks"), + Input("collapse-engagements-button", "n_clicks"), + ], + [ + State("collapse-years-button", "is_open"), + State("collapse-countries-button", "is_open"), + State("collapse-engagements-button", "is_open"), + ], +) +def toggle_collapse(n1, n2, n3, is_open1, is_open2, is_open3): + ctx = dash.callback_context + + if not ctx.triggered: + return False, False, False + else: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + + if button_id == "collapse-years-button" and n1: + return not is_open1, False, False + elif button_id == "collapse-countries-button" and n2: + return False, not is_open2, False + elif button_id == "collapse-engagements-button" and n3: + return False, False, not is_open3 + return False, False, False + + +def indicator_card( + selections, + card_id, + name, + numerator, + suffix, + denominator=None, + absolute=False, + average=False, + min_max=False, + sex_code=None, + age_group=None, +): + indicators = numerator.split(",") + + # TODO: Change to use albertos config + # lbassil: had to change this to cater for 2 dimensions set to the indicator card like age and sex + breakdown = "TOTAL" + # define the empty dimensions dict to be filled based on the card data filters + dimensions = {} + if age_group is not None: + dimensions["AGE"] = [age_group] + if sex_code is not None: + dimensions["SEX"] = [sex_code] + + filtered_data = get_filtered_dataset( + indicators, + selections["years"], + selections["countries"], + breakdown, + dimensions, + latest_data=True, + ) + + df_indicator_sources = df_sources[df_sources["Code"].isin(indicators)] + unique_indicator_sources = df_indicator_sources["Source_Full"].unique() + indicator_sources = ( + "; ".join(list(unique_indicator_sources)) + if len(unique_indicator_sources) > 0 + else "" + ) + source_link = ( + df_indicator_sources["Source_Link"].unique()[0] + if len(unique_indicator_sources) > 0 + else "" + ) + # lbassil: add this check because we are getting an exception where there is no data; i.e. no totals for all dimensions mostly age for the selected indicator + if filtered_data.empty: + indicator_header = "No data" + indicator_sources = "NA" + numerator_pairs = [] + return make_card( + card_id, + name, + indicator_header, + indicator_sources, + source_link, + numerator_pairs, + ) + + # select last value for each country + indicator_values = ( + filtered_data.groupby( + [ + "Country_name", + "TIME_PERIOD", + ] + ).agg({"OBS_VALUE": "sum", "CODE": "count"}) + ).reset_index() + + numerator_pairs = ( + indicator_values[indicator_values.CODE == len(indicators)] + .groupby("Country_name", as_index=False) + .last() + .set_index(["Country_name", "TIME_PERIOD"]) + ) + + if suffix.lower() == "countries": + # this is a hack to accomodate small cases (to discuss with James) + if "FREE" in numerator: + # trick to filter number of years of free education + indicator_sum = (numerator_pairs.OBS_VALUE >= 1).to_numpy().sum() + sources = numerator_pairs.index.tolist() + numerator_pairs = numerator_pairs[numerator_pairs.OBS_VALUE >= 1] + else: + # trick to accomodate cards for admin exams (AND for boolean indicators) + # filter exams according to number of indicators + indicator_sum = ( + (numerator_pairs.OBS_VALUE == len(indicators)).to_numpy().sum() + ) + sources = numerator_pairs.index.tolist() + + else: + indicator_sum = numerator_pairs["OBS_VALUE"].to_numpy().sum() + sources = numerator_pairs.index.tolist() + if average and len(sources) > 1: + indicator_sum = indicator_sum / len(sources) + + # define indicator header text: the resultant number except for the min-max range + if min_max and len(sources) > 1: + indicator_min = "{:,.1f}".format(numerator_pairs["OBS_VALUE"].min()) + indicator_max = "{:,.1f}".format(numerator_pairs["OBS_VALUE"].max()) + indicator_header = f"[{indicator_min} - {indicator_max}]" + else: + indicator_header = "{:,.0f}".format(indicator_sum) + + return make_card( + card_id, + name, + suffix, + indicator_sources, + source_link, + indicator_header, + numerator_pairs, + ) + +@app.callback( + Output("cards_row", "children"), + [ + Input("store", "data"), + ], + [State("cards_row", "children"), State("indicators", "data")], +) +def show_cards(selections, current_cards, indicators_dict): + cards = [] + + cards = [ + indicator_card( + selections, + f"card-{num}", + card["name"], + card["indicator"], + card["suffix"], + card.get("denominator"), + card.get("absolute"), + card.get("average"), + card.get("min_max"), + card.get("sex"), + card.get("age"), + ) + for num, card in enumerate(indicators_dict[selections["theme"]]["CARDS"]) + ] + + return cards diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/child_education.py b/transmonee_dashboard/src/transmonee_dashboard/pages/child_education.py index 0d6a627b..91abefbc 100644 --- a/transmonee_dashboard/src/transmonee_dashboard/pages/child_education.py +++ b/transmonee_dashboard/src/transmonee_dashboard/pages/child_education.py @@ -1,672 +1,27 @@ -from collections import defaultdict import plotly.express as px -from . import data, years from .base_page import get_base_layout + indicators_dict = { "PARTICIPATION": { "NAME": "Education access and participation", "CARDS": [ { "name": "Who are Out-of-School", - "indicator": "EDUNF_OFST_L1,EDUNF_OFST_L2,EDUNF_OFST_L3", + "indicator": "REPROV_EFFINAL_MUNICIPAL", "suffix": "Primary to upper secondary aged Children and Adolescents", - "age": "SCHOOL_AGE", - }, - { - "name": "Who are Out-of-School", - "indicator": "EDUNF_OFST_L1,EDUNF_OFST_L2,EDUNF_OFST_L3", - "suffix": "Primary to upper secondary aged Girls", - "sex": "F", - "age": "SCHOOL_AGE", - }, - { - "name": "Who are Out-of-School", - "indicator": "EDUNF_OFST_L1_UNDER1", - "suffix": "Children one year younger than the official primary entry age", - "age": "UNDER1_SCHOOL_ENTRY", - }, - ], - "MAIN": { - "name": "Out-of-School Children", - "geo": "Country_name", # REF_AREA - "options": dict( - locations="REF_AREA", - featureidkey="id", - color="OBS_VALUE", - color_continuous_scale=px.colors.sequential.GnBu, - mapbox_style="carto-positron", - zoom=2, - center={"lat": 62.995158, "lon": 88.048713}, - opacity=0.5, - labels={ - "OBS_VALUE": "Value", - "REF_AREA": "ISO3 Code", - "TIME_PERIOD": "Year", - "Country_name": "Country", - }, - hover_data={ - "OBS_VALUE": True, - "REF_AREA": False, - "Country_name": True, - "TIME_PERIOD": True, - }, - animation_frame="TIME_PERIOD", - height=750, - ), - "indicators": [ - "EDUNF_ROFST_L1", - "EDUNF_ROFST_L2", - "EDUNF_ROFST_L3", - "EDUNF_OFST_L1", - "EDUNF_OFST_L2", - "EDUNF_OFST_L3", - "EDU_SDG_PRYA", - "EDUNF_ROFST_L1_UNDER1", - "EDUNF_ROFST_L1T3", - ], - "default": "EDUNF_ROFST_L1", - }, - "AREA_1": { - "name": "Education entry and transition", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - # "compare": "Sex", - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "bar", - "indicators": [ - "EDUNF_ROFST_L1", - "EDUNF_ROFST_L2", - "EDUNF_ROFST_L3", - "EDUNF_ROFST_L1_UNDER1", - "EDUNF_ROFST_L1T3", - "EDUNF_STU_L1_TOT", - "EDUNF_STU_L2_TOT", - "EDUNF_STU_L3_TOT", - "EDUNF_GER_L2_GEN", - "EDUNF_GER_L2_VOC", - "EDUNF_GER_L3", - "EDUNF_GER_L3_GEN", - "EDUNF_GER_L3_VOC", - ], - "default": "EDUNF_ROFST_L1", - }, - "AREA_2": { - "name": "Education entry and transition", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - # "compare": "Sex", - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "line", - "indicators": [ - "EDUNF_NER_L02", - "EDUNF_NERA_L1_UNDER1", - "EDUNF_NERA_L1", - "EDUNF_NERA_L2", - "EDUNF_GER_L1", - "EDUNF_GER_L2", - "EDUNF_GER_L3", - "EDUNF_NIR_L1_ENTRYAGE", - "EDUNF_TRANRA_L2", - ], - "default": "EDUNF_TRANRA_L2", - }, - "AREA_3": { - "name": "Safe and inclusive learning environments", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - # "compare": "Sex", - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "bar", - "indicators": [ - "EDU_SDG_SCH_L1", - "EDU_SDG_SCH_L2", - "EDU_SDG_SCH_L3", - "WS_SCH_H-B", - "WS_SCH_S-B", - "WS_SCH_W-B", - "EDU_CHLD_DISAB", - "EDU_CHLD_DISAB_GENERAL", - "EDU_CHLD_DISAB_SPECIAL", - "EDU_CHLD_DISAB_GENERAL_BOARDING", - "EDU_CHLD_DISAB_SPECIAL_BOARDING", - "EDU_CHLD_DISAB_HOME", - "EDU_SDG_PRYA", - ], - "default": "EDU_CHLD_DISAB", - }, - "AREA_4": { - "name": "Education completion", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - # "compare": "Sex", - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "bar", - "indicators": [ - "EDUNF_CR_L1", - "EDUNF_CR_L2", - "EDUNF_CR_L3", - "EDUNF_DR_L1", - "EDUNF_DR_L2", - "EDUNF_GER_L2", - ], - "default": "EDUNF_CR_L1", - }, - }, - "QUALITY": { - "NAME": "Learning quality and skills", - "CARDS": [ - { - "name": "(enrolled in the same grade for a second or further year) in primary and lower secondary education", - "indicator": "EDUNF_RPTR_L1,EDUNF_RPTR_L2", - "suffix": "Children and adolescent repeaters", - }, - { - "name": "from primary education", - "indicator": "EDUNF_ESL_L1", - "suffix": "Early school leavers", - }, - { - "name": "administering nationally representative learning assessment in both reading and math at the end of primary education", - "indicator": "EDUNF_ADMIN_L1_GLAST_REA,EDUNF_ADMIN_L1_GLAST_MAT", - "suffix": "Countries", - }, - { - "name": "participating in the latest round of PISA", - "indicator": "EDU_PISA_MAT,EDU_PISA_REA,EDU_PISA_SCI", - "suffix": "Countries", - "absolute": True, - }, - ], - "MAIN": { - "name": "What students know and can do", - "geo": "Country_name", - "options": dict( - locations="REF_AREA", - featureidkey="id", - color="OBS_VALUE", - color_continuous_scale=px.colors.sequential.GnBu, - mapbox_style="carto-positron", - zoom=2, - center={"lat": 62.995158, "lon": 88.048713}, - opacity=0.5, - labels={ - "OBS_VALUE": "Value", - "Country_name": "Country", - "TIME_PERIOD": "Year", - "REF_AREA": "ISO3 Code", - }, - hover_data={ - "OBS_VALUE": True, - "REF_AREA": False, - "Country_name": True, - "TIME_PERIOD": True, - }, - animation_frame="TIME_PERIOD", - height=750, - ), - "indicators": ["EDU_PISA_MAT", "EDU_PISA_REA", "EDU_PISA_SCI"], - "default": "EDU_PISA_MAT", - }, - "AREA_1": { - "name": "Foundational skills", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - # "compare": "Sex", - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "bar", - "indicators": [ - "ECD_CHLD_36-59M_LMPSL", - "EDU_SDG_STU_L2_GLAST_MAT", - "EDU_SDG_STU_L2_GLAST_REA", - "EDU_SDG_STU_L1_GLAST_MAT", - "EDU_SDG_STU_L1_G2OR3_MAT", - "EDU_SDG_STU_L1_GLAST_REA", - "EDU_SDG_STU_L1_G2OR3_REA", - "EDUNF_LR_YOUTH", - "EDUNF_LR_ADULT", - ], - "default": "EDU_SDG_STU_L2_GLAST_MAT", - }, - "AREA_2": { - "name": "Foundational skills", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - # "compare": "Sex", - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "line", - "indicators": [ - "ECD_CHLD_36-59M_LMPSL", - "EDU_SDG_STU_L2_GLAST_MAT", - "EDU_SDG_STU_L2_GLAST_REA", - "EDU_SDG_STU_L1_GLAST_MAT", - "EDU_SDG_STU_L1_G2OR3_MAT", - "EDU_SDG_STU_L1_GLAST_REA", - "EDU_SDG_STU_L1_G2OR3_REA", - "EDUNF_LR_YOUTH", - "EDUNF_LR_ADULT", - ], - "default": "EDU_SDG_STU_L2_GLAST_MAT", - }, - "AREA_3": { - "name": "Trained and qualified teachers", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - # "compare": "Sex", - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "bar", - "indicators": [ - "EDU_SDG_TRTP_L02", - "EDU_SDG_TRTP_L1", - "EDU_SDG_TRTP_L2", - "EDU_SDG_TRTP_L3", - ], - "default": "EDU_SDG_TRTP_L2", - }, - "AREA_4": { - "name": "Trained and qualified teachers", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - # "compare": "Sex", - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "bar", - "indicators": [ - "EDU_SDG_QUTP_L02", - "EDU_SDG_QUTP_L1", - "EDU_SDG_QUTP_L2", - "EDU_SDG_QUTP_L3", - ], - "default": "EDU_SDG_QUTP_L2", - }, - }, - "SYSTEM": { - "NAME": "Education System", - "CARDS": [ - { - "name": "Guaranteeing at least one year of free pre-primary education in their legal frameworks", - "indicator": "EDU_SDG_FREE_EDU_L02", - "suffix": "Countries", - }, - { - "name": "Enrolled in private institutions (primary, lower secondary and upper secondary education)", - "indicator": "EDUNF_STU_L1_PRV,EDUNF_STU_L2_PRV,EDUNF_STU_L3_PRV", - "suffix": "Children and Adolescents", - }, - { - "name": "Total in primary, lower secondary and upper secondary education", - "indicator": "EDUNF_TEACH_L1,EDUNF_TEACH_L2,EDUNF_TEACH_L3", - "suffix": "Classroom Teachers", + "age": "_T", }, ], - "MAIN": { - "name": "Guaranteeing and paying for education", # "Education Expenditures and Legal Frameworks", - "geo": "Country_name", - "options": dict( - locations="REF_AREA", - featureidkey="id", - color="OBS_VALUE", - color_continuous_scale=px.colors.sequential.GnBu, - mapbox_style="carto-positron", - zoom=2, - center={"lat": 62.995158, "lon": 88.048713}, - opacity=0.5, - labels={ - "OBS_VALUE": "Value", - "Country_name": "Country", - "TIME_PERIOD": "Year", - "REF_AREA": "ISO3 Code", - }, - hover_data={ - "OBS_VALUE": True, - "REF_AREA": False, - "Country_name": True, - "TIME_PERIOD": True, - }, - animation_frame="TIME_PERIOD", - height=750, - ), - "indicators": [ - "EDU_FIN_EXP_PT_GDP", - "EDU_FIN_EXP_PT_TOT", - "EDU_SDG_FREE_EDU_L02", - "EDU_SDG_COMP_EDU_L02", - "EDU_FIN_EXP_CONST_PPP", - ], - "default": "EDU_FIN_EXP_PT_GDP", - }, - "AREA_1": { - "name": "Public and private enrolments", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - # "compare": "Sex", - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "bar", - "indicators": [ - "EDUNF_PRP_L02", - "EDUNF_PRP_L1", - "EDUNF_PRP_L2", - "EDUNF_PRP_L3", - "EDUNF_STU_L01_PUB", - "EDUNF_STU_L02_PUB", - "EDUNF_STU_L1_PUB", - "EDUNF_STU_L2_PUB", - "EDUNF_STU_L3_PUB", - "EDUNF_STU_L01_PRV", - "EDUNF_STU_L02_PRV", - "EDUNF_STU_L1_PRV", - "EDUNF_STU_L2_PRV", - "EDUNF_STU_L3_PRV", - ], - "default": "EDUNF_PRP_L1", - }, - "AREA_2": { - "name": "Public and private enrolments", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - # "compare": "Sex", - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "line", - "indicators": [ - "EDUNF_PRP_L02", - "EDUNF_PRP_L1", - "EDUNF_PRP_L2", - "EDUNF_PRP_L3", - "EDUNF_STU_L01_PUB", - "EDUNF_STU_L02_PUB", - "EDUNF_STU_L1_PUB", - "EDUNF_STU_L2_PUB", - "EDUNF_STU_L3_PUB", - "EDUNF_STU_L01_PRV", - "EDUNF_STU_L02_PRV", - "EDUNF_STU_L1_PRV", - "EDUNF_STU_L2_PRV", - "EDUNF_STU_L3_PRV", - ], - "default": "EDUNF_PRP_L2", - }, - "AREA_3": { - "name": "Government education expenditure", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - # "compare": "Sex", - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "bar", - "indicators": [ - "EDU_FIN_EXP_L02", - "EDU_FIN_EXP_L1", - "EDU_FIN_EXP_L2", - "EDU_FIN_EXP_L3", - "EDU_FIN_EXP_L4", - "EDU_FIN_EXP_L5T8", - "EDU_FIN_EXP_CONST_PPP", - ], - "default": "EDU_FIN_EXP_L2", - }, - "AREA_4": { - "name": "Administration of learning assessments", - "graphs": { - "bar": { - "options": dict( - x="Country_name", - y="OBS_VALUE", - barmode="group", - # text="TIME_PERIOD", - text="OBS_VALUE", - hover_name="TIME_PERIOD", - ), - }, - "line": { - "options": dict( - x="TIME_PERIOD", - y="OBS_VALUE", - color="Country_name", - hover_name="Country_name", - line_shape="spline", - render_mode="svg", - ), - "trace_options": dict(mode="lines+markers"), - }, - }, - "default_graph": "bar", - "indicators": { - "EDUNF_ADMIN_L1_GLAST_REA": {}, - "EDUNF_ADMIN_L1_GLAST_MAT": {}, - "EDUNF_ADMIN_L2_REA": {}, - "EDUNF_ADMIN_L2_MAT": {"dtype": "str"}, - "EDUNF_ADMIN_L1_G2OR3_REA": {}, - "EDUNF_ADMIN_L1_G2OR3_MAT": {}, - }, - "default": "EDUNF_ADMIN_L2_MAT", - }, }, } -main_title = "Education, Leisure, and Culture" +main_title = "Education" def get_layout(**kwargs): kwargs["indicators"] = indicators_dict kwargs["main_title"] = main_title - return get_base_layout(**kwargs) + return get_base_layout(**kwargs) \ No newline at end of file diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/home.py b/transmonee_dashboard/src/transmonee_dashboard/pages/home.py index f5aff8f1..3e1fd207 100644 --- a/transmonee_dashboard/src/transmonee_dashboard/pages/home.py +++ b/transmonee_dashboard/src/transmonee_dashboard/pages/home.py @@ -39,15 +39,9 @@ def get_layout(**kwargs): # dbc.CardHeader(html.H3("State of Children Rights")), dbc.CardBody( [ - html.Img( - src="assets/home.png", - className="rounded mx-auto d-block", - ), - html.Br(), - # html.H4( - # "What you can find here...", - # className="card-title", - # ), + html.Div([ + html.P("Brazil info") + ]) ] ), ] diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/__init__.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/__init__.py new file mode 100644 index 00000000..46cd8726 --- /dev/null +++ b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/__init__.py @@ -0,0 +1,1074 @@ +import json +import logging +import pathlib +import collections +from io import BytesIO +from re import L +import urllib + +import dash_html_components as html +import numpy as np +import pandas as pd +import requests +from requests.exceptions import HTTPError + +import pandasdmx as sdmx + + +# TODO: Move all of these to env/setting vars from production +sdmx_url = "https://sdmx.data.unicef.org/ws/public/sdmxapi/rest/data/ECARO,TRANSMONEE,1.0/.{}....?format=csv&startPeriod={}&endPeriod={}" + +geo_json_file = ( + pathlib.Path(__file__).parent.parent.absolute() / "assets/countries.geo.json" +) +with open(geo_json_file) as shapes_file: + geo_json_countries = json.load(shapes_file) + +with open( + pathlib.Path(__file__).parent.parent.absolute() / "assets/indicator_config.json" +) as config_file: + indicators_config = json.load(config_file) + +unicef = sdmx.Request("UNICEF") + +metadata = unicef.dataflow("TRANSMONEE", provider="ECARO", version="1.0") +dsd = metadata.structure["DSD_ECARO_TRANSMONEE"] + +indicator_names = { + code.id: code.name.en + for code in dsd.dimensions.get("INDICATOR").local_representation.enumerated +} +# lbassil: get the age groups code list as it is not in the DSD +cl_age = unicef.codelist("CL_AGE", version="1.0") +age_groups = sdmx.to_pandas(cl_age) +dict_age_groups = age_groups["codelist"]["CL_AGE"].reset_index() +age_groups_names = { + age["CL_AGE"]: age["name"] + for index, age in dict_age_groups.iterrows() + if age["CL_AGE"] != "_T" +} + +units_names = { + unit.id: str(unit.name) + for unit in dsd.attributes.get("UNIT_MEASURE").local_representation.enumerated +} + +# lbassil: get the names of the residence dimensions +residence_names = { + residence.id: str(residence.name) + for residence in dsd.dimensions.get("RESIDENCE").local_representation.enumerated +} + +# lbassil: get the names of the wealth quintiles dimensions +wealth_names = { + wealth.id: str(wealth.name) + for wealth in dsd.dimensions.get("WEALTH_QUINTILE").local_representation.enumerated +} + +gender_names = {"F": "Female", "M": "Male", "_T": "Total"} + +dimension_names = { + "SEX": "Sex_name", + "AGE": "Age_name", + "RESIDENCE": "Residence_name", + "WEALTH_QUINTILE": "Wealth_name", +} + +adolescent_codes = [ + "DM_ASYL_FRST", + "DM_ASYL_UASC", + "HT_ADOL_UNMETMED_FEAR", + "HT_ADOL_UNMETMED_HOPING", + "HT_ADOL_UNMETMED_NOKNOW", + "HT_ADOL_UNMETMED_NOTIME", + "HT_ADOL_UNMETMED_NOUNMET", + "HT_ADOL_UNMETMED_TOOEFW", + "HT_ADOL_UNMETMED_TOOEXP", + "HT_ADOL_UNMETMED_TOOFAR", + "HT_ADOL_UNMETMED_WAITING", + "HT_ADOL_UNMETMED_OTH", + "HT_CDRT_SELF_HARM", + "HT_SH_HIV_INCD", + "HVA_EPI_LHIV_0-19", + "HVA_EPI_LHIV_15-24", + "JJ_PRISIONERS_RT", + "MNCH_CSEC", + "MNCH_PNCMOM", + "PT_ADLT_PS_NEC", + "PT_CHLD_1-14_PS-PSY-V_CGVR", + "PT_CHLD_5-17_LBR_ECON", + "PT_CHLD_5-17_LBR_ECON-HC", + "PT_CHLD_CARED_BY_FOSTER", + "PT_CHLD_ENTEREDFOSTER", + "PT_CHLD_INRESIDENTIAL", + "PT_F_15-49_W-BTNG", + "PT_F_GE15_PS-SX-EM_V_PTNR_12MNTH", + "PT_M_15-49_W-BTNG", + "PT_ST_13-15_BUL_30-DYS", + "PV_AROPE", + "PV_AROPRT", + "PV_SD_MDP_MUHC", + "PV_SEV_MAT_DPRT", + "PV_SI_POV_EMP1", +] + +adolescent_age_groups = [ + "_T", + "Y14T17", + "Y0T13", + "Y1T4", + "Y0", + "Y0T14", + "Y14T15", + "Y16T17", + "Y16T19", + "Y16T24", + "Y10T14", + "Y15T19", + "Y15T24", + "Y10T19", + "Y0T17", + "Y15T49", + "Y20T24", + "Y18T49", + "Y18T29", + "Y0T24", + "Y25T39", + "Y40T59", + "Y_GE50", + "Y_GE15", + "Y1T14", + "Y2T14", + "Y5T14", + "Y5T17", + "Y7T17", + "Y10T17", + "Y13T15", + "Y15", + "Y11T15", + "Y12T17", + "Y0T15", + "Y0T4", + "M0", +] + +# Add all indicators found in the data dictionary to get their data to the query data page +data_query_codes = [ + "DM_BRTS", + # "DM_POP_TOT", + # "DM_AVG_POP_TOT", + # "DM_POP_PROP", + "DM_DPR_AGE", + "DM_DPR_CHD", + "DM_DPR_OLD", + # "DM_IMG", + # "DM_EMG", + # "DM_NEXTRT_MG", + "DM_MRG_AGE", + "DM_DIV", + "DM_CRDIVRT", + "DM_CHLD_DIV", + "DM_CHLDRT_DIV", + "DM_POP_TOT_AGE", + "FT_WHS_PBR", + "MT_SP_DYN_CDRT_IN", + "DM_LIFE_EXP", + "HT_SH_HAP_HBSAG", + "HT_SH_TBS_INCD", + "HT_SH_SUD_ALCOL", + "HT_DIST79DTP3_P", + "IM_MCV2", + "HT_DIST79MCV2_P", + "HT_SDG_PM25", + "ECD_CHLD_36-59M_ADLT_SRC", + "HT_SH_SUD_TREAT", + "EDUNF_STU_L01_TOT", + "EDU_SDG_GER_L01", + "EDUNF_STU_L02_TOT", + "EDUNF_FEP_L02", + "EDUNF_NARA_L1_UNDER1", + "EDUNF_FEP_L1", + "EDUNF_FEP_L2", + "EDUNF_FEP_L3", + "EDUNF_STU_L3_GEN", + "EDUNF_STU_L3_VOC", + "EDUNF_STU_L3_GEN_PUB", + "EDUNF_STU_L3_GEN_PRV", + "EDUNF_STU_L3_VOC_PUB", + "EDUNF_STU_L3_VOC_PRV", + "EDUNF_GER_L1AND2", + "EDU_TIMSS_MAT4", + "EDU_TIMSS_SCI4", + "EDU_TIMSS_MAT8", + "EDU_TIMSS_SCI8", + "EDU_PIRLS_REA", + "EDUNF_REPP_L1", + "EDUNF_REPP_L2", + "EDUNF_FRP_L1", + "EDUNF_SR_L1", + "EDUNF_SR_L2", + "EDUNF_STU_L4_TOT", + "EDUNF_STU_L4_PUB", + "EDUNF_STU_L4_PRV", + "EDUNF_PRP_L4", + "EDUNF_FEP_L4", + "EDUNF_STU_L5T8_TOT", + "EDUNF_STU_L5T8_PUB", + "EDUNF_STU_L5T8_PRV", + "EDUNF_PRP_L5T8", + "EDUNF_FEP_L5T8", + "EDUNF_GER_GPI_L02", + "EDUNF_GER_GPI_L1", + "EDUNF_GER_GPI_L2", + "EDUNF_GER_GPI_L3", + "EDUNF_GER_GPI_L2AND3", + "EDUNF_PTR_L1", + "EDUNF_PTR_L2", + "EDUNF_PTR_L2AND3", + "EDUNF_PTR_L3", + "EDU_SDG_PTTR_L02", + "EDU_SDG_PTTR_L1", + "EDU_SDG_PTTR_L2", + "EDU_SDG_PTTR_L3", + "EDU_SDG_PQTR_L02", + "EDU_SDG_PQTR_L1", + "EDU_SDG_PQTR_L2", + "EDU_SDG_PQTR_L3", + "ECD_CHLD_U5_BKS-HM", + "ECD_CHLD_U5_PLYTH-HM", + "EDUNF_EA_L2T8", + "EDU_PISA_MAT2", + "EDU_PISA_MAT3", + "EDU_PISA_MAT4", + "EDU_PISA_MAT5", + "EDU_PISA_MAT6", + "EDU_PISA_REA2", + "EDU_PISA_REA3", + "EDU_PISA_REA4", + "EDU_PISA_REA5", + "EDU_PISA_REA6", + "EDU_PISA_SCI2", + "EDU_PISA_SCI3", + "EDU_PISA_SCI4", + "EDU_PISA_SCI5", + "EDU_PISA_SCI6", + "EDU_CHLD_DISAB_L02", + "EDU_CHLD_DISAB_L1", + "EDU_CHLD_DISAB_L2", + "EDU_CHLD_DISAB_L3", + "EDUNF_SAP_L02", + "EDUNF_SAP_L1", + "EDUNF_SAP_L2", + "EDUNF_SAP_L3", + "EDUNF_SAP_L2_GLAST", + "EDUNF_FRP_L2AND3", + "EDUNF_GER_GPI_L01", + "EDUNF_OFST_L1T3", + "EDUNF_PRP_L2AND3", + "EDUNF_STU_L2AND3_PRV", + "EDUNF_COMP_YR", + "EDUNF_COMP_YR_L02T3", + "EDUNF_PTR_L02", + "EDUNF_GECER_L01", + "EDUNF_GECER_L02", + "ED_ANAR_L1", + "ED_ANAR_L2", + "ED_ANAR_L3", + "EDU_SE_ACS_INTNT_L1", + "EDU_SE_ACS_INTNT_L2", + "EDU_SE_ACS_INTNT_L3", + "EDU_SE_ACS_CMPTR_L1", + "EDU_SE_ACS_CMPTR_L2", + "EDU_SE_ACS_CMPTR_L3", + "EDU_SE_ACS_ELECT_L1", + "EDU_SE_ACS_ELECT_L2", + "EDU_SE_ACS_ELECT_L3", + "EDU_SE_TOT_GPI_L1_REA", + "EDU_SE_TOT_GPI_L2_REA", + "EDU_SE_TOT_GPI_L1_MAT", + "EDU_SE_TOT_GPI_L2_MAT", + "EDU_SE_TOT_GPI_FS_LIT", + "EDU_SE_TOT_GPI_FS_NUM", + "EDU_SE_AGP_CPRA_L1", + "EDU_SE_AGP_CPRA_L2", + "EDU_SE_AGP_CPRA_L3", + "EDU_SE_GPI_PART", + "EDU_SE_GPI_PTNPRE", + "EDU_SE_GPI_TCAQ_L02", + "EDU_SE_GPI_TCAQ_L1", + "EDU_SE_GPI_TCAQ_L2", + "EDU_SE_GPI_TCAQ_L3", + "EDU_SE_NAP_ACHI_L1_REA", + "EDU_SE_NAP_ACHI_L2_REA", + "EDU_SE_NAP_ACHI_L1_MAT", + "EDU_SE_NAP_ACHI_L2_MAT", + "EDU_SE_IMP_FPOF_LIT", + "EDU_SE_IMP_FPOF_NUM", + "EDU_SE_LGP_ACHI_L1_REA", + "EDU_SE_LGP_ACHI_L2_REA", + "EDU_SE_LGP_ACHI_L1_MAT", + "EDU_SE_LGP_ACHI_L2_MAT", + "EDU_SE_ALP_CPLR_L1", + "EDU_SE_ALP_CPLR_L2", + "EDU_SE_ALP_CPLR_L3", + "EDU_SE_TOT_SESPI_L1_REA", + "EDU_SE_TOT_SESPI_L2_REA", + "EDU_SE_TOT_SESPI_L1_MAT", + "EDU_SE_TOT_SESPI_L2_MAT", + "EDU_SE_TOT_SESPI_FS_LIT", + "EDU_SE_TOT_SESPI_FS_NUM", + "EDU_SE_TOT_RUPI_L1_REA", + "EDU_SE_TOT_RUPI_L2_REA", + "EDU_SE_TOT_RUPI_L1_MAT", + "EDU_SE_TOT_RUPI_L2_MAT", + "EDU_SE_AWP_CPRA_L1", + "EDU_SE_AWP_CPRA_L2", + "EDU_SE_AWP_CPRA_L3", + "EDU_SE_GPI_ICTS_ATCH", + "EDU_SE_GPI_ICTS_CPT", + "EDU_SE_GPI_ICTS_CDV", + "EDU_SE_GPI_ICTS_SSHT", + "EDU_SE_GPI_ICTS_PRGM", + "EDU_SE_GPI_ICTS_PST", + "EDU_SE_GPI_ICTS_SFWR", + "EDU_SE_GPI_ICTS_TRFF", + "EDU_SE_GPI_ICTS_CMFL", + "EDUNF_STEM_GRAD_RT", + "DM_TOT_POP_PROSP", + "DM_SP_POP_BRTH_MF", + "DM_ADOL_YOUTH_POP", + "DM_REPD_AGE_POP", + "GN_MTNTY_LV_BNFTS", + "GN_PTNTY_LV_BNFTS", + "EC_GDI", + "EC_HCI_OVRL", + "EC_MIN_WAGE", + "EC_IQ_CPA_GNDR_XQ", + "EC_SIGI", + "EC_YOUTH_UNE_RT", + "EC_EAP_RT", + "EC_GNI_PCAP_PPP", + "EC_FB_BNK_ACCSS", + "SL_DOM_TSPD", + "SG_GEN_PARL", + "CR_VC_VOV_GDSD", + "PT_ADLS_10-14_LBR_HC", + "ECD_CHLD_U5_LFT-ALN", + "MNCH_MATERNAL_DEATHS", + "MNCH_SH_MMR_RISK", + "MNCH_SH_MMR_RISK_ZS", + "MNCH_INSTDEL", + "MNCH_BIRTH18", + "HT_NCD_BMI_18A", + "HVA_EPI_INF_ANN_15-24", + "CR_CCRI_VUL_HT", + "CR_CCRI_VUL_EDU", + "CR_CCRI_VUL_WASH", + "CR_CCRI_VUL_SP", + "CR_CCRI_VUL_ES", + "CR_CCRI", + "CR_CCRI_EXP_WS", + "CR_CCRI_EXP_RF", + "CR_CCRI_EXP_CF", + "CR_CCRI_EXP_TC", + "CR_CCRI_EXP_VBD", + "CR_CCRI_EXP_HEAT", + "CR_CCRI_EXP_AP", + "CR_CCRI_EXP_SWP", + "CR_CCRI_EXP_CESS", + "CR_UN_CHLD_RIGHTS", + "CR_UN_CHLD_SALE", + "CR_UN_RIGHTS_DISAB", +] + +years = list(range(2010, 2022)) + +# a key:value dictionary of countries where the 'key' is the country name as displayed in the selection +# tree whereas the 'value' is the country name as returned by the sdmx list: https://sdmx.data.unicef.org/ws/public/sdmxapi/rest/codelist/UNICEF/CL_COUNTRY/1.0 +countries_iso3_dict = { + "Albania": "ALB", + "Andorra": "AND", + "Armenia": "ARM", + "Austria": "AUT", + "Azerbaijan": "AZE", + "Belarus": "BLR", + "Belgium": "BEL", + "Bosnia and Herzegovina": "BIH", + "Bulgaria": "BGR", + "Croatia": "HRV", + "Cyprus": "CYP", + "Czech Republic": "CZE", + "Denmark": "DNK", + "Estonia": "EST", + "Finland": "FIN", + "France": "FRA", + "Georgia": "GEO", + "Germany": "DEU", + "Greece": "GRC", + "Holy See": "VAT", + "Hungary": "HUN", + "Iceland": "ISL", + "Ireland": "IRL", + "Italy": "ITA", + "Kazakhstan": "KAZ", + "Kosovo (UN SC resolution 1244)": "XKX", # UNDP defines it as KOS + "Kyrgyzstan": "KGZ", + "Latvia": "LVA", + "Liechtenstein": "LIE", + "Lithuania": "LTU", + "Luxembourg": "LUX", + "Malta": "MLT", + "Monaco": "MCO", + "Montenegro": "MNE", + "Netherlands": "NLD", + "North Macedonia": "MKD", + "Norway": "NOR", + "Poland": "POL", + "Portugal": "PRT", + "Republic of Moldova": "MDA", + "Romania": "ROU", + "Russian Federation": "RUS", + "San Marino": "SMR", + "Serbia": "SRB", + "Slovakia": "SVK", + "Slovenia": "SVN", + "Spain": "ESP", + "Sweden": "SWE", + "Switzerland": "CHE", + "Tajikistan": "TJK", + "Turkey": "TUR", + "Turkmenistan": "TKM", + "Ukraine": "UKR", + "United Kingdom": "GBR", + "Uzbekistan": "UZB", +} + +# create a list of country names in the same order as the countries_iso3_dict +countries = list(countries_iso3_dict.keys()) + +unicef_country_prog = [ + "Albania", + "Armenia", + "Azerbaijan", + "Belarus", + "Bosnia and Herzegovina", + "Bulgaria", + "Croatia", + "Georgia", + "Greece", + "Kazakhstan", + "Kosovo (UN SC resolution 1244)", + "Kyrgyzstan", + "Montenegro", + "North Macedonia", + "Republic of Moldova", + "Romania", + "Serbia", + "Tajikistan", + "Turkey", + "Turkmenistan", + "Ukraine", + "Uzbekistan", +] + +country_selections = [ + { + "label": "Eastern Europe and Central Asia", + "value": [ + {"label": "Caucasus", "value": ["Armenia", "Azerbaijan", "Georgia"]}, + { + "label": "Western Balkans", + "value": [ + "Albania", + "Bosnia and Herzegovina", + "Croatia", + "Kosovo (UN SC resolution 1244)", + "North Macedonia", + "Montenegro", + "Serbia", + ], + }, + { + "label": "Central Asia", + "value": [ + "Kazakhstan", + "Kyrgyzstan", + "Tajikistan", + "Turkmenistan", + "Uzbekistan", + ], + }, + { + "label": "Eastern Europe", + "value": [ + "Bulgaria", + "Belarus", + "Republic of Moldova", + "Romania", + "Russian Federation", + "Turkey", + "Ukraine", + ], + }, + ], + }, + { + "label": "Western Europe", + "value": [ + "Andorra", + "Austria", + "Belgium", + "Cyprus", + "Czech Republic", + "Denmark", + "Estonia", + "Finland", + "France", + "Germany", + "Greece", + "Holy See", + "Hungary", + "Iceland", + "Ireland", + "Italy", + "Latvia", + "Liechtenstein", + "Lithuania", + "Luxembourg", + "Malta", + "Monaco", + "Netherlands", + "Norway", + "Poland", + "Portugal", + "San Marino", + "Slovakia", + "Slovenia", + "Spain", + "Sweden", + "Switzerland", + "United Kingdom", + ], + }, + { + "label": "By EU Engagement", + "value": [ + { + "label": "Central Asia", + "value": [ + "Kazakhstan", + "Kyrgyzstan", + "Tajikistan", + "Turkmenistan", + "Uzbekistan", + ], + }, + { + "label": "Eastern Partnership", + "value": [ + "Armenia", + "Azerbaijan", + "Belarus", + "Georgia", + "Republic of Moldova", + "Ukraine", + ], + }, + { + "label": "EFTA", + "value": ["Iceland", "Liechtenstein", "Norway", "Switzerland"], + }, + { + "label": "EU Member States", + "value": [ + "Andorra", + "Austria", + "Belgium", + "Bulgaria", + "Croatia", + "Cyprus", + "Czech Republic", + "Denmark", + "Estonia", + "Finland", + "France", + "Germany", + "Greece", + "Hungary", + "Ireland", + "Italy", + "Latvia", + "Lithuania", + "Luxembourg", + "Malta", + "Netherlands", + "Poland", + "Portugal", + "Romania", + "Slovakia", + "Slovenia", + "Spain", + "Sweden", + ], + }, + { + "label": "Other", + "value": [ + "Andorra", + "Monaco", + "Holy See", + "San Marino", + ], + }, + { + "label": "Pre-accession countries", + "value": [ + "Albania", + "Bosnia and Herzegovina", + "Kosovo (UN SC resolution 1244)", + "North Macedonia", + "Montenegro", + "Serbia", + "Turkey", + ], + }, + { + "label": "Russian Federation", + "value": ["Russian Federation"], + }, + { + "label": "United Kingdom (left EU on January 31, 2020)", + "value": ["United Kingdom"], + }, + ], + }, +] + +data_sources = { + "CDDEM": "CountDown 2030", + "CCRI": "Children's Climate Risk Index", + "UN Treaties": "UN Treaties", + "ESTAT": "Euro Stat", + "Helix": " Health Entrepreneurship and LIfestyle Xchange", + "ILO": "International Labour Organization", + "WHO": "World Health Organization", + "Immunization Monitoring (WHO)": "Immunization Monitoring (WHO)", + "WB": "World Bank", + "OECD": "Organisation for Economic Co-operation and Development", + "SDG": "Sustainable Development Goals", + "UIS": "UNESCO Institute for Statistics", + "UNDP": "United Nations Development Programme", + "TMEE": "Transformative Monitoring for Enhanced Equity", +} + +topics_subtopics = { + "All": ["All"], + "Education, Leisure, and Culture": [ + {"Participation": "Education access and participation"}, + {"Quality": "Learning quality and skills"}, + {"System": "Education system"}, + ], + "Family Environment and Protection": [ + {"Violence": "Violence against Children and Women"}, + {"Care": "Children without parental care"}, + {"Justice": "Justice for Children"}, + {"Marriage": "Child marriage and other harmful practices"}, + {"Labour": "Child labour and other forms of exploitation"}, + ], + "Health and Nutrition": [ + {"HS": "Health System"}, + {"MNCH": "Maternal, newborn and child health"}, + {"Immunization": "Immunization"}, + {"Nutrition": "Nutrition"}, + {"Adolescent": "Adolescent physical, mental, and reproductive health"}, + {"HIVAIDS": "HIV/AIDS"}, + {"Wash": "Water, sanitation and hygiene"}, + ], + "Poverty and Social Protection": [ + {"Poverty": "Child Poverty and Material Deprivation"}, + {"Protection": "Social protection system"}, + ], + "Child Rights Landscape and Governance": [ + {"Demography": "Demographics"}, + {"Economy": "Political Economy"}, + {"Migration": "Migration and Displacement"}, + {"Access": "Access to Justice"}, + {"Data": "Data on Children"}, + {"Spending": "Public spending on Children"}, + ], + "Participation and Civil Rights": [ + {"Registration": "Birth registration and identity"}, + {"Information": "Information, Internet and Protection of privacy"}, + {"Leisure": "Education, Leisure, and Culture"}, + ], +} + +dict_topics_subtopics = { + "Education, Leisure, and Culture": [ + "Education access and participation", + "Learning quality and skills", + "Education System", + ], + "Family Environment and Protection": [ + "Violence against Children and Women", + "Children without parental care", + "Justice for Children", + "Child marriage and other harmful practices", + "Child labour and other forms of exploitation", + ], + "Health and Nutrition": [ + "Health System", + "Maternal, newborn and child health", + "Immunization", + "Nutrition", + "Adolescent physical, mental, and reproductive health", + "HIV/AIDS", + "Water, sanitation and hygiene", + ], + "Poverty and Social Protection": [ + "Child Poverty and Material Deprivation", + "Social protection system", + ], + "Child Rights Landscape and Governance": [ + "Demographics", + "Political Economy", + "Migration and Displacement", + "Access to Justice", + "Data on Children", + "Public spending on Children", + ], + "Participation and Civil Rights": [ + "Birth registration and identity", + "Information, Internet and Protection of privacy", + "Education, Leisure, and Culture", + ], +} + + +def get_search_countries(add_all): + all_countries = {"label": "All", "value": "All"} + countries_list = [ + { + "label": key, + "value": countries_iso3_dict[key], + } + for key in countries_iso3_dict.keys() + ] + if add_all: + countries_list.insert(0, all_countries) + return countries_list + + +def get_sector(subtopic): + for key in dict_topics_subtopics.keys(): + if subtopic.strip() in dict_topics_subtopics.get(key): + return key + return "" + + +# function to check if the config of a certain indicator are only about its dtype +def only_dtype(config): + return list(config.keys()) == ["DTYPE"] + + +def get_filtered_dataset( + indicators: list, + years: list, + country_codes: list, + breakdown: str = "TOTAL", # send default breakdown as Total + dimensions: dict = {}, + latest_data: bool = True, +) -> pd.DataFrame: + + # TODO: This is temporary, need to move to config + # Add all dimensions by default to the keys + keys = { + "REF_AREA": country_codes, + "INDICATOR": indicators, + "SEX": [], + "AGE": [], + "RESIDENCE": [], + "WEALTH_QUINTILE": [], + } + + # get the first indicator of the list... we have more than one indicator in the cards + indicator_config = ( + indicators_config[indicators[0]] if indicators[0] in indicators_config else {} + ) + # check if the indicator has special config, update the keys from the config + if indicator_config and not only_dtype(indicator_config): + # TODO: need to confirm that a TOTAL is always available when a config is available for the indicator + card_keys = indicator_config[breakdown] + if ( + dimensions + ): # if we are sending cards related filters, update the keys with the set values + card_keys.update(dimensions) + keys.update(card_keys) # update the keys with the sent values + + try: + data = unicef.data( + "TRANSMONEE", + provider="ECARO", + key=keys, + params=dict( + startPeriod=years[0], + endPeriod=years[-1], + lastNObservations=1 if latest_data else 0, + ), + dsd=dsd, + ) + logging.debug(f"URL: {data.response.url} CACHED: {data.response.from_cache}") + except HTTPError as e: + logging.exception(f"URL: {e.response}", e) + # TODO: Maybe do something better here + return pd.DataFrame() + + # lbassil: add sorting by Year to display the years in proper order on the x-axis + dtype = ( + eval(indicator_config["DTYPE"]) if "DTYPE" in indicator_config else np.float64 + ) + data = ( + data.to_pandas(attributes="o", rtype="rows", dtype=dtype) + .sort_values(by=["TIME_PERIOD"]) + .reset_index() + ) + data.rename(columns={"value": "OBS_VALUE", "INDICATOR": "CODE"}, inplace=True) + # replace Yes by 1 and No by 0 + data.OBS_VALUE.replace({"Yes": "1", "No": "0", "<": "", ">": ""}, inplace=True) + + # convert to numeric and round + data["OBS_VALUE"] = pd.to_numeric(data.OBS_VALUE, errors="coerce") + data.dropna(subset=["OBS_VALUE"], inplace=True) + data = data.round({"OBS_VALUE": 2}) + # converting TIME_PERIOD to numeric: we should get integers by default + data["TIME_PERIOD"] = pd.to_numeric(data.TIME_PERIOD) + + # lbassil: add the code to fill the country names + countries_val_list = list(countries_iso3_dict.values()) + + def create_labels(row): + row["Country_name"] = countries[countries_val_list.index(row["REF_AREA"])] + row["Unit_name"] = str(units_names.get(str(row["UNIT_MEASURE"]), "")) + row["Sex_name"] = str(gender_names.get(str(row["SEX"]), "")) + row["Residence_name"] = str(residence_names.get(str(row["RESIDENCE"]), "")) + row["Wealth_name"] = str(wealth_names.get(str(row["WEALTH_QUINTILE"]), "")) + row["Age_name"] = str(age_groups_names.get(str(row["AGE"]), "")) + return row + + data = data.apply(create_labels, axis="columns") + + return data + + +# create two dicts, one for display tree and one with the index of all possible selections +selection_index = collections.OrderedDict({"0": countries}) +selection_tree = dict(title="Select All", key="0", children=[]) +for num1, group in enumerate(country_selections): + parent = dict(title=group["label"], key=f"0-{num1}", children=[]) + group_countries = [] + + for num2, region in enumerate(group["value"]): + child_region = dict( + title=region["label"] if "label" in region else region, + key=f"0-{num1}-{num2}", + children=[], + ) + parent.get("children").append(child_region) + if "value" in region: + selection_index[f"0-{num1}-{num2}"] = ( + region["value"] + if isinstance(region["value"], list) + else [region["value"]] + ) + for num3, country in enumerate(region["value"]): + child_country = dict(title=country, key=f"0-{num1}-{num2}-{num3}") + if len(region["value"]) > 1: + # only create child nodes for more then one child + child_region.get("children").append(child_country) + selection_index[f"0-{num1}-{num2}-{num3}"] = [country] + group_countries.append(country) + else: + selection_index[f"0-{num1}-{num2}"] = [region] + group_countries.append(region) + + selection_index[f"0-{num1}"] = group_countries + selection_tree.get("children").append(parent) + +programme_country_indexes = [ + next( + key + for key, value in selection_index.items() + if value[0] == item and len(value) == 1 + ) + for item in unicef_country_prog +] + +data = pd.DataFrame() +data_query_inds = set(data_query_codes) + +# column data types coerced +col_types = { + "COVERAGE_TIME": str, + "OBS_FOOTNOTE": str, + "OBS_VALUE": str, + "Frequency": str, + "Unit multiplier": str, + "OBS_STATUS": str, + "Observation Status": str, + "TIME_PERIOD": int, +} + +# avoid a loop to query SDMX +try: + data_query_sdmx = pd.read_csv( + sdmx_url.format("+".join(data_query_inds), years[0], years[-1]), + dtype=col_types, + storage_options={"Accept-Encoding": "gzip"}, + low_memory=False, + ) +except urllib.error.HTTPError as e: + raise e + +data = data.append(data_query_sdmx) +# no need to create column CODE, just rename indicator +data.rename(columns={"INDICATOR": "CODE"}, inplace=True) + +# replace Yes by 1 and No by 0 +data.OBS_VALUE.replace({"Yes": "1", "No": "0"}, inplace=True) + + +# check and drop non-numeric observations, eg: SDMX accepts > 95 as an OBS_VALUE +filter_non_num = pd.to_numeric(data.OBS_VALUE, errors="coerce").isnull() +if filter_non_num.any(): + not_num_code_val = data[["CODE", "OBS_VALUE"]][filter_non_num] + f"Non-numeric observations in {not_num_code_val.CODE.unique()}\ndiscarded: {not_num_code_val.OBS_VALUE.unique()}" + data.drop(data[filter_non_num].index, inplace=True) + +# convert to numeric +data["OBS_VALUE"] = pd.to_numeric(data.OBS_VALUE) +data = data.round({"OBS_VALUE": 2}) +# print(data.shape) + +# TODO: calculations for children age population +indicators = data["Indicator"].unique() + +# extract the indicators that have gender/sex disaggregation +gender_indicators = data.groupby("CODE").agg({"SEX": "nunique"}).reset_index() +# Keep only indicators with gender/sex disaggregation +gender_indicators = gender_indicators[gender_indicators["SEX"] > 1] + +# path to excel data dictionary in repo +github_url = "https://github.com/UNICEFECAR/data-etl/raw/proto_API/tmee/data_in/data_dictionary/indicator_dictionary_TM_v8.xlsx" +data_dict_content = requests.get(github_url).content +# Reading the downloaded content and turning it into a pandas dataframe and read Snapshot sheet from excel data-dictionary +snapshot_df = pd.read_excel(BytesIO(data_dict_content), sheet_name="Snapshot") +snapshot_df.dropna(subset=["Source_name"], inplace=True) +snapshot_df["Source"] = snapshot_df["Source_name"].apply(lambda x: x.split(":")[0]) +# read indicators table from excel data-dictionary +df_topics_subtopics = pd.read_excel(BytesIO(data_dict_content), sheet_name="Indicator") +df_topics_subtopics.dropna(subset=["Issue"], inplace=True) +df_sources = pd.merge(df_topics_subtopics, snapshot_df, how="outer", on=["Code"]) +# assign source = TMEE to all indicators without a source since they all come from excel data collection files +df_sources.fillna("TMEE", inplace=True) +# Concatenate sectors/subtopics dictionary value lists (mapping str lower) +sitan_subtopics = list(map(str.lower, sum(dict_topics_subtopics.values(), []))) + +df_sources.rename( + columns={ + "Name_x": "Indicator", + "Issue": "Subdomain", + }, + inplace=True, +) +# filter the sources to keep only sitan related sectors and sub-topics +df_sources["Subdomain"] = df_sources["Subdomain"].str.strip() +df_sources["Domain"] = df_sources["Subdomain"].apply( + lambda x: get_sector(x) if not pd.isna(x) else "" +) +df_sources["Source_Full"] = df_sources["Source"].apply( + lambda x: data_sources[x] if not pd.isna(x) else "" +) + +df_sources = df_sources[df_sources["Subdomain"].str.lower().isin(sitan_subtopics)] +# read source table from excel data-dictionary and merge +source_table_df = pd.read_excel(BytesIO(data_dict_content), sheet_name="Source") +df_sources = df_sources.merge( + source_table_df[["Source_Id", "Source_Link"]], + on="Source_Id", + how="left", + sort=False, +) +# assign source link for TMEE, url: UNICEF_RDM/indicator_code +tmee_source_link = df_sources.Source_Link.isnull() +unicef_rdm_url = "https://data.unicef.org/indicator-profile/{helix_code}/" +df_sources.loc[tmee_source_link, "Source_Link"] = df_sources[ + tmee_source_link +].Code.apply(lambda x: unicef_rdm_url.format(helix_code=x)) +df_sources_groups = df_sources.groupby("Source") +df_sources_summary_groups = df_sources.groupby("Source_Full") +# Extract the indicators' potential unique disaggregations. +# Group by indicator code and keep only unique aggregations for the 4 possible dimensions: +# Sex, Age, Residence and Wealth. +indicators_disagg = ( + data.groupby("CODE") + .agg( + { + "AGE": "nunique", + "SEX": "nunique", + "RESIDENCE": "nunique", + "WEALTH_QUINTILE": "nunique", + } + ) + .reset_index() +) +# Filter the dimensions with count greater than 1 which means Total is there (default) in addition to other possible values. +indicators_disagg_no_total = indicators_disagg[ + (indicators_disagg["AGE"] > 1) + | (indicators_disagg["SEX"] > 1) + | (indicators_disagg["RESIDENCE"] > 1) + | (indicators_disagg["WEALTH_QUINTILE"] > 1) +] + +# include the indicators with Total only to show in the data query +indicators_disagg_with_total = indicators_disagg[ + (indicators_disagg["AGE"] >= 1) + | (indicators_disagg["SEX"] >= 1) + | (indicators_disagg["RESIDENCE"] >= 1) + | (indicators_disagg["WEALTH_QUINTILE"] >= 1) +] +# Get the data for all the indicators having disaggregated data by any of the 4 dimensions. +indicators_disagg_details = data[ + data["CODE"].isin(indicators_disagg_with_total["CODE"]) +] + +# Filter the dataframe to be used in the data query to keep indicators code and the possible disaggregations. +indicators_disagg_details = indicators_disagg_details[ + ["CODE", "Age", "Sex", "Residence", "Wealth Quintile"] +] +indicators_disagg_details = indicators_disagg_details.drop_duplicates() + +# extract the indicators that have gender/sex disaggregation +age_indicators_counts = data.groupby("CODE").agg({"AGE": "nunique"}).reset_index() +# Keep only indicators with gender/sex disaggregation +age_indicators_counts = age_indicators_counts[age_indicators_counts["AGE"] > 1] +# age_indicators_counts.to_csv("age_indicators_counts.csv", index=True) +age_indicators = pd.merge(data, age_indicators_counts, on=["CODE"]) +age_indicators = age_indicators[["CODE", "Indicator", "Age"]] +age_indicators = age_indicators.drop_duplicates() +age_indicators = age_indicators.sort_values(by=["CODE", "Age"]) +# age_indicators.to_csv("age_indicators.csv", index=False) + +# extract the indicators that have gender/sex disaggregation +age_indicators_counts = data.groupby("CODE").agg({"AGE": "nunique"}).reset_index() +# Keep only indicators with gender/sex disaggregation +age_indicators_counts = age_indicators_counts[age_indicators_counts["AGE"] > 1] +# age_indicators_counts.to_csv("age_indicators_counts.csv", index=True) +age_indicators = pd.merge(data, age_indicators_counts, on=["CODE"]) +age_indicators = age_indicators[["CODE", "Indicator", "Age"]] +age_indicators = age_indicators.drop_duplicates() +age_indicators = age_indicators.sort_values(by=["CODE", "Age"]) +# age_indicators.to_csv("age_indicators.csv", index=False) + + +def page_not_found(pathname): + return html.P("No page '{}'".format(pathname)) diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/adolescent.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/adolescent.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/adolescent.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/adolescent.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/base_page.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/base_page.py new file mode 100644 index 00000000..4f7d9bfc --- /dev/null +++ b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/base_page.py @@ -0,0 +1,1096 @@ +import textwrap + +import dash +import dash_bootstrap_components as dbc +import dash_core_components as dcc +import dash_html_components as html +import dash_treeview_antd +import numpy as np +import plotly.express as px +import plotly.graph_objects as go +import plotly.io as pio +from dash.dependencies import MATCH, ClientsideFunction, Input, Output, State +from scipy.stats import zscore + +from ..app import app +from ..components import fa +from . import ( + countries, + countries_iso3_dict, + df_sources, + dimension_names, + geo_json_countries, + get_filtered_dataset, + indicator_names, + indicators_config, + programme_country_indexes, + selection_index, + selection_tree, + unicef_country_prog, + years, + get_search_countries, +) + +# set defaults +pio.templates.default = "plotly_white" +px.defaults.color_continuous_scale = px.colors.sequential.BuGn +px.defaults.color_discrete_sequence = px.colors.qualitative.Dark24 + +colours = [ + "primary", + "success", + "warning", + "danger", + "secondary", + "info", + "success", + "danger", +] +AREA_KEYS = ["MAIN", "AREA_1", "AREA_2", "AREA_3", "AREA_4", "AREA_5", "AREA_6"] +DEFAULT_LABELS = { + "Country_name": "Country", + "TIME_PERIOD": "Year", + "Sex_name": "Sex", + "Residence_name": "Residence", + "Age_name": "Age", + "Wealth_name": "Wealth Quintile", +} +CARD_TEXT_STYLE = {"textAlign": "center", "color": "#0074D9"} + +EMPTY_CHART = { + "layout": { + "xaxis": {"visible": False}, + "yaxis": {"visible": False}, + "annotations": [ + { + "text": "No data is available for the selected filters", + "xref": "paper", + "yref": "paper", + "showarrow": False, + "font": {"size": 28}, + } + ], + } +} + + +def make_area(area_name): + area_id = {"type": "area", "index": area_name} + popover_id = {"type": "area_sources", "index": area_name} + historical_data_style = {"display": "none"} + exclude_outliers_style = {"paddingLeft": 20, "display": "block"} + breakdowns_style = {"display": "block"} + + # lbassil: still differentiating main area id from other areas ids because the call backs are still not unified + if area_name == "MAIN": + area_id = f"{area_name.lower()}_area" + popover_id = f"{area_name.lower()}_area_sources" + historical_data_style = {"display": "block"} + exclude_outliers_style = {"display": "none"} + breakdowns_style = {"display": "none"} + + # lbassil: unifying both main and figure area generations by tweaking the ids and styles + area = dbc.Card( + [ + dbc.CardHeader( + id={"type": "area_title", "index": area_name}, + style={"fontWeight": "bold"}, + ), + dbc.CardBody( + [ + dcc.Dropdown( + id={"type": "area_options", "index": area_name}, + className="dcc_control", + ), + html.Br(), + dbc.Checklist( + options=[ + { + "label": "Show historical data", + "value": 1, + } + ], + value=[], + id={ + "type": "historical_data_toggle", + "index": area_name, + }, + switch=True, + style=historical_data_style, + ), + html.Br(), + dbc.RadioItems( + id={"type": "area_types", "index": area_name}, + inline=True, + ), + dcc.Loading([dcc.Graph(id=area_id)]), + dbc.Checklist( + options=[ + { + "label": "Exclude outliers ", + "value": 1, + } + ], + value=[1], + id={ + "type": "exclude_outliers_toggle", + "index": area_name, + }, + switch=True, + style=exclude_outliers_style, + ), + html.Br(), + dbc.RadioItems( + id={"type": "area_breakdowns", "index": area_name}, + inline=True, + style=breakdowns_style, + ), + html.Div( + fa("fas fa-info-circle"), + id=f"{area_name.lower()}_area_info", + className="float-right", + ), + dbc.Popover( + [ + dbc.PopoverHeader("Sources"), + dbc.PopoverBody(id=popover_id), + ], + id="hover", + target=f"{area_name.lower()}_area_info", + trigger="hover", + ), + ] + ), + ], + id={"type": "area_parent", "index": area_name}, + ) + return area + + +def get_base_layout(**kwargs): + indicators_dict = kwargs.get("indicators") + main_title = kwargs.get("main_title") + is_country_profile = kwargs.get("is_country_profile") + country_dropdown_style = {"display": "none"} + themes_row_style = {"verticalAlign": "center", "display": "flex"} + countries_filter_style = {"display": "block"} + programme_toggle_style = {"display": "block"} + main_area_style = {"display": "block"} + + if is_country_profile: + country_dropdown_style = { + "minWidth": 400, + "maxWidth": 600, + "paddingRight": 4, + "verticalAlign": "center", + "display": "visible", + } + themes_row_style = {"display": "none"} + countries_filter_style = {"display": "none"} + programme_toggle_style = {"display": "none"} + main_area_style = {"display": "none"} + + return html.Div( + [ + dcc.Store(id="indicators", data=indicators_dict), + dcc.Location(id="theme"), + html.Div( + className="heading", + style={"padding": 36}, + children=[ + html.Div( + className="heading-content", + children=[ + html.Div( + className="heading-panel", + style={"padding": 20}, + children=[ + html.H1( + main_title, + id="main_title", + className="heading-title", + ), + html.P( + id="subtitle", + className="heading-subtitle", + ), + ], + ), + ], + ) + ], + ), + dbc.Row( + children=[ + dbc.Col( + [ + dbc.Row( + [ + dbc.ButtonGroup( + id="themes", + ), + ], + id="theme-row", + # width=4, + className="my-2", + # no_gutters=True, + justify="center", + style=themes_row_style, + ), + dbc.Row( + [ + dbc.DropdownMenu( + label=f"Years: {years[0]} - {years[-1]}", + id="collapse-years-button", + className="m-2", + color="info", + # block=True, + children=[ + dbc.Card( + dcc.RangeSlider( + id="year_slider", + min=0, + max=len(years) - 1, + step=None, + marks={ + index: str(year) + for index, year in enumerate( + years + ) + }, + value=[0, len(years) - 1], + ), + style={ + "maxHeight": "250px", + "minWidth": "500px", + }, + className="overflow-auto", + body=True, + ), + ], + ), + dcc.Dropdown( + id="country_profile_selector", + options=get_search_countries(False), + value="ALB", + multi=False, + placeholder="Select a country...", + className="m-2", + style=country_dropdown_style, + ), + dbc.DropdownMenu( + label=f"Countries: {len(countries)}", + id="collapse-countries-button", + className="m-2", + color="info", + style=countries_filter_style, + children=[ + dbc.Card( + dash_treeview_antd.TreeView( + id="country_selector", + multiple=True, + checkable=True, + checked=["0"], + # selected=[], + expanded=["0"], + data=selection_tree, + ), + style={ + "maxHeight": "250px", + # "maxWidth": "300px", + }, + className="overflow-auto", + body=True, + ), + ], + ), + dbc.FormGroup( + [ + dbc.Checkbox( + id="programme-toggle", + className="custom-control-input", + ), + dbc.Label( + "UNICEF Country Programmes", + html_for="programme-toggle", + className="custom-control-label", + color="primary", + ), + ], + className="custom-control custom-switch m-2", + check=True, + inline=True, + style=programme_toggle_style, + ), + ], + id="filter-row", + no_gutters=True, + justify="center", + ), + ] + ), + ], + # sticky="top", + className="sticky-top bg-light", + ), + dbc.Row( + [ + dbc.CardDeck( + id="cards_row", + className="mt-3", + ), + ], + justify="center", + ), + html.Br(), + dbc.CardDeck( + [make_area(area) for area in ["MAIN"]], + style=main_area_style, + ), + html.Br(), + dbc.CardDeck( + [make_area(area) for area in ["AREA_1", "AREA_2"]], + ), + html.Br(), + dbc.CardDeck( + [make_area(area) for area in ["AREA_3", "AREA_4"]], + ), + html.Br(), + dbc.CardDeck( + [make_area(area) for area in ["AREA_5", "AREA_6"]], + ), + html.Br(), + ], + ) + + +def make_card( + card_id, + name, + suffix, + indicator_sources, + source_link, + indicator_header, + numerator_pairs, +): + card = dbc.Card( + [ + dbc.CardBody( + [ + html.H1( + indicator_header, + className="display-4", + style={ + # "fontSize": 50, + "textAlign": "center", + "color": "#1cabe2", + }, + ), + html.H4(suffix, className="card-title"), + html.P(name, className="lead"), + html.Div( + fa("fas fa-info-circle"), + id=f"{card_id}_info", + # className="float-right", + style={ + "position": "absolute", + "bottom": "10px", + "right": "10px", + }, + ), + ], + style={ + # "fontSize": 50, + "textAlign": "center", + }, + ), + dbc.Popover( + [ + dbc.PopoverHeader( + html.A( + html.P(f"Sources: {indicator_sources}"), + href=source_link, + target="_blank", + ) + ), + dbc.PopoverBody( + dcc.Markdown(get_card_popover_body(numerator_pairs)) + ), + ], + id="hover", + target=f"{card_id}_info", + trigger="hover", + ), + ], + color="primary", + outline=True, + id=card_id, + ) + return card + + +def get_card_popover_body(sources): + """This function is used to generate the list of countries that are part of the card's + displayed result; it displays the countries as a list, each on a separate line + + Args: + sources (_type_): _description_ + + Returns: + _type_: _description_ + """ + countries = [] + # lbassil: added this condition to stop the exception when sources is empty + if len(sources) > 0: + for index, source_info in sources.sort_values(by="OBS_VALUE").iterrows(): + countries.append(f"- {index[0]}, {source_info[0]} ({index[1]})") + card_countries = "\n".join(countries) + return card_countries + else: + return "NA" + + +# TODO: Move to client side call back +@app.callback( + Output("collapse-years", "is_open"), + Output("collapse-countries", "is_open"), + Output("collapse-engagements", "is_open"), + [ + Input("collapse-years-button", "n_clicks"), + Input("collapse-countries-button", "n_clicks"), + Input("collapse-engagements-button", "n_clicks"), + ], + [ + State("collapse-years-button", "is_open"), + State("collapse-countries-button", "is_open"), + State("collapse-engagements-button", "is_open"), + ], +) +def toggle_collapse(n1, n2, n3, is_open1, is_open2, is_open3): + ctx = dash.callback_context + + if not ctx.triggered: + return False, False, False + else: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + + if button_id == "collapse-years-button" and n1: + return not is_open1, False, False + elif button_id == "collapse-countries-button" and n2: + return False, not is_open2, False + elif button_id == "collapse-engagements-button" and n3: + return False, False, not is_open3 + return False, False, False + + +@app.callback( + Output({"type": "area_parent", "index": MATCH}, "hidden"), + Input("theme", "hash"), + [ + State("indicators", "data"), + State({"type": "area_parent", "index": MATCH}, "id"), + ], +) +def display_areas(theme, indicators_dict, id): + area = id["index"] + theme = theme[1:].upper() if theme else next(iter(indicators_dict.keys())) + return area not in indicators_dict[theme] + + +@app.callback( + Output("store", "data"), + Output("country_selector", "checked"), + Output("programme-toggle", "checked"), + Output("collapse-years-button", "label"), + Output("collapse-countries-button", "label"), + [ + Input("theme", "hash"), + Input("year_slider", "value"), + Input("country_selector", "checked"), + Input("programme-toggle", "checked"), + Input("country_profile_selector", "value"), + ], + State("indicators", "data"), +) +def apply_filters( + theme, + years_slider, + country_selector, + programme_toggle, + selected_country, + indicators, +): + ctx = dash.callback_context + selected = ctx.triggered[0]["prop_id"].split(".")[0] + countries_selected = set() + current_theme = theme[1:].upper() if theme else next(iter(indicators.keys())) + # check if it is the country profile page + is_country_profile = current_theme == "COUNTRYPROFILE" + # check if the user clicked on the generate button in the country profile page + if is_country_profile: + key_list = list(countries_iso3_dict.keys()) + val_list = list(countries_iso3_dict.values()) + # get the name of the selected country in the dropdown to filter the data accordingly + countries_selected = ( + [key_list[val_list.index(selected_country)]] if selected_country else [] + ) + elif programme_toggle and selected == "programme-toggle": + countries_selected = unicef_country_prog + country_selector = programme_country_indexes + # Add the condition to know when the user unchecks the UNICEF country programs! + elif not country_selector or ( + not programme_toggle and selected == "programme-toggle" + ): + countries_selected = countries + # Add this to check all the items in the selection tree + country_selector = ["0"] + else: + for index in country_selector: + countries_selected.update(selection_index[index]) + if countries_selected == countries: + # if all countries are all selected then stop + break + + countries_selected = list(countries_selected) + country_text = f"{len(countries_selected)} Selected" + # need to include the last selected year as it was exluded in the previous method + selected_years = years[years_slider[0] : years_slider[1] + 1] + # selected_years = years[slice(*years_slider)] + + # Use the dictionary to return the values of the selected countries based on the SDMX ISO3 codes + countries_selected_codes = [ + countries_iso3_dict[country] for country in countries_selected + ] + selections = dict( + theme=current_theme, + indicators_dict=indicators, + years=selected_years, + countries=countries_selected_codes, + is_adolescent=("ADOLESCENT" in indicators), + ) + + return ( + selections, + country_selector, + countries_selected == unicef_country_prog, + f"Years: {selected_years[0]} - {selected_years[-1]}", + "Countries: {}".format(country_text), + ) + + +def indicator_card( + selections, + card_id, + name, + numerator, + suffix, + denominator=None, + absolute=False, + average=False, + min_max=False, + sex_code=None, + age_group=None, +): + indicators = numerator.split(",") + + # TODO: Change to use albertos config + # lbassil: had to change this to cater for 2 dimensions set to the indicator card like age and sex + breakdown = "TOTAL" + # define the empty dimensions dict to be filled based on the card data filters + dimensions = {} + if age_group is not None: + dimensions["AGE"] = [age_group] + if sex_code is not None: + dimensions["SEX"] = [sex_code] + + filtered_data = get_filtered_dataset( + indicators, + selections["years"], + selections["countries"], + breakdown, + dimensions, + latest_data=True, + ) + + df_indicator_sources = df_sources[df_sources["Code"].isin(indicators)] + unique_indicator_sources = df_indicator_sources["Source_Full"].unique() + indicator_sources = ( + "; ".join(list(unique_indicator_sources)) + if len(unique_indicator_sources) > 0 + else "" + ) + source_link = ( + df_indicator_sources["Source_Link"].unique()[0] + if len(unique_indicator_sources) > 0 + else "" + ) + # lbassil: add this check because we are getting an exception where there is no data; i.e. no totals for all dimensions mostly age for the selected indicator + if filtered_data.empty: + indicator_header = "No data" + indicator_sources = "NA" + numerator_pairs = [] + return make_card( + card_id, + name, + indicator_header, + indicator_sources, + source_link, + numerator_pairs, + ) + + # select last value for each country + indicator_values = ( + filtered_data.groupby( + [ + "Country_name", + "TIME_PERIOD", + ] + ).agg({"OBS_VALUE": "sum", "CODE": "count"}) + ).reset_index() + + numerator_pairs = ( + indicator_values[indicator_values.CODE == len(indicators)] + .groupby("Country_name", as_index=False) + .last() + .set_index(["Country_name", "TIME_PERIOD"]) + ) + + if suffix.lower() == "countries": + # this is a hack to accomodate small cases (to discuss with James) + if "FREE" in numerator: + # trick to filter number of years of free education + indicator_sum = (numerator_pairs.OBS_VALUE >= 1).to_numpy().sum() + sources = numerator_pairs.index.tolist() + numerator_pairs = numerator_pairs[numerator_pairs.OBS_VALUE >= 1] + else: + # trick to accomodate cards for admin exams (AND for boolean indicators) + # filter exams according to number of indicators + indicator_sum = ( + (numerator_pairs.OBS_VALUE == len(indicators)).to_numpy().sum() + ) + sources = numerator_pairs.index.tolist() + + else: + indicator_sum = numerator_pairs["OBS_VALUE"].to_numpy().sum() + sources = numerator_pairs.index.tolist() + if average and len(sources) > 1: + indicator_sum = indicator_sum / len(sources) + + # define indicator header text: the resultant number except for the min-max range + if min_max and len(sources) > 1: + indicator_min = "{:,.1f}".format(numerator_pairs["OBS_VALUE"].min()) + indicator_max = "{:,.1f}".format(numerator_pairs["OBS_VALUE"].max()) + indicator_header = f"[{indicator_min} - {indicator_max}]" + else: + indicator_header = "{:,.0f}".format(indicator_sum) + + return make_card( + card_id, + name, + suffix, + indicator_sources, + source_link, + indicator_header, + numerator_pairs, + ) + + +@app.callback( + Output("cards_row", "children"), + [ + Input("store", "data"), + ], + [State("cards_row", "children"), State("indicators", "data")], +) +def show_cards(selections, current_cards, indicators_dict): + cards = [ + indicator_card( + selections, + f"card-{num}", + card["name"], + card["indicator"], + card["suffix"], + card.get("denominator"), + card.get("absolute"), + card.get("average"), + card.get("min_max"), + card.get("sex"), + card.get("age"), + ) + for num, card in enumerate(indicators_dict[selections["theme"]]["CARDS"]) + ] + return cards + + +@app.callback( + Output("subtitle", "children"), + Output("themes", "children"), + [ + Input("store", "data"), + Input("country_profile_selector", "value"), + ], + State("indicators", "data"), +) +def show_themes(selections, selected_country, indicators_dict): + # check if it is the country profile page + is_country_profile = selections["theme"] == "COUNTRYPROFILE" + ctx = dash.callback_context + ctrl_id = ctx.triggered[0]["prop_id"].split(".")[0] + # check if the countries dropdown is causing this callback in order to set the sub-title to the country's name + if is_country_profile or ctrl_id == "countries": + key_list = list(countries_iso3_dict.keys()) + val_list = list(countries_iso3_dict.values()) + subtitle = ( + key_list[val_list.index(selected_country)] + if selected_country + else "Country Name" + ) + return subtitle, [] + + subtitle = indicators_dict[selections["theme"]].get("NAME") + url_hash = "#{}".format((next(iter(selections.items())))[1].lower()) + # hide the buttons when only one option is available + if len(indicators_dict.items()) == 1: + return subtitle, [] + buttons = [ + dbc.Button( + value["NAME"], + id=key, + color=colours[num], + className="theme mx-1", + href=f"#{key.lower()}", + active=url_hash == f"#{key.lower()}", + ) + for num, (key, value) in enumerate(indicators_dict.items()) + ] + return subtitle, buttons + + +@app.callback( + Output({"type": "area_title", "index": MATCH}, "children"), + Output({"type": "area_options", "index": MATCH}, "options"), + Output({"type": "area_types", "index": MATCH}, "options"), + Output({"type": "area_options", "index": MATCH}, "value"), + Output({"type": "area_types", "index": MATCH}, "value"), + Input("store", "data"), + [ + State("indicators", "data"), + State({"type": "area_options", "index": MATCH}, "id"), + ], +) +def set_options(theme, indicators_dict, id): + + area = id["index"] + + area_options = area_types = [] + if area in indicators_dict[theme["theme"]]: + indicators = indicators_dict[theme["theme"]][area].get("indicators") + area_indicators = indicators.keys() if indicators is dict else indicators + area_options = [ + { + "label": indicator_names[code], + "value": code, + } + for code in area_indicators + ] + + area_types = [ + { + "label": name.capitalize(), + "value": name, + } + for name in indicators_dict[theme["theme"]][area].get("graphs", {}).keys() + ] + + name = ( + indicators_dict[theme["theme"]][area].get("name") + if area in indicators_dict[theme["theme"]] + else "" + ) + default_option = ( + indicators_dict[theme["theme"]][area].get("default") + if area in indicators_dict[theme["theme"]] + else "" + ) + default_graph = ( + indicators_dict[theme["theme"]][area].get("default_graph") + if area in indicators_dict[theme["theme"]] + else "" + ) + + return name, area_options, area_types, default_option, default_graph + + +@app.callback( + Output({"type": "area_breakdowns", "index": MATCH}, "options"), + [ + Input({"type": "area_options", "index": MATCH}, "value"), + Input({"type": "area_types", "index": MATCH}, "value"), + ], + [ + State({"type": "area_breakdowns", "index": MATCH}, "id"), + ], +) +def breakdown_options(indicator, fig_type, id): + + options = [{"label": "Total", "value": "TOTAL"}] + # lbassil: change the disaggregation to use the names of the dimensions instead of the codes + all_breakdowns = [ + {"label": "Sex", "value": "SEX"}, + {"label": "Age", "value": "AGE"}, + {"label": "Residence", "value": "RESIDENCE"}, + {"label": "Wealth Quintile", "value": "WEALTH_QUINTILE"}, + ] + dimensions = indicators_config.get(indicator, {}).keys() + # keep only TOTAL for line charts + if dimensions and fig_type != "line": + for breakdown in all_breakdowns: + if breakdown["value"] in dimensions: + options.append(breakdown) + return options + + +@app.callback( + Output({"type": "area_breakdowns", "index": MATCH}, "value"), + [ + Input("store", "data"), + Input({"type": "area_breakdowns", "index": MATCH}, "options"), + Input({"type": "area_types", "index": MATCH}, "value"), + ], + [ + State("indicators", "data"), + State({"type": "area_breakdowns", "index": MATCH}, "id"), + ], +) +def set_default_compare( + selections, compare_options, selected_type, indicators_dict, id +): + area = id["index"] + # lbassil: add this condition to stop the exception for the main area + if area in indicators_dict[selections["theme"]] and area != "MAIN": + default = indicators_dict[selections["theme"]][area]["default_graph"] + fig_type = selected_type if selected_type else default + config = indicators_dict[selections["theme"]][area]["graphs"][fig_type] + default_compare = config.get("compare") + + return ( + "TOTAL" + if fig_type == "line" or default_compare is None + else default_compare + if default_compare in compare_options + else compare_options[1]["value"] + if len(compare_options) > 1 + else compare_options[0]["value"] + ) + return "TOTAL" + + +@app.callback( + Output("main_area", "figure"), + Output("main_area_sources", "children"), + [ + Input({"type": "area_options", "index": "MAIN"}, "value"), + Input({"type": "historical_data_toggle", "index": "MAIN"}, "value"), + Input("store", "data"), + ], + [ + State("indicators", "data"), + ], +) +def main_figure(indicator, show_historical_data, selections, indicators_dict): + latest_data = not show_historical_data + options = indicators_dict[selections["theme"]]["MAIN"]["options"] + + data = get_filtered_dataset( + [indicator], + selections["years"], + selections["countries"], + latest_data=latest_data, + ) + + # check if the dataframe is empty meaning no data to display as per the user's selection + if data.empty: + return EMPTY_CHART, "" + + # lbassil: replace UNIT_MEASURE by Unit_name to use the name of the unit instead of the code + name = ( + data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()[0] + if len(data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()) > 0 + else "" + ) + df_indicator_sources = df_sources[df_sources["Code"] == indicator] + unique_indicator_sources = df_indicator_sources["Source_Full"].unique() + source = ( + "; ".join(list(unique_indicator_sources)) + if len(unique_indicator_sources) > 0 + else "" + ) + source_link = ( + df_indicator_sources["Source_Link"].unique()[0] + if len(unique_indicator_sources) > 0 + else "" + ) + + options["labels"] = DEFAULT_LABELS.copy() + options["labels"]["OBS_VALUE"] = name + options["labels"]["text"] = "OBS_VALUE" + options["geojson"] = geo_json_countries + if latest_data: + # remove the animation frame and show all countries at once + options.pop("animation_frame") + # add the year to show on hover + options["hover_name"] = "TIME_PERIOD" + + main_figure = px.choropleth_mapbox(data, **options) + main_figure.update_layout(margin={"r": 0, "t": 1, "l": 2, "b": 1}) + + # check if this area's config has an animation frame and hence a slider + if len(main_figure.layout["sliders"]) > 0: + # set last frame as the active one; i.e. select the max year as the default displayed year + main_figure.layout["sliders"][0]["active"] = len(main_figure.frames) - 1 + # assign the data of the last year to the map; without this line the data will show the first year; + main_figure = go.Figure( + data=main_figure["frames"][-1]["data"], + frames=main_figure["frames"], + layout=main_figure.layout, + ) + return main_figure, html.A(html.P(source), href=source_link, target="_blank") + + +@app.callback( + Output({"type": "area", "index": MATCH}, "figure"), + Output({"type": "area_sources", "index": MATCH}, "children"), + [ + Input("store", "data"), + Input({"type": "area_options", "index": MATCH}, "value"), + Input({"type": "area_breakdowns", "index": MATCH}, "value"), + Input({"type": "area_types", "index": MATCH}, "value"), + Input({"type": "exclude_outliers_toggle", "index": MATCH}, "value"), + ], + [ + State("indicators", "data"), + State({"type": "area_options", "index": MATCH}, "id"), + ], +) +def area_figure( + selections, + indicator, + compare, + selected_type, + exclude_outliers, + indicators_dict, + id, +): + # only run if indicator not empty + if not indicator: + return {}, {} + # check if it is the country profile page + is_country_profile = selections["theme"] == "COUNTRYPROFILE" + + area = id["index"] + indicators = indicators_dict[selections["theme"]][area]["indicators"] + default_graph = indicators_dict[selections["theme"]][area].get( + "default_graph", "line" + ) + fig_type = selected_type if selected_type else default_graph + fig_config = indicators_dict[selections["theme"]][area]["graphs"][fig_type] + options = fig_config.get("options") + traces = fig_config.get("trace_options") + dimension = False if fig_type == "line" or compare == "TOTAL" else compare + + indicator_name = str(indicator_names.get(indicator, "")) + # do we need `indicator_settings` below? + indicator_settings = ( + indicators.get(indicator, {}) if type(indicators) is dict else {} + ) + data = get_filtered_dataset( + [indicator], + selections["years"], + selections["countries"], + compare, + latest_data=False if fig_type == "line" or is_country_profile else True, + ).sort_values("OBS_VALUE", ascending=False) + # check if the dataframe is empty meaning no data to display as per the user's selection + if data.empty: + return EMPTY_CHART, "" + + # check if the exclude outliers checkbox is checked + if exclude_outliers: + # filter the data to the remove the outliers + # (df < df.quantile(0.1)).any() (df > df.quantile(0.9)).any() + data["z_scores"] = np.abs(zscore(data["OBS_VALUE"])) # calculate z-scores of df + # filter the data entries to remove the outliers + data = data[(data["z_scores"] < 3) | (data["z_scores"].isnull())] + + # lbassil: was UNIT_MEASURE + name = ( + data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()[0] + if len(data[data["CODE"] == indicator]["Unit_name"].astype(str).unique()) > 0 + else "" + ) + df_indicator_sources = df_sources[df_sources["Code"] == indicator] + unique_indicator_sources = df_indicator_sources["Source_Full"].unique() + source = ( + "; ".join(list(unique_indicator_sources)) + if len(unique_indicator_sources) > 0 + else "" + ) + source_link = ( + df_indicator_sources["Source_Link"].unique()[0] + if len(unique_indicator_sources) > 0 + else "" + ) + + options["labels"] = DEFAULT_LABELS.copy() + options["labels"]["OBS_VALUE"] = name + + # set the chart title, wrap the text when the indicator name is too long + chart_title = textwrap.wrap( + indicator_name, + width=74, + ) + chart_title = "
".join(chart_title) + + # set the layout to center the chart title and change its font size and color + layout = go.Layout( + title=chart_title, + title_x=0.5, + font=dict(family="Arial", size=12), + legend=dict(x=0.9, y=0.5), + ) + + # Add this code to avoid having decimal year on the x-axis for time series charts + if fig_type == "line" or is_country_profile: + data.sort_values(by=["TIME_PERIOD"], inplace=True) + layout["xaxis"] = dict( + tickmode="linear", + tick0=selections["years"][0], + dtick=1, + categoryorder="total ascending", + ) + + if dimension: + # lbassil: use the dimension name instead of the code + dimension_name = str(dimension_names.get(dimension, "")) + options["color"] = dimension_name + if compare == "WEALTH_QUINTILE": + wealth_dict = { + "Lowest": 0, + "Second": 1, + "Middle": 2, + "Fourth": 3, + "Highest": 4, + } + data.sort_values( + by=[dimension], key=lambda x: x.map(wealth_dict), inplace=True + ) + else: + # sort by the compare value to have the legend in the right ascending order + data.sort_values(by=[dimension], inplace=True) + + fig = getattr(px, fig_type)(data, **options) + fig.update_layout(layout) + if traces: + fig.update_traces(**traces) + + return fig, html.A(html.P(source), href=source_link, target="_blank") diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_education.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_education.py new file mode 100644 index 00000000..0d6a627b --- /dev/null +++ b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_education.py @@ -0,0 +1,672 @@ +from collections import defaultdict +import plotly.express as px + +from . import data, years +from .base_page import get_base_layout + +indicators_dict = { + "PARTICIPATION": { + "NAME": "Education access and participation", + "CARDS": [ + { + "name": "Who are Out-of-School", + "indicator": "EDUNF_OFST_L1,EDUNF_OFST_L2,EDUNF_OFST_L3", + "suffix": "Primary to upper secondary aged Children and Adolescents", + "age": "SCHOOL_AGE", + }, + { + "name": "Who are Out-of-School", + "indicator": "EDUNF_OFST_L1,EDUNF_OFST_L2,EDUNF_OFST_L3", + "suffix": "Primary to upper secondary aged Girls", + "sex": "F", + "age": "SCHOOL_AGE", + }, + { + "name": "Who are Out-of-School", + "indicator": "EDUNF_OFST_L1_UNDER1", + "suffix": "Children one year younger than the official primary entry age", + "age": "UNDER1_SCHOOL_ENTRY", + }, + ], + "MAIN": { + "name": "Out-of-School Children", + "geo": "Country_name", # REF_AREA + "options": dict( + locations="REF_AREA", + featureidkey="id", + color="OBS_VALUE", + color_continuous_scale=px.colors.sequential.GnBu, + mapbox_style="carto-positron", + zoom=2, + center={"lat": 62.995158, "lon": 88.048713}, + opacity=0.5, + labels={ + "OBS_VALUE": "Value", + "REF_AREA": "ISO3 Code", + "TIME_PERIOD": "Year", + "Country_name": "Country", + }, + hover_data={ + "OBS_VALUE": True, + "REF_AREA": False, + "Country_name": True, + "TIME_PERIOD": True, + }, + animation_frame="TIME_PERIOD", + height=750, + ), + "indicators": [ + "EDUNF_ROFST_L1", + "EDUNF_ROFST_L2", + "EDUNF_ROFST_L3", + "EDUNF_OFST_L1", + "EDUNF_OFST_L2", + "EDUNF_OFST_L3", + "EDU_SDG_PRYA", + "EDUNF_ROFST_L1_UNDER1", + "EDUNF_ROFST_L1T3", + ], + "default": "EDUNF_ROFST_L1", + }, + "AREA_1": { + "name": "Education entry and transition", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + # "compare": "Sex", + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "bar", + "indicators": [ + "EDUNF_ROFST_L1", + "EDUNF_ROFST_L2", + "EDUNF_ROFST_L3", + "EDUNF_ROFST_L1_UNDER1", + "EDUNF_ROFST_L1T3", + "EDUNF_STU_L1_TOT", + "EDUNF_STU_L2_TOT", + "EDUNF_STU_L3_TOT", + "EDUNF_GER_L2_GEN", + "EDUNF_GER_L2_VOC", + "EDUNF_GER_L3", + "EDUNF_GER_L3_GEN", + "EDUNF_GER_L3_VOC", + ], + "default": "EDUNF_ROFST_L1", + }, + "AREA_2": { + "name": "Education entry and transition", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + # "compare": "Sex", + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "line", + "indicators": [ + "EDUNF_NER_L02", + "EDUNF_NERA_L1_UNDER1", + "EDUNF_NERA_L1", + "EDUNF_NERA_L2", + "EDUNF_GER_L1", + "EDUNF_GER_L2", + "EDUNF_GER_L3", + "EDUNF_NIR_L1_ENTRYAGE", + "EDUNF_TRANRA_L2", + ], + "default": "EDUNF_TRANRA_L2", + }, + "AREA_3": { + "name": "Safe and inclusive learning environments", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + # "compare": "Sex", + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "bar", + "indicators": [ + "EDU_SDG_SCH_L1", + "EDU_SDG_SCH_L2", + "EDU_SDG_SCH_L3", + "WS_SCH_H-B", + "WS_SCH_S-B", + "WS_SCH_W-B", + "EDU_CHLD_DISAB", + "EDU_CHLD_DISAB_GENERAL", + "EDU_CHLD_DISAB_SPECIAL", + "EDU_CHLD_DISAB_GENERAL_BOARDING", + "EDU_CHLD_DISAB_SPECIAL_BOARDING", + "EDU_CHLD_DISAB_HOME", + "EDU_SDG_PRYA", + ], + "default": "EDU_CHLD_DISAB", + }, + "AREA_4": { + "name": "Education completion", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + # "compare": "Sex", + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "bar", + "indicators": [ + "EDUNF_CR_L1", + "EDUNF_CR_L2", + "EDUNF_CR_L3", + "EDUNF_DR_L1", + "EDUNF_DR_L2", + "EDUNF_GER_L2", + ], + "default": "EDUNF_CR_L1", + }, + }, + "QUALITY": { + "NAME": "Learning quality and skills", + "CARDS": [ + { + "name": "(enrolled in the same grade for a second or further year) in primary and lower secondary education", + "indicator": "EDUNF_RPTR_L1,EDUNF_RPTR_L2", + "suffix": "Children and adolescent repeaters", + }, + { + "name": "from primary education", + "indicator": "EDUNF_ESL_L1", + "suffix": "Early school leavers", + }, + { + "name": "administering nationally representative learning assessment in both reading and math at the end of primary education", + "indicator": "EDUNF_ADMIN_L1_GLAST_REA,EDUNF_ADMIN_L1_GLAST_MAT", + "suffix": "Countries", + }, + { + "name": "participating in the latest round of PISA", + "indicator": "EDU_PISA_MAT,EDU_PISA_REA,EDU_PISA_SCI", + "suffix": "Countries", + "absolute": True, + }, + ], + "MAIN": { + "name": "What students know and can do", + "geo": "Country_name", + "options": dict( + locations="REF_AREA", + featureidkey="id", + color="OBS_VALUE", + color_continuous_scale=px.colors.sequential.GnBu, + mapbox_style="carto-positron", + zoom=2, + center={"lat": 62.995158, "lon": 88.048713}, + opacity=0.5, + labels={ + "OBS_VALUE": "Value", + "Country_name": "Country", + "TIME_PERIOD": "Year", + "REF_AREA": "ISO3 Code", + }, + hover_data={ + "OBS_VALUE": True, + "REF_AREA": False, + "Country_name": True, + "TIME_PERIOD": True, + }, + animation_frame="TIME_PERIOD", + height=750, + ), + "indicators": ["EDU_PISA_MAT", "EDU_PISA_REA", "EDU_PISA_SCI"], + "default": "EDU_PISA_MAT", + }, + "AREA_1": { + "name": "Foundational skills", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + # "compare": "Sex", + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "bar", + "indicators": [ + "ECD_CHLD_36-59M_LMPSL", + "EDU_SDG_STU_L2_GLAST_MAT", + "EDU_SDG_STU_L2_GLAST_REA", + "EDU_SDG_STU_L1_GLAST_MAT", + "EDU_SDG_STU_L1_G2OR3_MAT", + "EDU_SDG_STU_L1_GLAST_REA", + "EDU_SDG_STU_L1_G2OR3_REA", + "EDUNF_LR_YOUTH", + "EDUNF_LR_ADULT", + ], + "default": "EDU_SDG_STU_L2_GLAST_MAT", + }, + "AREA_2": { + "name": "Foundational skills", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + # "compare": "Sex", + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "line", + "indicators": [ + "ECD_CHLD_36-59M_LMPSL", + "EDU_SDG_STU_L2_GLAST_MAT", + "EDU_SDG_STU_L2_GLAST_REA", + "EDU_SDG_STU_L1_GLAST_MAT", + "EDU_SDG_STU_L1_G2OR3_MAT", + "EDU_SDG_STU_L1_GLAST_REA", + "EDU_SDG_STU_L1_G2OR3_REA", + "EDUNF_LR_YOUTH", + "EDUNF_LR_ADULT", + ], + "default": "EDU_SDG_STU_L2_GLAST_MAT", + }, + "AREA_3": { + "name": "Trained and qualified teachers", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + # "compare": "Sex", + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "bar", + "indicators": [ + "EDU_SDG_TRTP_L02", + "EDU_SDG_TRTP_L1", + "EDU_SDG_TRTP_L2", + "EDU_SDG_TRTP_L3", + ], + "default": "EDU_SDG_TRTP_L2", + }, + "AREA_4": { + "name": "Trained and qualified teachers", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + # "compare": "Sex", + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "bar", + "indicators": [ + "EDU_SDG_QUTP_L02", + "EDU_SDG_QUTP_L1", + "EDU_SDG_QUTP_L2", + "EDU_SDG_QUTP_L3", + ], + "default": "EDU_SDG_QUTP_L2", + }, + }, + "SYSTEM": { + "NAME": "Education System", + "CARDS": [ + { + "name": "Guaranteeing at least one year of free pre-primary education in their legal frameworks", + "indicator": "EDU_SDG_FREE_EDU_L02", + "suffix": "Countries", + }, + { + "name": "Enrolled in private institutions (primary, lower secondary and upper secondary education)", + "indicator": "EDUNF_STU_L1_PRV,EDUNF_STU_L2_PRV,EDUNF_STU_L3_PRV", + "suffix": "Children and Adolescents", + }, + { + "name": "Total in primary, lower secondary and upper secondary education", + "indicator": "EDUNF_TEACH_L1,EDUNF_TEACH_L2,EDUNF_TEACH_L3", + "suffix": "Classroom Teachers", + }, + ], + "MAIN": { + "name": "Guaranteeing and paying for education", # "Education Expenditures and Legal Frameworks", + "geo": "Country_name", + "options": dict( + locations="REF_AREA", + featureidkey="id", + color="OBS_VALUE", + color_continuous_scale=px.colors.sequential.GnBu, + mapbox_style="carto-positron", + zoom=2, + center={"lat": 62.995158, "lon": 88.048713}, + opacity=0.5, + labels={ + "OBS_VALUE": "Value", + "Country_name": "Country", + "TIME_PERIOD": "Year", + "REF_AREA": "ISO3 Code", + }, + hover_data={ + "OBS_VALUE": True, + "REF_AREA": False, + "Country_name": True, + "TIME_PERIOD": True, + }, + animation_frame="TIME_PERIOD", + height=750, + ), + "indicators": [ + "EDU_FIN_EXP_PT_GDP", + "EDU_FIN_EXP_PT_TOT", + "EDU_SDG_FREE_EDU_L02", + "EDU_SDG_COMP_EDU_L02", + "EDU_FIN_EXP_CONST_PPP", + ], + "default": "EDU_FIN_EXP_PT_GDP", + }, + "AREA_1": { + "name": "Public and private enrolments", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + # "compare": "Sex", + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "bar", + "indicators": [ + "EDUNF_PRP_L02", + "EDUNF_PRP_L1", + "EDUNF_PRP_L2", + "EDUNF_PRP_L3", + "EDUNF_STU_L01_PUB", + "EDUNF_STU_L02_PUB", + "EDUNF_STU_L1_PUB", + "EDUNF_STU_L2_PUB", + "EDUNF_STU_L3_PUB", + "EDUNF_STU_L01_PRV", + "EDUNF_STU_L02_PRV", + "EDUNF_STU_L1_PRV", + "EDUNF_STU_L2_PRV", + "EDUNF_STU_L3_PRV", + ], + "default": "EDUNF_PRP_L1", + }, + "AREA_2": { + "name": "Public and private enrolments", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + # "compare": "Sex", + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "line", + "indicators": [ + "EDUNF_PRP_L02", + "EDUNF_PRP_L1", + "EDUNF_PRP_L2", + "EDUNF_PRP_L3", + "EDUNF_STU_L01_PUB", + "EDUNF_STU_L02_PUB", + "EDUNF_STU_L1_PUB", + "EDUNF_STU_L2_PUB", + "EDUNF_STU_L3_PUB", + "EDUNF_STU_L01_PRV", + "EDUNF_STU_L02_PRV", + "EDUNF_STU_L1_PRV", + "EDUNF_STU_L2_PRV", + "EDUNF_STU_L3_PRV", + ], + "default": "EDUNF_PRP_L2", + }, + "AREA_3": { + "name": "Government education expenditure", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + # "compare": "Sex", + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "bar", + "indicators": [ + "EDU_FIN_EXP_L02", + "EDU_FIN_EXP_L1", + "EDU_FIN_EXP_L2", + "EDU_FIN_EXP_L3", + "EDU_FIN_EXP_L4", + "EDU_FIN_EXP_L5T8", + "EDU_FIN_EXP_CONST_PPP", + ], + "default": "EDU_FIN_EXP_L2", + }, + "AREA_4": { + "name": "Administration of learning assessments", + "graphs": { + "bar": { + "options": dict( + x="Country_name", + y="OBS_VALUE", + barmode="group", + # text="TIME_PERIOD", + text="OBS_VALUE", + hover_name="TIME_PERIOD", + ), + }, + "line": { + "options": dict( + x="TIME_PERIOD", + y="OBS_VALUE", + color="Country_name", + hover_name="Country_name", + line_shape="spline", + render_mode="svg", + ), + "trace_options": dict(mode="lines+markers"), + }, + }, + "default_graph": "bar", + "indicators": { + "EDUNF_ADMIN_L1_GLAST_REA": {}, + "EDUNF_ADMIN_L1_GLAST_MAT": {}, + "EDUNF_ADMIN_L2_REA": {}, + "EDUNF_ADMIN_L2_MAT": {"dtype": "str"}, + "EDUNF_ADMIN_L1_G2OR3_REA": {}, + "EDUNF_ADMIN_L1_G2OR3_MAT": {}, + }, + "default": "EDUNF_ADMIN_L2_MAT", + }, + }, +} + + +main_title = "Education, Leisure, and Culture" + + +def get_layout(**kwargs): + kwargs["indicators"] = indicators_dict + kwargs["main_title"] = main_title + return get_base_layout(**kwargs) diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/child_health.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_health.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/child_health.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_health.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/child_participation.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_participation.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/child_participation.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_participation.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/child_poverty.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_poverty.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/child_poverty.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_poverty.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/child_protection.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_protection.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/child_protection.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_protection.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/child_rights.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_rights.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/child_rights.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/child_rights.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/climate.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/climate.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/climate.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/climate.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/country_profiles.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/country_profiles.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/country_profiles.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/country_profiles.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/data_query.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/data_query.py similarity index 99% rename from transmonee_dashboard/src/transmonee_dashboard/pages/data_query.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/data_query.py index a81056a2..8f0b0ed8 100644 --- a/transmonee_dashboard/src/transmonee_dashboard/pages/data_query.py +++ b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/data_query.py @@ -13,7 +13,7 @@ from pandas.io.formats import style import re -from transmonee_dashboard.pages.base_page import indicator_card +from transmonee_dashboard.pages__TMONEE.base_page import indicator_card from ..app import app from . import ( diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/disability.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/disability.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/disability.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/disability.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/ecd.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/ecd.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/ecd.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/ecd.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/gender.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/gender.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/gender.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/gender.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/home.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/home.py new file mode 100644 index 00000000..f5aff8f1 --- /dev/null +++ b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/home.py @@ -0,0 +1,60 @@ +import dash_core_components as dcc +import dash_html_components as html +import dash_bootstrap_components as dbc +from dash.dependencies import Input, State, Output + +from ..app import app + + +def get_layout(**kwargs): + return html.Div( + children=[ + html.Div( + className="heading", + style={"padding": 36}, + children=[ + html.Div( + className="heading-content", + children=[ + html.Div( + className="heading-panel", + style={"padding": 20}, + children=[ + html.H1( + "Home", + id="main_title", + className="heading-title", + ), + ], + ), + ], + ), + ], + ), + html.Br(), + html.Div( + children=[ + dbc.Card( + [ + # dbc.CardHeader(html.H3("State of Children Rights")), + dbc.CardBody( + [ + html.Img( + src="assets/home.png", + className="rounded mx-auto d-block", + ), + html.Br(), + # html.H4( + # "What you can find here...", + # className="card-title", + # ), + ] + ), + ] + ), + ], + style={"textAlign": "center"}, + ), + html.Br(), + ], + ) diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/overview.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/overview.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/overview.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/overview.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/resources.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/resources.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/resources.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/resources.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/pages/risks.py b/transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/risks.py similarity index 100% rename from transmonee_dashboard/src/transmonee_dashboard/pages/risks.py rename to transmonee_dashboard/src/transmonee_dashboard/pages__TMONEE/risks.py diff --git a/transmonee_dashboard/src/transmonee_dashboard/sdmx_utils/__init__.py b/transmonee_dashboard/src/transmonee_dashboard/sdmx_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/transmonee_dashboard/src/transmonee_dashboard/sdmx_utils/utils.py b/transmonee_dashboard/src/transmonee_dashboard/sdmx_utils/utils.py new file mode 100644 index 00000000..1eec73dd --- /dev/null +++ b/transmonee_dashboard/src/transmonee_dashboard/sdmx_utils/utils.py @@ -0,0 +1,49 @@ +import requests +import json + + +def get_codelist(url, lang=None): + codelist_get_params = {"format": "sdmx-json", "detail": "full", "references": "none"} + if lang is None: + r = requests.get(url, params=codelist_get_params) + else: + r = requests.get(url, params=codelist_get_params, headers={"Accept-Language": lang}) + + if r.ok: + ret = [] + # jsondata = r.content.decode("utf-8") + # jsondata = json.loads(jsondata) + jsondata = r.json() + for c in jsondata['data']['codelists'][0]['codes']: + ret.append({"id": c["id"], "name": c["name"]}) + if "description" in c: + ret[-1]["description"] = c["description"] + if "parent" in c: + ret[-1]["parent"] = c["parent"] + return ret + else: + raise (ConnectionError( + "Error downloading " + url + " status code: " + str(r.status_code) + r.raise_for_status())) + + +''' +def _json_codelist_to_dataframe(codelist_sdmx_json, lang = None, force_any_language_when_no_lang = True): + ret = pd.DataFrame(columns=["id", "name", "desc", "parent"], dtype=str) + + for i in codelist_sdmx_json['data']['codelists'][0]['codes']: + #toAdd = {"id": i["id"], "name": i["names"].items()[0]} + toAdd = {"id": i["id"]} + if lang is not None and lang in i["names"]: + toAdd["name"] = i["names"][lang] + elif force_any_language_when_no_lang: #fallback + toAdd["name"] = list(i["names"].values())[0] + else: toAdd["name"] = None + + if "description" in i: + toAdd["desc"] = i["description"] + if "parent" in i: + toAdd["parent"] = i["parent"] + ret = ret.append(toAdd, ignore_index=True) + return ret + + '''