Building a Vietnam Development Indicators Explorer

I attempt to build an explorer/dashboard to explore different Development Indicators of Vietnam using World Bank Data API
Visualization
Python
Author

Nguyen Truong Thinh

Published

April 23, 2024

Modified

June 20, 2024

Motivation

I want to create a Vietnam Development Indicators Explorer using Shiny. I came across the World Bank’s World Development Indicators which contains a lot of data on Vietnam. I think creating an interactive explorer can help popularize and make the data more consumable.

JTBDs

I came across the World Development Indicators data set from the World Bank while doing some research for assignments. I noticed that they also have World Bank APIs that allows public access to all of the data. This Vietnam Development Indicators are built with Python.

#| standalone: true
#| viewerHeight: 1000
#| components: [viewer]
#| layout: vertical

## file: app.py
from shiny import reactive
from shiny.express import input, render, ui
from shinywidgets import render_plotly, render_widget, render_altair
from shinyswatch import theme
import requests
import pandas as pd
import geopandas as gpd
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import pyodide.http
from pathlib import Path
from os.path import dirname

import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# Get data from World Bank API
def getAllResponse(url):
  try:
    responseData = requests.get(url=url).json()
    pages = responseData[0]["pages"]
    fullData = []
    for p in range(1, pages + 1):
      u = url + f"&page={p}"
      try:
        pdata = requests.get(url=u).json()
        fullData += pdata[1]
      except:
        pass
    df = pd.DataFrame(fullData)
    return df
  except:
    pd.DataFrame()


indicator = getAllResponse(
    "https://api.worldbank.org/v2/indicator?format=json&source=2"
)

# Data for the SEA countries
sea_countries = [
    "BRN",  # Brunei
    "MMR",  # Myanmar
    "KHM",  # Cambodia
    "TLS",  # Timor-Leste
    "IDN",  # Indonesia
    "LAO",  # Laos
    "MYS",  # Malaysia
    "PHL",  # Philippines
    "SGP",  # Singapore
    "THA",  # Thailand
    "VNM",  # Vietnam
]


# Actual app
@reactive.calc
def vUrl():
    return f"https://api.worldbank.org/v2/country/vnm/indicator/{input.indicator()}?format=json"


@reactive.calc
def wUrl():
    return f"https://api.worldbank.org/v2/country/all/indicator/{input.indicator()}?format=json&source=2&mrnev=1"


@reactive.calc
def iUrl():
    return f"https://api.worldbank.org/v2/indicator/{input.indicator()}?format=json&source=2"


@reactive.calc
async def getData():
    df = getAllResponse(vUrl())

    # Normalize the columns
    df["i_id"] = df["indicator"].apply(lambda x: x["id"])
    df["indicator"] = df["indicator"].apply(lambda x: x["value"])
    df["c_id"] = df["country"].apply(lambda x: x["id"])
    df["country"] = df["country"].apply(lambda x: x["value"])
    return df


@reactive.calc
async def getWorldData():
    df = getAllResponse(wUrl())
    return df


@reactive.calc
async def getIndicatorData():
    df = getAllResponse(iUrl())
    return df


theme.zephyr()

ui.page_opts(title="Devlopment Indicators Explorer", fillable=True, id="page")
ui.tags.style(
    """
    .navbar-brand, .navbar-brand:hover { color: #ffffff }
    .navbar {background: #3459e6 !important;}
    .card-header { color: white; background: #3459e6 !important; }
    .plotly .modebar { display: none; }
    """
)

with ui.sidebar(width="25%"):
    ui.markdown(
        """
      ##### About
  
      This is an interactive dashboard to explore different Development Indicators of Vietnam and other countries.
      """
    )
    ui.hr()

    choices = dict(zip(indicator["id"], indicator["name"]))
    ui.input_selectize(
        "indicator", "Choose an indicator", choices=choices, selected="NY.GNP.MKTP.CD"
    )

    ui.input_action_button(
        "random",
        "Surprise me!",
        class_="btn btn-primary",
        icon="✨",
    )

    @reactive.effect
    @reactive.event(input.random)
    def random_indicator():
        import random

        ui.update_selectize(
            "indicator", choices=choices, selected=random.choice(list(choices.keys()))
        )


