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