library(here)
library(sf)
library(leaflet)
library(tidyverse)
library(readxl)
library(shiny)
library(dplyr)
library(plotly)
library(DT)
library(tidyr)
library(ggiraph)
library(ggplot2)
library(patchwork)
China in the Development World
Irene Chen, Crystal Yixin Luo, Vaishnavi Singh, Gabriel Soto, Tian Tong
Introduction
This project analyzes China’s development finance investments through multiple analytical lenses, including sectoral distributions, geographical patterns, and temporal trends. While the core dataset tracks financial investments and funding flows, our analysis extends beyond purely monetary aspects to understand the broader patterns and characteristics of these development projects.
1. Data Importing
1.1 Load and filter the original dataset
#getting specific columns
<- df %>% select(
df_filtered "AidData Record ID", "Financier Country", "Recipient",
"Recipient Region", "Commitment Year", "Completion Year",
"Title", "Description", "Status", "Intent",
"Flow Type Simplified", "Flow Class", "Sector Name",
"Infrastructure", "Funding Agencies Type",
"Implementing Agencies Type",
"Adjusted Amount (Constant USD 2021)",
"Location Narrative", "OECD ODA Income Group",
"Geographic Level of Precision Available",
"Geospatial Feature Available",
"Interest Rate", # Added
"Grace Period"
)
#filtering to get 961 projects with geospatial features
<- df_filtered %>%
df_filtered filter(`Flow Class` != "Vague (Official Finance)",
`Adjusted Amount (Constant USD 2021)` > 100000000.00,
`Geographic Level of Precision Available` == "Precise",
`Geospatial Feature Available` == "Yes")
1.2 Load and process the GeoJSON files
loads individual GeoJSON files into a list
# Absolute path to directory
<- "./data-spatial"
geojson_dir
# Get file paths
<- list.files(path = geojson_dir, pattern = "\\.(geojson|GeoJSON|json)$", full.names = TRUE)
geojson_files
<- list.files(path = geojson_dir, pattern = "\\.geojson$", full.names = TRUE)
geojson_files
# Read files into list
<- lapply(geojson_files, st_read, quiet = TRUE)
geojson_list names(geojson_list) <- basename(geojson_files)
matches GeoJSON features with AidData records and adds properties
# Iterate and add properties
<- lapply(geojson_list, function(geojson) {
geojson_list # Get the GeoJSON's unique identifier (assumes it's in a column `id`)
<- geojson$id[1] # Replace with the actual name of your `id` field
geo_id
# Match the row in `df`
<- df_filtered %>% filter(`AidData Record ID` == geo_id)
matched_row
# Add the properties if a match is found
if (nrow(matched_row) > 0) {
$Title <- matched_row$Title
geojson$Amount <- matched_row$Amount
geojson$Status <- matched_row$Status
geojson$`Recipient Region` <- matched_row$`Recipient Region`
geojson$Description <- matched_row$Description
geojson$Intent <- matched_row$Intent
geojson$`Flow Type Simplified` <- matched_row$`Flow Type Simplified`
geojson$`Flow Class` <- matched_row$`Flow Class`
geojson$`Funding Agencies Type` <- matched_row$`Funding Agencies Type`
geojson$`Implementing Agencies Type` <- matched_row$`Implementing Agencies Type`
geojson$`Location Narrative` <- matched_row$`Location Narrative`
geojson$`OECD ODA Income Group` <- matched_row$`OECD ODA Income Group`
geojson
else {
} # If no match, assign default or NA
$Title <- NA
geojson$Amount <- NA
geojson$Status <- NA
geojson$`Recipient Region` <- NA
geojson$Description <- NA
geojson$Intent <- NA
geojson$`Flow Type Simplified` <- NA
geojson$`Flow Class` <- NA
geojson$`Funding Agencies Type` <- NA
geojson$`Implementing Agencies Type` <- NA
geojson$`Location Narrative` <- NA
geojson$`OECD ODA Income Group` <- NA
geojson
}
return(geojson)
})
Combining and Fixing Geometries:
<- do.call(rbind, geojson_list) %>%
combined_geojson st_make_valid() %>%
# Add coordinate check
{<- st_is_valid(., reason = TRUE)
invalid_geoms print(paste("Invalid geometries found:", sum(invalid_geoms != "Valid Geometry")))
.%>%
} # Remove any remaining invalid geometries
filter(st_is_valid(.)) %>%
# Ensure proper CRS
st_transform(4326)
[1] "Invalid geometries found: 0"
# Print summary for verification
print(paste("Total features:", nrow(combined_geojson)))
[1] "Total features: 961"
print(paste("Unique regions:", length(unique(combined_geojson$`Recipient Region`))))
[1] "Unique regions: 6"
2. Creating Interactive Plots
2.1 Interactive line Plots
# Calculate yearly averages
<- df_filtered %>%
yearly_metrics group_by(`Commitment Year`) %>%
summarise(
avg_interest_rate = mean(`Interest Rate`, na.rm = TRUE),
avg_grace_period = mean(`Grace Period`, na.rm = TRUE),
project_count = n()
%>%
) filter(!is.na(`Commitment Year`)) # Remove any NA years
# Create a data frame with Chinese domestic interest rates for the relevant years
<- data.frame(
chinese_rates year = c(2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021),
rate = c(3.24, 3.24, 2.70, 2.70, 3.33, 3.33, 3.33, 3.33, 3.9375, 2.79, 2.8283,
3.25, 3.25, 3.25, 3.25, 2.90, 2.90, 2.90, 2.90, 2.90, 2.90, 2.90)
)
# Create the plot with modified x-axis settings
<- plot_ly(yearly_metrics, x = ~`Commitment Year`) %>%
p # Add the main interest rate trace
add_trace(
y = ~avg_interest_rate,
name = 'Interest Rate',
type = 'scatter',
mode = 'lines+markers',
line = list(color = '#8884d8'),
marker = list(color = '#8884d8', size = 8),
hovertemplate = paste(
"Year: %{x}<br>",
"Interest Rate: %{y:.2f}%<br>",
"<extra></extra>"
)%>%
) # Add the grace period trace
add_trace(
y = ~avg_grace_period,
name = 'Grace Period',
type = 'scatter',
mode = 'lines+markers',
line = list(color = '#82ca9d'),
marker = list(color = '#82ca9d', size = 8),
hovertemplate = paste(
"Year: %{x}<br>",
"Grace Period: %{y:.2f} years<br>",
"<extra></extra>"
)%>%
) # Add Chinese domestic interest rate line
add_trace(
data = chinese_rates,
x = ~year,
y = ~rate,
name = 'China Domestic Rate',
type = 'scatter',
mode = 'lines',
line = list(color = '#FF6B6B', dash = 'dot'),
hovertemplate = paste(
"Year: %{x}<br>",
"Domestic Rate: %{y:.2f}%<br>",
"<extra></extra>"
)%>%
) # Configure the layout with modified x-axis settings
layout(
title = list(
text = 'Average Interest Rate and Grace Period by Commitment Year',
x = 0.5,
y = 0.95,
font = list(size = 20)
),xaxis = list(
title = 'Commitment Year',
gridcolor = '#E2E2E2',
showgrid = TRUE,
# Set tick marks every 2 years from 2000 to 2021
tickmode = "array",
tickvals = seq(2000, 2021, by = 2),
ticktext = as.character(seq(2000, 2021, by = 2))
),yaxis = list(
title = 'Years / Percentage',
gridcolor = '#E2E2E2',
showgrid = TRUE,
range = c(0, 7)
),legend = list(
title = list(text = 'Metric'),
orientation = 'h',
y = 1.1,
x = 0.5,
xanchor = 'center',
yanchor = 'bottom'
),margin = list(t = 100),
hoverlabel = list(bgcolor = "white"),
plot_bgcolor = '#FFFFFF',
paper_bgcolor = '#FFFFFF'
)
# Display the plot
p
Interest Rate Trends:
Started high around 6.3% in 2000 and showed a general declining trend
Shows some volatility but less extreme than grace period changes
Typically remained higher than China’s domestic rate until around 2008-2009
Grace Period Patterns:
Much more volatile than interest rates, especially in early years
Shows a slight declining trend in recent years (2015-2020)
2.2 Interactive Box Plot
# Calculate yearly averages
<- df_filtered %>%
df_clean filter(!is.na('Interest Rate'),
!is.na('Commitment Year'))
<- range(df_clean$`Commitment Year`, na.rm = TRUE)
year_range <- seq(year_range[1], year_range[2])
all_years
<- df_clean %>%
yearly_averages group_by(`Commitment Year`) %>%
summarise(avg_interest_rate = mean(`Interest Rate`, na.rm = TRUE))
# Create the visualization with both boxplot and average line
<- plot_ly() %>%
p # First add the original boxplot
add_boxplot(
data = df_clean,
x = ~`Commitment Year`,
y = ~`Interest Rate`,
name = 'Interest Rate Distribution',
boxpoints = 'all',
jitter = 0,
pointpos = 0,
marker = list(
color = '#8884d8',
opacity = 0.6,
size = 7
),line = list(color = '#8884d8'),
fillcolor = 'rgba(136, 132, 216, 0.2)'
%>%
) # Add the average line
add_trace(
data = yearly_averages,
x = ~`Commitment Year`,
y = ~avg_interest_rate,
type = 'scatter',
mode = 'lines',
name = 'Yearly Average',
line = list(
color = 'red',
width = 2
)%>%
) layout(
title = list(
text = 'Distribution of Project Interest Rates by Commitment Year',
x = 0.5,
y = 0.95,
font = list(size = 20)
),xaxis = list(
title = 'Commitment Year',
gridcolor = '#E2E2E2',
showgrid = TRUE,
tickmode = "array",
tickvals = all_years,
ticktext = as.character(all_years),
tickangle = 45,
tickfont = list(size = 10)
),yaxis = list(
title = 'Interest Rate (%)',
gridcolor = '#E2E2E2',
showgrid = TRUE,
zeroline = TRUE,
zerolinecolor = '#969696',
zerolinewidth = 1
),margin = list(
t = 100,
b = 120,
l = 80,
r = 50
),showlegend = TRUE, # Changed to TRUE to show the line in legend
plot_bgcolor = '#FFFFFF',
paper_bgcolor = '#FFFFFF'
)
# Display the plot
p
Structural Break in 2000-2001:
Sharp decline from 6% to 4%, marking a clear policy shift. After this drop, rates stabilized and never returned to pre-2001 levels
Long-term Stability (2001-2021):
Average interest rates consistently maintained between 2-4%. Shows China’s sustained commitment to relatively low-cost financing
Consistent Rate Structure:
Most project rates cluster around 2-4%, suggesting a standardized lending approach with occasional special cases.
2.3 Interactive Linked Scatter plot with Python
Please check the plot at: https://pp5202-final-project-python.netlify.app/
3. Creating plots with Shiny App
3.1 Creating Comprehensive Overview Dashboard
Using the original polygons instead of circled markers
Adding a tab for Analysis with: (a) a Barplot to show Sector-wise distribution; (b) a Pie Chart to show project status distribution
Adding a stacked bar plot to show Regional Variation across sectors
Adding a tab for Data table (allowing filter selection)
https://yc1171.shinyapps.io/shiny-2/
Combined dashboards:
# UI
<- fluidPage(
ui titlePanel("China Development Finance Projects"),
sidebarLayout(
sidebarPanel(width = 3,
selectInput("region_filter",
"Filter by Region:",
choices = c("All", sort(unique(combined_geojson$`Recipient Region`)))),
selectInput("sector_filter",
"Filter by Sector:",
choices = c("All", sort(unique(combined_geojson$Sector.Name)))),
checkboxGroupInput("status_filter",
"Project Status:",
choices = sort(unique(combined_geojson$Status))),
sliderInput("amount_filter",
"Investment Amount (USD Millions):",
min = 0,
max = 10000, # Natural limit for better display
value = c(0, 10000), # Default slider range
step = 1),
actionButton("reset", "Reset Filters", class = "btn-primary")
),
mainPanel(width = 9,
fluidRow(
column(4,
div(class = "well well-sm",
h4("Total Projects"),
textOutput("total_projects"))),
column(4,
div(class = "well well-sm",
h4("Total Investment (USD Billions)"),
textOutput("total_investment"))),
column(4,
div(class = "well well-sm",
h4("Average Project Size (USD Millions)"),
textOutput("avg_project")))
),
tabsetPanel(
tabPanel("Map View", leafletOutput("map", height = "600px")),
tabPanel("Analysis",
fluidRow(
column(6, plotlyOutput("sector_plot")),
column(6, plotlyOutput("status_plot"))
),plotlyOutput("regional_plot")),
tabPanel("Data Table", DTOutput("project_table"))
)
)
)
)
# Server
<- function(input, output, session) {
server
# Reset button functionality
observeEvent(input$reset, {
updateSelectInput(session, "region_filter", selected = "All")
updateSelectInput(session, "sector_filter", selected = "All")
updateCheckboxGroupInput(session, "status_filter", selected = character(0))
updateSliderInput(session, "amount_filter",
min = 0,
max = 10000, # Reset to default limits
value = c(0, 10000))
})
# Reactive data based on filters
<- reactive({
filtered_data <- combined_geojson
data
if (input$region_filter != "All") {
<- data %>% filter(`Recipient Region` == input$region_filter)
data
}if (input$sector_filter != "All") {
<- data %>% filter(Sector.Name == input$sector_filter)
data
}if (length(input$status_filter) > 0) {
<- data %>% filter(Status %in% input$status_filter)
data
}<- data %>%
data filter(Amount..Constant.USD.2021./1e6 >= input$amount_filter[1],
2021./1e6 <= input$amount_filter[2])
Amount..Constant.USD.
validate(
need(nrow(data) > 0, "No projects match the selected filters")
)
data
})
# Map
$map <- renderLeaflet({
outputreq(filtered_data())
<- colorFactor(palette = "viridis", domain = unique(filtered_data()$Sector.Name))
pal
leaflet(filtered_data()) %>%
addProviderTiles("CartoDB.Positron") %>%
addPolygons(
fillColor = ~pal(Sector.Name),
color = ~pal(Sector.Name),
weight = 2,
opacity = 1,
fillOpacity = 0.7,
highlightOptions = highlightOptions(
weight = 3,
color = "white",
fillOpacity = 1,
bringToFront = TRUE
),popup = ~paste0(
"<strong>Title:</strong> ", Title, "<br>",
"<strong>Amount:</strong> $", formatC(Amount..Constant.USD.2021./1e6, format = "f", digits = 1, big.mark = ","), "M<br>",
"<strong>Status:</strong> ", Status, "<br>",
"<strong>Sector:</strong> ", Sector.Name
),label = ~Title
%>%
) addLegend(
position = "bottomright",
pal = pal,
values = ~Sector.Name,
title = "Sectors",
opacity = 1
%>%
) # Add custom control with zoom instructions
addControl(
html = '<div style="padding: 6px 8px; font-size: 14px; background: white;
background: rgba(255,255,255,0.8); box-shadow: 0 0 15px rgba(0,0,0,0.2);
border-radius: 5px; line-height: 1.5">
<strong>Map Instructions:</strong><br>
• Zoom in significantly to see detailed project shapes<br>
• Projects show actual footprints (e.g., factory layouts, infrastructure corridors)<br>
• Use mouse wheel or +/- buttons to zoom<br>
• Click on shapes for project details
</div>',
position = "topleft"
%>%
) # Set a slightly zoomed-in initial view
setView(lng = 0, lat = 20, zoom = 3)
})
# Sector plot
$sector_plot <- renderPlotly({
output<- filtered_data() %>%
sector_data st_drop_geometry() %>%
group_by(Sector.Name) %>%
summarise(Total = sum(Amount..Constant.USD.2021./1e6, na.rm = TRUE)) %>%
arrange(desc(Total))
plot_ly(
data = sector_data,
x = ~Total,
y = ~reorder(Sector.Name, Total),
type = 'bar',
orientation = 'h',
marker = list(color = "blue")
%>%
) layout(
title = "Top Sectors by Investment",
xaxis = list(title = "Investment (USD Millions)"),
yaxis = list(title = ""),
showlegend = FALSE
)
})
# Status plot
$status_plot <- renderPlotly({
output<- filtered_data() %>%
status_data st_drop_geometry() %>%
group_by(Status) %>%
summarise(Count = n()) %>%
mutate(Percentage = Count/sum(Count) * 100)
plot_ly(
data = status_data,
labels = ~Status,
values = ~Count,
type = "pie",
textinfo = "label+percent",
marker = list(colors = c("#2ecc71", "#3498db", "#e74c3c"))
%>%
) layout(
title = "Project Status Distribution",
showlegend = TRUE
)
})
# Regional Distribution Plot
$regional_plot <- renderPlotly({
output<- filtered_data() %>%
regional_data st_drop_geometry() %>%
group_by(`Recipient Region`, Sector.Name) %>%
summarise(TotalInvestment = sum(Amount..Constant.USD.2021., na.rm = TRUE)) %>%
arrange(desc(TotalInvestment))
plot_ly(
data = regional_data,
x = ~`Recipient Region`,
y = ~TotalInvestment / 1e6,
type = 'bar',
color = ~Sector.Name,
colors = viridis::viridis_pal(option = "D")(length(unique(regional_data$Sector.Name))),
text = ~paste(
"Region:", `Recipient Region`, "<br>",
"Sector:", Sector.Name, "<br>",
"Investment:", scales::comma(TotalInvestment / 1e6, suffix = "M")
),hoverinfo = "text"
%>%
) layout(
title = "Regional Distribution of Investments by Sector",
xaxis = list(title = "Region"),
yaxis = list(title = "Investment (USD Millions)"),
barmode = "stack",
font = list(size = 12) # Improved font size for clarity
)
})
# Summary statistics
$total_projects <- renderText({
outputnrow(filtered_data())
})
$total_investment <- renderText({
output<- sum(filtered_data()$Amount..Constant.USD.2021., na.rm = TRUE) / 1e9
total paste0("$", formatC(total, format = "f", digits = 1, big.mark = ","), "B")
})
$avg_project <- renderText({
output<- mean(filtered_data()$Amount..Constant.USD.2021., na.rm = TRUE) / 1e6
avg paste0("$", formatC(avg, format = "f", digits = 1, big.mark = ","), "M")
})
# Data table with improved formatting
$project_table <- renderDT({
output<- filtered_data() %>% st_drop_geometry()
data
datatable(
data,options = list(
pageLength = 10,
scrollX = TRUE,
autoWidth = FALSE,
columnDefs = list(
# For ID, feature count, and ISO codes - very short
list(
targets = c("id", "feature_count", "Recipient.ISO.3", "Status",
"Infrastructure", "Flow Class", "Flow Type Simplified"),
width = "80px",
render = JS("
function(data, type, row) {
if (type === 'display' && data != null) {
return data.toString();
}
return data;
}")
),# For dates - no truncation
list(
targets = c("Commitment.Date..MM.DD.YYYY.",
"Actual.Implementation.Start.Date..MM.DD.YYYY.",
"Actual.Completion.Date..MM.DD.YYYY.",
"Commitment.Year", "Implementation.Start.Year",
"Completion.Year"),
width = "120px",
render = JS("
function(data, type, row) {
if (type === 'display' && data != null) {
return data.toString();
}
return data;
}")
),# For text-heavy columns
list(
targets = c("Title", "Description", "Location Narrative"),
width = "300px",
render = JS("
function(data, type, row) {
if (type === 'display' && data != null && data.length > 100) {
return data.toString().substr(0, 100) + '...';
}
return data;
}")
),# Default for remaining columns
list(
targets = "_all",
width = "150px",
render = JS("
function(data, type, row) {
if (type === 'display' && data != null && data.length > 50) {
return data.toString().substr(0, 50) + '...';
}
return data;
}")
)
)
),rownames = FALSE,
class = 'stripe', # Removed 'compact' class
selection = 'none'
%>%
) formatStyle(
columns = colnames(data),
fontSize = '13px',
lineHeight = '1.4',
padding = '8px',
height = '80px', # Increased row height
backgroundColor = 'white',
whiteSpace = 'normal' # Allow text wrapping
%>%
) formatCurrency(
columns = "Amount..Constant.USD.2021.",
currency = "$",
digits = 0
)
})
}
# Run the Shiny app
shinyApp(ui = ui, server = server)
3.2 Creating OECD ODA Income Group Dashboard
https://yl1652.shinyapps.io/china-development-new/
library(shiny)
library(dplyr)
library(leaflet)
library(plotly)
library(sf)
library(viridis)
library(tidyr)
# UI
<- fluidPage(
ui titlePanel("OECD ODA Income Group Dashboard"),
sidebarLayout(
sidebarPanel(width = 3,
div(class = "well well-sm",
h4("Total Projects"),
textOutput("total_projects")),
div(class = "well well-sm",
h4("Total Investment (USD Billions)"),
textOutput("total_investment")),
selectInput("income_group_filter",
"Filter by Income Group:",
choices = c("All", unique(na.omit(combined_geojson$`OECD ODA Income Group`)))),
sliderInput("amount_filter",
"Investment Amount (USD Millions):",
min = 0,
max = ceiling(max(combined_geojson$Amount..Constant.USD.2021./1e6, na.rm = TRUE)/4000)*4000,
step = 4000,
value = c(0, ceiling(max(combined_geojson$Amount..Constant.USD.2021./1e6, na.rm = TRUE)/4000)*4000)),
selectInput("year_filter",
"Filter by Completion Year:",
choices = sort(unique(as.character(combined_geojson$Completion.Year))),
selected = NULL,
multiple = TRUE)
),
mainPanel(width = 9,
# Introduction Section
fluidRow(
column(12,
div(class = "description-section",
h4("1. Introduction"),
p("This OECD ODA Income Group Dashboard provides an interactive visualization of Official Development Assistance (ODA) projects.
The dashboard highlights project distribution, investment amounts, and income group categorizations globally. Users can filter projects by
completion year and income group to explore trends and patterns in ODA funding.")
)
)
),# Belt and Road Initiative Section
fluidRow(
column(12,
div(class = "description-section",
h4("2. Belt and Road Initiative Context"),
p("The Belt and Road Initiative (BRI) was launched in 2013 by Chinese President Xi Jinping. It aims to enhance global trade and
infrastructure connectivity through the Silk Road Economic Belt and the 21st Century Maritime Silk Road."),
p("Project Overview:"),
$ul(
tags$li("2000–2013: Total projects = 171"),
tags$li("2013–2023: Total projects = 479")
tags
)
)
)
),# Bar Chart Description
fluidRow(
column(12,
div(class = "description-section",
h4("3. Bar Chart"),
p("Visualizes the total investment (in USD millions) segmented by different income group categories. The bars provide a clear comparison
of funding allocation across income groups.")
)
)
),# Bar Chart Plot
fluidRow(
column(12, plotlyOutput("income_group_plot", height = "400px", width = "100%"))
),# Map Description
fluidRow(
column(12,
div(class = "description-section",
h4("Interactive Map"),
p("Geographical representation of project locations worldwide. Interactive tooltips provide detailed information for individual projects, such as:"),
$ul(
tags$li("Title: Project name or description."),
tags$li("Amount: The financial investment in the project."),
tags$li("Income Group: The income group classification for the project's target region.")
tags
)
)
)
),# Map
fluidRow(
column(12, leafletOutput("map", height = "600px"))
)
)
)
)
# Server
<- function(input, output, session) {
server
# Reactive filtered data
<- reactive({
filtered_data <- combined_geojson %>%
data filter(!is.na(`OECD ODA Income Group`)) # Exclude NA values
# Income group filter
if (input$income_group_filter != "All") {
<- data %>%
data filter(`OECD ODA Income Group` == input$income_group_filter)
}
# Investment amount filter
<- data %>%
data filter(Amount..Constant.USD.2021./1e6 >= input$amount_filter[1],
2021./1e6 <= input$amount_filter[2])
Amount..Constant.USD.
# Year filter
if (!is.null(input$year_filter) && length(input$year_filter) > 0) {
<- data %>%
data filter(as.character(Completion.Year) %in% input$year_filter)
}
return(data)
})
# Income group plot
$income_group_plot <- renderPlotly({
output<- filtered_data() %>%
group_data st_drop_geometry() %>%
group_by(`OECD ODA Income Group`) %>%
summarise(Total = sum(Amount..Constant.USD.2021./1e6, na.rm = TRUE)) %>%
::complete(`OECD ODA Income Group`, fill = list(Total = 0)) %>%
tidyrarrange(desc(Total)) %>%
mutate(Short_Label = case_when( # Shorten labels for x-axis
`OECD ODA Income Group` == "Low income" ~ "Low",
`OECD ODA Income Group` == "Lower middle income" ~ "Lower Middle",
`OECD ODA Income Group` == "Upper middle income" ~ "Upper"
))
# Calculate max for buffer space
<- max(group_data$Total, na.rm = TRUE) * 1.1
y_max
plot_ly(group_data,
x = ~Short_Label, # Use shortened labels for x-axis
y = ~Total,
type = "bar",
text = ~paste0(round(Total, 1), "M"),
textposition = 'outside',
hoverinfo = "text", # Use custom hover text
hovertext = ~paste("Income Group:", `OECD ODA Income Group`, "<br>",
"Total Investment:", round(Total, 1), "M USD"),
color = ~`OECD ODA Income Group`,
colors = viridis::viridis(n_distinct(group_data$`OECD ODA Income Group`), option = "D")) %>%
layout(
title = "Investment by Income Group",
xaxis = list(title = "Income Group", titlefont = list(size = 14), tickangle = 0),
yaxis = list(title = "USD Millions", range = c(0, y_max)),
margin = list(b = 100, t = 50), # Adjust margins
legend = list(orientation = "h", x = 0, y = -0.4) # Move legend below the plot
)
})
# Map rendering
$map <- renderLeaflet({
outputreq(filtered_data())
<- filtered_data()
filtered if (nrow(filtered) > 0) {
<- colorFactor(
pal palette = viridis::viridis(length(unique(filtered$`OECD ODA Income Group`)), option = "D"),
domain = unique(filtered$`OECD ODA Income Group`)
)
leaflet(filtered) %>%
addProviderTiles("CartoDB.Positron") %>%
addCircleMarkers(
lng = st_coordinates(st_centroid(filtered))[,1],
lat = st_coordinates(st_centroid(filtered))[,2],
radius = 6,
color = ~pal(`OECD ODA Income Group`),
fillOpacity = 0.7,
popup = ~paste0("<strong>Title:</strong> ", Title,
"<br><strong>Amount:</strong> $",
formatC(Amount..Constant.USD.2021./1e6, format="f", digits=1, big.mark=","), "M",
"<br><strong>Income Group:</strong> ", `OECD ODA Income Group`)
%>%
) addLegend("bottomright",
pal = pal,
values = ~`OECD ODA Income Group`,
title = "Income Groups")
else {
} leaflet() %>% addProviderTiles("CartoDB.Positron")
}
})
# Total projects
$total_projects <- renderText({
outputnrow(filtered_data())
})
# Total investment
$total_investment <- renderText({
output<- sum(filtered_data()$Amount..Constant.USD.2021., na.rm = TRUE) / 1e9
total paste0("$", formatC(total, format="f", digits=1, big.mark=","), "B")
})
}
# Run the Shiny app
shinyApp(ui = ui, server = server)
3.3 Creating Combined Shiny App
<- bs4DashPage(
ui bs4DashNavbar(title = "Development Finance Analysis Dashboard"),
bs4DashSidebar(
bs4SidebarMenu(
bs4SidebarMenuItem("China Development Finance", tabName = "china_dashboard", icon = icon("chart-line")),
bs4SidebarMenuItem("OECD ODA Analysis", tabName = "oecd_dashboard", icon = icon("globe"))
)
),
bs4DashBody(
bs4TabItems(
# China Development Finance Dashboard
bs4TabItem(
tabName = "china_dashboard",
fluidPage(
titlePanel("China Development Finance Projects"),
sidebarLayout(
sidebarPanel(
width = 3,
selectInput("region_filter", "Filter by Region:",
choices = c("All", sort(unique(combined_geojson$`Recipient Region`)))),
selectInput("sector_filter", "Filter by Sector:",
choices = c("All", sort(unique(combined_geojson$Sector.Name)))),
checkboxGroupInput("status_filter", "Project Status:",
choices = sort(unique(combined_geojson$Status))),
sliderInput("amount_filter", "Investment Amount (USD Millions):",
min = 0, max = 10000, value = c(0, 10000), step = 1),
actionButton("reset", "Reset Filters", class = "btn-primary")
),mainPanel(
width = 9,
fluidRow(
column(4, div(class = "well well-sm", h4("Total Projects"), textOutput("total_projects"))),
column(4, div(class = "well well-sm", h4("Total Investment (USD Billions)"), textOutput("total_investment"))),
column(4, div(class = "well well-sm", h4("Average Project Size (USD Millions)"), textOutput("avg_project")))
),tabsetPanel(
tabPanel("Map View", leafletOutput("map", height = "600px")),
tabPanel("Analysis",
fluidRow(
column(6, plotlyOutput("sector_plot")),
column(6, plotlyOutput("status_plot"))
),plotlyOutput("regional_plot")),
tabPanel("Data Table", DTOutput("project_table"))
)
)
)
)
),
# OECD ODA Income Group Dashboard
bs4TabItem(
tabName = "oecd_dashboard",
fluidPage(
titlePanel("OECD ODA Income Group Dashboard"),
sidebarLayout(
sidebarPanel(
width = 3,
div(class = "well well-sm",
h4("Total Projects"),
textOutput("total_projects_oecd")),
div(class = "well well-sm",
h4("Total Investment (USD Billions)"),
textOutput("total_investment_oecd")),
selectInput("income_group_filter",
"Filter by Income Group:",
choices = c("All", unique(na.omit(combined_geojson$`OECD ODA Income Group`)))),
sliderInput("amount_filter",
"Investment Amount (USD Millions):",
min = 0,
max = ceiling(max(combined_geojson$Amount..Constant.USD.2021./1e6, na.rm = TRUE)/4000)*4000,
step = 4000,
value = c(0, ceiling(max(combined_geojson$Amount..Constant.USD.2021./1e6, na.rm = TRUE)/4000)*4000)),
selectInput("year_filter",
"Filter by Completion Year:",
choices = sort(unique(as.character(combined_geojson$Completion.Year))),
selected = NULL,
multiple = TRUE)
),mainPanel(
width = 9,
# Introduction Section
fluidRow(
column(12,
div(class = "description-section",
h4("1. Introduction"),
p("This OECD ODA Income Group Dashboard provides an interactive visualization of Official Development Assistance (ODA) projects.
The dashboard highlights project distribution, investment amounts, and income group categorizations globally. Users can filter projects by
completion year and income group to explore trends and patterns in ODA funding.")
)
)
),# Belt and Road Initiative Section
fluidRow(
column(12,
div(class = "description-section",
h4("2. Belt and Road Initiative Context"),
p("The Belt and Road Initiative (BRI) was launched in 2013 by Chinese President Xi Jinping. It aims to enhance global trade and
infrastructure connectivity through the Silk Road Economic Belt and the 21st Century Maritime Silk Road."),
p("Project Overview:"),
$ul(
tags$li("2000–2013: Total projects = 171"),
tags$li("2013–2023: Total projects = 479")
tags
)
)
)
),# Bar Chart Section with Description
fluidRow(
column(12,
div(class = "description-section",
h4("3. Bar Chart"),
p("Visualizes the total investment segmented by different income group categories.")
)
)
),fluidRow(
column(12, plotlyOutput("income_group_plot_oecd", height = "400px", width = "100%"))
),# Map Section
fluidRow(
column(12,
div(class = "description-section",
h4("Interactive Map"),
p("Geographical representation of project locations worldwide.")
)
)
),fluidRow(
column(12, leafletOutput("map_oecd", height = "600px"))
)
)
)
)
)
)
)# Added comma here after the entire ui definition
)
# Server
<- function(input, output, session) {
server
# Reset button functionality for China dashboard
observeEvent(input$reset, {
updateSelectInput(session, "region_filter", selected = "All")
updateSelectInput(session, "sector_filter", selected = "All")
updateCheckboxGroupInput(session, "status_filter", selected = character(0))
updateSliderInput(session, "amount_filter", min = 0, max = 10000, value = c(0, 10000))
})
# China Dashboard: Reactive filtered data
<- reactive({
filtered_data_china <- combined_geojson
data
if (input$region_filter != "All") {
<- data %>% filter(`Recipient Region` == input$region_filter)
data
}if (input$sector_filter != "All") {
<- data %>% filter(Sector.Name == input$sector_filter)
data
}if (length(input$status_filter) > 0) {
<- data %>% filter(Status %in% input$status_filter)
data
}<- data %>%
data filter(Amount..Constant.USD.2021./1e6 >= input$amount_filter[1],
2021./1e6 <= input$amount_filter[2])
Amount..Constant.USD.
print(paste("Rows in filtered_data_china:", nrow(data))) # Debugging line
return(data)
})
# Total Projects
$total_projects <- renderText({
output<- filtered_data_china()
data <- nrow(data)
total_projects if (total_projects > 0) {
total_projectselse {
} "0" # Display '0' if no data matches filters
}
})
# Total Investment (in Billions)
$total_investment <- renderText({
output<- filtered_data_china()
data <- sum(data$Amount..Constant.USD.2021., na.rm = TRUE) / 1e9
total_investment if (!is.na(total_investment) && total_investment > 0) {
paste0("$", formatC(total_investment, format = "f", digits = 2, big.mark = ","), "B")
else {
} "$0B"
}
})
# Average Project Size (in Millions)
$avg_project <- renderText({
output<- filtered_data_china()
data <- mean(data$Amount..Constant.USD.2021., na.rm = TRUE) / 1e6
avg_project if (!is.na(avg_project) && avg_project > 0) {
paste0("$", formatC(avg_project, format = "f", digits = 2, big.mark = ","), "M")
else {
} "$0M"
}
})
$project_table <- renderDT({
output# Get the filtered data
<- filtered_data_china() %>% st_drop_geometry()
data
# Add debug print
print(paste("Number of rows in data table:", nrow(data)))
print("Column names:")
print(names(data))
if (nrow(data) == 0) {
datatable(data.frame(Message = "No data available based on the current filters."),
options = list(dom = 't'))
else {
} datatable(
data,options = list(
pageLength = 10,
scrollX = TRUE,
autoWidth = FALSE,
columnDefs = list(
# For ID, feature count, and ISO codes - very short
list(
targets = c("id", "feature_count", "Recipient.ISO.3", "Status",
"Infrastructure", "Flow Class", "Flow Type Simplified"),
width = "80px",
render = JS("
function(data, type, row) {
if (type === 'display' && data != null) {
return data.toString();
}
return data;
}")
),# For dates
list(
targets = c("Commitment.Date..MM.DD.YYYY.",
"Actual.Implementation.Start.Date..MM.DD.YYYY.",
"Actual.Completion.Date..MM.DD.YYYY.",
"Commitment.Year", "Implementation.Start.Year",
"Completion.Year"),
width = "120px",
render = JS("
function(data, type, row) {
if (type === 'display' && data != null) {
return data.toString();
}
return data;
}")
),# For text-heavy columns
list(
targets = c("Title", "Description", "Location Narrative"),
width = "300px",
render = JS("
function(data, type, row) {
if (type === 'display' && data != null && data.length > 100) {
return data.toString().substr(0, 100) + '...';
}
return data;
}")
),# Default for remaining columns
list(
targets = "_all",
width = "150px",
render = JS("
function(data, type, row) {
if (type === 'display' && data != null && data.length > 50) {
return data.toString().substr(0, 50) + '...';
}
return data;
}")
)
)
),rownames = FALSE,
class = 'stripe',
selection = 'none'
%>%
) formatStyle(
columns = colnames(data),
fontSize = '13px',
lineHeight = '1.4',
padding = '8px',
height = '80px',
backgroundColor = 'white',
whiteSpace = 'normal'
%>%
) formatCurrency(
columns = "Amount..Constant.USD.2021.",
currency = "$",
digits = 0
)
}
})
# Map output
# Map with Instructions for China Development Finance Dashboard
$map <- renderLeaflet({
outputreq(filtered_data_china())
<- colorFactor(palette = "viridis", domain = unique(filtered_data_china()$Sector.Name))
pal
leaflet(filtered_data_china()) %>%
addProviderTiles("CartoDB.Positron") %>%
addPolygons(
fillColor = ~pal(Sector.Name),
color = ~pal(Sector.Name),
weight = 2,
opacity = 1,
fillOpacity = 0.7,
highlightOptions = highlightOptions(
weight = 3,
color = "white",
fillOpacity = 1,
bringToFront = TRUE
),popup = ~paste0(
"<strong>Title:</strong> ", Title, "<br>",
"<strong>Amount:</strong> $", formatC(Amount..Constant.USD.2021./1e6, format = "f", digits = 1, big.mark = ","), "M<br>",
"<strong>Status:</strong> ", Status, "<br>",
"<strong>Sector:</strong> ", Sector.Name
),label = ~Title
%>%
) addLegend(
position = "bottomright",
pal = pal,
values = ~Sector.Name,
title = "Sectors",
opacity = 1
%>%
) # Add custom instructions
addControl(
html = '<div style="padding: 6px 8px; font-size: 14px; background: white;
background: rgba(255,255,255,0.8); box-shadow: 0 0 15px rgba(0,0,0,0.2);
border-radius: 5px; line-height: 1.5">
<strong>Map Instructions:</strong><br>
• Zoom in significantly to see detailed project shapes<br>
• Projects show actual footprints (e.g., factory layouts, infrastructure corridors)<br>
• Use mouse wheel or +/- buttons to zoom<br>
• Click on shapes for project details
</div>',
position = "topleft"
%>%
) # Set default map view
setView(lng = 0, lat = 20, zoom = 3)
})
# Sector plot
$sector_plot <- renderPlotly({
output<- filtered_data_china() %>%
sector_data st_drop_geometry() %>%
group_by(Sector.Name) %>%
summarise(Total = sum(Amount..Constant.USD.2021./1e6, na.rm = TRUE)) %>%
arrange(desc(Total))
plot_ly(sector_data, x = ~Total, y = ~reorder(Sector.Name, Total), type = 'bar', orientation = 'h') %>%
layout(title = "Top Sectors by Investment", xaxis = list(title = "Investment (USD Millions)"))
})
# Status plot
$status_plot <- renderPlotly({
output<- filtered_data_china() %>%
status_data st_drop_geometry() %>%
group_by(Status) %>%
summarise(Count = n())
plot_ly(status_data, labels = ~Status, values = ~Count, type = "pie", textinfo = "label+percent") %>%
layout(title = "Project Status Distribution")
})
# Regional distribution plot
$regional_plot <- renderPlotly({
output<- filtered_data_china() %>%
regional_data st_drop_geometry() %>%
group_by(`Recipient Region`, Sector.Name) %>%
summarise(TotalInvestment = sum(Amount..Constant.USD.2021., na.rm = TRUE))
plot_ly(regional_data, x = ~`Recipient Region`, y = ~TotalInvestment / 1e6, type = 'bar', color = ~Sector.Name) %>%
layout(title = "Regional Distribution of Investments by Sector", xaxis = list(title = "Region"), barmode = "stack")
})
# OECD Dashboard: Reactive filtered data
# OECD Dashboard: Reactive filtered data
# Filtered data reactive
<- reactive({
filtered_data_oecd <- combined_geojson %>%
data filter(!is.na(`OECD ODA Income Group`))
if (input$income_group_filter != "All") {
<- data %>%
data filter(`OECD ODA Income Group` == input$income_group_filter)
}
<- data %>%
data filter(Amount..Constant.USD.2021./1e6 >= input$amount_filter[1],
2021./1e6 <= input$amount_filter[2])
Amount..Constant.USD.
if (!is.null(input$year_filter) && length(input$year_filter) > 0) {
<- data %>%
data filter(as.character(Completion.Year) %in% input$year_filter)
}
return(data)
})
# Income group plot with improved styling
$income_group_plot_oecd <- renderPlotly({
output<- filtered_data_oecd() %>%
group_data st_drop_geometry() %>%
group_by(`OECD ODA Income Group`) %>%
summarise(Total = sum(Amount..Constant.USD.2021./1e6, na.rm = TRUE)) %>%
::complete(`OECD ODA Income Group`, fill = list(Total = 0)) %>%
tidyrarrange(desc(Total)) %>%
mutate(Short_Label = case_when(
`OECD ODA Income Group` == "Low income" ~ "Low",
`OECD ODA Income Group` == "Lower middle income" ~ "Lower Middle",
`OECD ODA Income Group` == "Upper middle income" ~ "Upper"
))
<- max(group_data$Total, na.rm = TRUE) * 1.1
y_max
plot_ly(group_data,
x = ~Short_Label,
y = ~Total,
type = "bar",
text = ~paste0(round(Total, 1), "M"),
textposition = 'outside',
hoverinfo = "text",
hovertext = ~paste("Income Group:", `OECD ODA Income Group`, "<br>",
"Total Investment:", round(Total, 1), "M USD"),
color = ~`OECD ODA Income Group`,
colors = viridis::viridis(n_distinct(group_data$`OECD ODA Income Group`), option = "D")) %>%
layout(
title = "Investment by Income Group",
xaxis = list(title = "Income Group", titlefont = list(size = 14), tickangle = 0),
yaxis = list(title = "USD Millions", range = c(0, y_max)),
margin = list(b = 100, t = 50),
legend = list(orientation = "h", x = 0, y = -0.4)
)
})
# Enhanced map with improved tooltips
$map_oecd <- renderLeaflet({
outputreq(filtered_data_oecd())
<- filtered_data_oecd()
filtered
if (nrow(filtered) > 0) {
<- colorFactor(
pal palette = viridis::viridis(length(unique(filtered$`OECD ODA Income Group`)), option = "D"),
domain = unique(filtered$`OECD ODA Income Group`)
)
leaflet(filtered) %>%
addProviderTiles("CartoDB.Positron") %>%
addCircleMarkers(
lng = st_coordinates(st_centroid(filtered))[,1],
lat = st_coordinates(st_centroid(filtered))[,2],
radius = 6,
color = ~pal(`OECD ODA Income Group`),
fillOpacity = 0.7,
popup = ~paste0("<strong>Title:</strong> ", Title,
"<br><strong>Amount:</strong> $",
formatC(Amount..Constant.USD.2021./1e6, format="f", digits=1, big.mark=","), "M",
"<br><strong>Income Group:</strong> ", `OECD ODA Income Group`)
%>%
) addLegend("bottomright",
pal = pal,
values = ~`OECD ODA Income Group`,
title = "Income Groups")
else {
} leaflet() %>% addProviderTiles("CartoDB.Positron")
}
})
# Update the metrics outputs
$total_projects_oecd <- renderText({
outputnrow(filtered_data_oecd())
})
$total_investment_oecd <- renderText({
output<- sum(filtered_data_oecd()$Amount..Constant.USD.2021., na.rm = TRUE) / 1e9
total paste0("$", formatC(total, format="f", digits=1, big.mark=","), "B")
})}
# Run the Shiny app
shinyApp(ui = ui, server = server)