with ui.navset_tab(id="tab"):
    with ui.nav_panel("Explorer"):
        with ui.layout_columns(col_widths=(4, 8, 12), fill=True):
            with ui.card(height=400):

                @render.ui
                async def ind():
                    df = await getIndicatorData()

                    name = df["name"].str.cat(sep=" ")
                    code = str(input.indicator())
                    notes = df["sourceNote"].str.cat(sep=" ")
                    topics = (
                        df["topics"]
                        .apply(lambda x: ", ".join([topic["value"] for topic in x]))
                        .str.cat(sep=" ")
                    )

                    text = f"""
                      #### {name}
        
                      {notes}
                      
                      **Code:** {code}
        
                      **Topics:** {topics}
                    """
                    return ui.markdown(text)
            
            with ui.card():
                ui.card_header("Vietnam")

                @render_plotly
                async def plot():
                    df = await getData()

                    # Create the plot
                    data = go.Bar(
                        x=df["date"], y=df["value"], marker=dict(color="#3459e6")
                    )
                    layout = go.Layout(
                        xaxis=dict(
                            title="Year", rangeslider=dict(visible=True), type="linear"
                        ),
                        yaxis=dict(title=None),
                        margin=dict(pad=10),
                        hovermode="x unified",
                        template="plotly_white",
                    )
                    fig = go.FigureWidget(data=[data], layout=layout)
                    return fig
            
            with ui.card(height=600):
                ui.card_header("World - Most recent value")

                @render_plotly
                async def map():
                    df = await getWorldData()
                    df["i_id"] = df["indicator"].apply(lambda x: x["id"])
                    df["indicator"] = df["indicator"].apply(lambda x: x["value"])
                    df["c_id"] = df["country"].apply(lambda x: x["id"])
                    df["country"] = df["country"].apply(lambda x: x["value"])
                    df.dropna(subset=['countryiso3code'], inplace=True)

                    geojson_url = 'https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson'
                    response = requests.get(geojson_url)
                    geo_json = response.json()
                    gdf = gpd.GeoDataFrame.from_features(geo_json['features'])
                    
                    data = pd.merge(df, gdf, how="inner", left_on='countryiso3code', right_on='ISO_A3')
                    
                    fig = make_subplots(rows=2, cols=2, column_widths=[0.6, 0.4],
                                        specs=[
                                          [{"type": "choropleth", "rowspan": 2}, {"type": "bar"}],
                                          [            None                    , {"type": "bar"}]
                                          ])
                    
                    # Map of the world
                    fig.add_trace(go.Choropleth(locations=data['countryiso3code'],
                                                z=data['value'],
                                                customdata=data['date'],
                                                text=data['country'],
                                                hovertemplate="<b>%{text}</b><br>Year: %{customdata}<br>Value: %{z:$,.2f}<extra></extra>",
                                                colorbar=dict(borderwidth=0,
                                                      orientation="v",
                                                      thickness=20,
                                                      xref='paper',
                                                      x=-0.05),
                                                colorscale="Plotly3",
                                                reversescale=True,
                                                marker_opacity=0.5,
                                                marker_line_width=0), 
                                  row=1, col=1)
                    
                    data_sorted = data.sort_values(by=['value'], ignore_index=True)
                    
                    # Top 5 countries
                    fig.add_trace(go.Bar(y=data_sorted['country'].tail(5), 
                                         x=data_sorted['value'].tail(5),
                                         customdata=data_sorted['date'],
                                         marker=dict(color="#3459e6"),
                                         hovertemplate="Year: %{customdata}<br>Value: %{x}<extra></extra>",
                                         orientation='h',
                                         showlegend=False),
                                  row=1, col=2)
                    
                    # Bottom 5 countries
                    fig.add_trace(go.Bar(y=data_sorted['country'].head(5).sort_index(ascending=False),
                                         x=data_sorted['value'].head(5).sort_index(ascending=False),
                                         customdata=data_sorted['date'],
                                         marker=dict(color="#3459e6"),
                                         hovertemplate="Year: %{customdata}<br>Value: %{x}<extra></extra>",
                                         orientation='h',
                                         showlegend=False),
                                  row=2, col=2)
                    
                    fig.update_traces(hoverlabel=dict(bgcolor='black'))
                    
                    fig.update_geos(resolution=50,
                                    projection_type='orthographic',
                                    showcountries=True,
                                    projection_rotation=dict(
                                      lat=14,
                                      lon=108,
                                      roll=0),
                                    )
                    
                    fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0},
                                      clickmode='event+select',
                                      dragmode='pan',
                                      template='plotly_white',
                                      )
                    
                    return fig

    with ui.nav_panel("Data"):
        with ui.card():

            @render.table
            async def printDf():
                df = await getData()
                return df

        
## file: requirements.txt
plotly
requests
geopandas
shinywidgets
shinyswatch
urllib3

## file: utils.py