Conflict in Sub-Suharan Africa

2025

A series of graphs representing conflict in Sub-Suharan Africa from 2021 - 2022


Author

Affiliation

Benjamin Sam

 

Published

Jan. 18, 2026

Citation

Sam, 2026


Introduction

As we went through the data visualisation journey, one of the classes that grabbed me the most was the one involving maps. I hadn’t thought of using R to build much more than statistical representations, so the idea of a map wormed it’s way into my brain and seemed novel.

I also had it in my head to do the project on an interesting topic - I didn’t want to choose something dry beacuse the data was easier to work with. I settled on conflict in sub-suharan africa as it is a significant series of international conflicts that consistently go under-reported. With that as a target, I came across a map of conflict in sub-suharan Africa from 2022.

The replication process was more complicated than anticipated and I strugged at times. Annotations at scale are not for the faint of heart!

Original chart. Source: https://www.iiss.org/online-analysis/online-analysis/2022/11/acs-2022-sub-saharan-africa/

Replication

Packages

library(giscoR)
library(sf)
library(ggplot2)
library(ggforce)
library(patchwork)
library(grid)
library(png)
library(tidyverse)

Cleaning the data

This is the code to clean the data. I’ve saved the data used as two separate CSV files as the original file is large.

# data <- read_csv("data/Africa_aggregated_data_up_to-2025-11-29.csv")
# 
# data <- 
#   data |> 
#   mutate(WEEK = dmy(WEEK))
# 
# filtered_data <- 
#   data |> 
#   filter(WEEK >= as.Date("2021-03-01") & WEEK <= as.Date("2022-04-30"))
# 
# country_list <- c("Mali", "Burkina Faso", "Niger", "Chad", 
#                   "Sudan", "Nigeria", "Cameroon", "Central African Republic", 
#                   "South Sudan", "Ethiopia", "Somalia", 
#                   "Democratic Republic of Congo", "Uganda", "Mozambique")
# 
# filtered_data <- 
#   filtered_data |>
#   filter(COUNTRY %in% country_list) |> 
#   group_by(COUNTRY) |> 
#   summarise(total_fatalities = sum(FATALITIES))
# 
# # lookup table
# values_lookup <- 
#   tribble(
#     ~COUNTRY, ~human_impact, ~incidence, ~geopolitical_impact,
#     "Burkina Faso", 10, 15, 17,
#     "Cameroon", 6, 7, 7,
#     "Central African Republic", 8, 5, 44,
#     "Chad", 2, 1, 23,
#     "Democratic Republic of Congo", 32, 25, 42,
#     "Ethiopia", 24, 13, 8,
#     "Mali", 5, 10, 48,
#     "Mozambique", 4, 4, 24,
#     "Niger", 2, 3, 33,
#     "Nigeria", 26, 27, 12,
#     "Somalia", 20, 24, 42,
#     "South Sudan", 19, 7, 45,
#     "Sudan",19, 6, 30,
#     "Uganda", 0, 3, 7
# )
# 
# filtered_data <- 
#   filtered_data |> 
#   left_join(values_lookup, by = "COUNTRY")
# 
# write_csv(filtered_data, "data/filtered_data.csv")
# write_csv(values_lookup, "data/values_lookup.csv")

filtered_data <- read_csv("data/filtered_data.csv")
values_lookup <- read_csv("data/values_lookup.csv")

Base map

Colours for arc graphs

color_points <- tibble(
  value = c(0, 5, 10, 15, 17, 20, 23, 30, 32, 42, 44, 48),
  color = c("#FAF8B2", "#FAF3B0", "#F6E5A8", "#F0DA9F", "#EFD69C",
            "#EDCF97", "#E9C790", "#E4B585", "#E3B080", "#D89670",
            "#D9956D", "#CF8A69")
) |>
  arrange(value)

value_to_color <- function(val) {
  if (val %in% color_points$value) {
    return(color_points$color[color_points$value == val])
  } else {
    lower_idx <- max(which(color_points$value < val))
    upper_idx <- min(which(color_points$value > val))
    
    lower_val <- color_points$value[lower_idx]
    upper_val <- color_points$value[upper_idx]
    lower_col <- color_points$color[lower_idx]
    upper_col <- color_points$color[upper_idx]
    
    weight <- (val - lower_val) / (upper_val - lower_val)
    return(colorRamp(c(lower_col, upper_col))(weight) |>
             rgb(maxColorValue = 255))
  }
}

# Get African countries from giscoR
countries <- gisco_get_countries(
  year = "2020",
  epsg = "4326",
  resolution = "20"
)

# Filter to region
bbox <- st_bbox(c(xmin = -27, xmax = 64, ymin = -31, ymax = 41),
                crs = st_crs(4326))
bbox_poly <- st_as_sfc(bbox)
countries_in_view <- countries[st_intersects(countries, bbox_poly, sparse = FALSE), ]

# Define country colors
country_colors <- tribble(
  ~COUNTRY, ~color,
  # Conflict-affected (orange-y)
  "Mali", "#ED8758",
  "Burkina Faso", "#EC8657",
  "Niger", "#F29464",
  "Chad", "#F7B88D",
  "Sudan", "#F18B5C",
  "Abyei", "#F08A5B",
  "South Sudan", "#F08A5B",
  "Ethiopia", "#DD6F4B",
  "Somalia", "#ED8556",
  "Uganda", "#F6D1A5",
  "Democratic Republic of The Congo", "#E67D52",
  "Mozambique", "#F19F70",
  "Nigeria", "#D46443",
  "Cameroon", "#F3A172",
  "Central African Republic", "#F28C5D",
  # Other Sub-Saharan (white)
  "Mauritania", "#FFFFFF",
  "Senegal", "#FFFFFF",
  "Gambia", "#FFFFFF",
  "Guinea-Bissau", "#FFFFFF",
  "Cape Verde", "#FFFFFF",
  "Guinea", "#FFFFFF",
  "Sierra Leone", "#FFFFFF",
  "Côte D’Ivoire", "#FFFFFF",
  "Ghana", "#FFFFFF",
  "Togo", "#FFFFFF",
  "Benin", "#FFFFFF",
  "Gabon", "#FFFFFF",
  "Congo", "#FFFFFF",
  "Angola", "#FFFFFF",
  "Zambia", "#FFFFFF",
  "Namibia", "#FFFFFF",
  "Botswana", "#FFFFFF",
  "Zimbabwe", "#FFFFFF",
  "South Africa", "#FFFFFF",
  "Madagascar", "#FFFFFF",
  "Malawi", "#FFFFFF",
  "United Republic of Tanzania", "#FFFFFF",
  "Burundi", "#FFFFFF",
  "Rwanda", "#FFFFFF",
  "Kenya", "#FFFFFF",
  "Eritrea", "#FFFFFF",
  "Djibouti", "#FFFFFF",
  "Liberia", "#FFFFFF",
  "Equatorial Guinea", "#FFFFFF",
  "Eswatini", "#FFFFFF",
  "Lesotho", "#FFFFFF",
)

# Merge colors with map data
countries_in_view_colored <- countries_in_view |> 
  left_join(country_colors, by = c("NAME_ENGL" = "COUNTRY")) |>
  mutate(fill_color = ifelse(is.na(color), "#F0EDE3", color))

# Create coastline
all_countries_union <- st_union(countries_in_view)
coastline <- st_cast(st_boundary(all_countries_union), "LINESTRING")

# COUNTRY LABELS
# Auto-labeled countries (using centroids)
auto_countries <- c(
  "Niger", 
  "Chad", 
  "Senegal",  
  "Liberia", 
  "Togo", 
  "Gabon", 
  "Angola", 
  "Namibia",
  "South Africa", 
  "Botswana", 
  "Zimbabwe",  
  "Malawi",
  "Madagascar", 
  "Burundi", 
  "Rwanda", 
  "Kenya",
  "Djibouti"
)

countries_auto <- countries_in_view |>
  filter(NAME_ENGL %in% auto_countries)

# Precisely positioned countries (many did not behave well. Here the text at 
# the base of the plot about the sources of the data was plotted as well 
#originally, but I needed to affect it independently later so it was dropped to a different section.
custom_positions <- tribble(
  ~name, ~lon, ~lat,
    "SOUTH\nSUDAN", 30, 8.0,
  "CENTRAL AFRICAN\nREPUBLIC", 20.0, 6.5,
  "SOMALIA", 45.5, 4.2,
  "ETHIOPIA", 40.5, 9.0,
  "BURKINA\nFASO", -0.5, 12.5,
  "MOZAMBIQUE", 35.0, -18.0,
  "UGANDA", 33, 2.5,
  "SUDAN", 30.0, 15.0,
  "CABO\nVERDE", -23.5, 18.5,
  "MALI", -7, 14,
  "ERITREA", 38, 17,
  "TANZANIA", 35, -6,
  "CÔTE\nD'IVOIRE", -5.2, 8,
  "DEMOCRATIC\nREPUBLIC\nOF THE CONGO", 23.5, -5,
  "GUINEA-\nBISSAU", -18, 11,
  "REPUBLIC\nOF CONGO", 16, 1,
  "EQUATORIAL\nGUINEA", 5.5, 2,
  "CAMEROON", 12, 5,
  "BENIN", 2.5, 10.5,
  "GHANA", -1.5, 6.5,
  "SIERRA\nLEONE", -11.5, 9,
  "GUINEA", -11, 11,
  "THE GAMBIA", -18, 13,
  "MAURITANIA", -13, 19.5,
  "NIGERIA", 6.5, 9.5,
  "ZAMBIA", 25, -15) |>
  st_as_sf(coords = c("lon", "lat"), crs = 4326)

last_line_of_text <- tribble(
  ~name, ~lon, ~lat,
  "Sources: Armed Conflict Location & Event Data Project (ACLED), www.acleddata.com; IISS analysis", -6, -29) |> 
  st_as_sf(coords = c("lon", "lat"), crs = 4326)

# FINAL MAP
# Incorporates countries with colours, the coastline, the labels and the ocean 
# colour
base_map <- ggplot() +
  geom_sf(data = countries_in_view_colored,
          aes(fill = fill_color),
          color = "#B8C0BE",
          linewidth = 0.2) +
  scale_fill_identity() +
  geom_sf(data = coastline, color = "#387298", linewidth = 0.2) +
  geom_sf_text(data = countries_auto,
               aes(label = toupper(NAME_ENGL)),
               fontface = "plain", 
               size = 3, #2.3
               lineheight = 0.9) +
  geom_sf_text(data = custom_positions,
               aes(label = name),
               fontface = "plain",
               size = 3, #2.3
               lineheight = 0.9) +
  geom_sf_text(data = last_line_of_text,
               aes(label = name),
               fontface = "plain",
               size = 2.5, #2.3
               lineheight = 0.9) +
  coord_sf(xlim = c(-27, 64), ylim = c(-31, 41), expand = FALSE) +
  theme_void() +
  theme(panel.background = element_rect(fill = "#DCE8F2"),
        plot.background = element_rect(fill = "white"))

Data creation for country graphs

create_country_data <- function(country_name, filtered_data, value_to_color) {
  filtered_data |> 
    filter(COUNTRY == country_name) |> 
    pivot_longer(
      cols = c(human_impact, incidence, geopolitical_impact),
      names_to = "category",
      values_to = "value"
    ) |> 
    mutate(original_segment = row_number()) |> 
    arrange(desc(row_number())) |> 
    mutate(
      color = map_chr(value, value_to_color),
      segment = row_number(),
      original_start = pi - ((original_segment - 1) / 3 * pi),
      original_end = pi - (original_segment / 3 * pi),
      start_angle = (pi - ((segment - 1) / 3 * pi)) - pi/2,
      end_angle = (pi - (segment / 3 * pi)) - pi/2,
      mid_angle = (original_start + original_end) / 2,
      label_x = cos(mid_angle) * 1.08,
      label_y = sin(mid_angle) * 1.08
    )
}

# Create all country data frames using the function
cameroon_data <- create_country_data("Cameroon", filtered_data, value_to_color)
central_african_republic_data <- create_country_data("Central African Republic", 
                                                     filtered_data, value_to_color)
chad_data <- create_country_data("Chad", filtered_data, value_to_color)
democratic_republic_of_congo_data <- create_country_data("Democratic Republic of Congo", filtered_data, value_to_color)
ethiopia_data <- create_country_data("Ethiopia", filtered_data, value_to_color)
mali_data <- create_country_data("Mali", filtered_data, value_to_color)
mozambique_data <- create_country_data("Mozambique", filtered_data, value_to_color)
niger_data <- create_country_data("Niger", filtered_data, value_to_color)
nigeria_data <- create_country_data("Nigeria", filtered_data, value_to_color)
somalia_data <- create_country_data("Somalia", filtered_data, value_to_color)
south_sudan_data <- create_country_data("South Sudan", filtered_data, value_to_color)
sudan_data <- create_country_data("Sudan", filtered_data, value_to_color)
uganda_data <- create_country_data("Uganda", filtered_data, value_to_color)
burkina_faso_data <- create_country_data("Burkina Faso", filtered_data, value_to_color)

Country arc plots

create_arc_plot <- function(data, country_name, is_tall = FALSE) {
  ymin_val <- if(is_tall) -0.8 else -0.6
  ylim_val <- if(is_tall) c(-0.8, 1.0) else c(-0.6, 1.0)
  text_y <- if(is_tall) -0.55 else -0.42
  
  # Create base plot
  p <- ggplot(data) +
    annotate("rect", xmin = -1.2, xmax = 1.2, ymin = ymin_val, ymax = 1,
             fill = alpha("white", 0.5), color = "black", linewidth = 0.2) +
    geom_arc_bar(aes(x0 = 0, y0 = -0.3, r0 = 0.6, r = 0.9,
                     start = start_angle, end = end_angle, fill = color),
                 color = "white", linewidth = 0.2) +
    scale_fill_identity()
  
  # Add text layers conditionally based on the height of the plot (the 
  # placement of the text differs for 2 of the plots as the names of the 
  # countries are longer, so they needed to be taller.)
  
  if (is_tall) {
    p <- p +
      geom_text(aes(x = label_x * 1.05, y = (label_y - 0.3) * 1.05, 
                    label = value),
                fontface = "plain", size = 2.75, lineheight = 0.9) +
      annotate("text", x = 0, y = text_y, label = country_name, 
               fontface = "bold", size = 2.75, lineheight = 0.9)
  } else {
    p <- p +
      geom_text(aes(x = label_x * 1.05, y = (label_y - 0.3) * 1.05, 
                    label = value),
                fontface = "plain", size = 2.75, lineheight = 0.9) +
      annotate("text", x = 0, y = text_y, label = country_name, 
               fontface = "bold", size = 2.75, lineheight = 0.9)
  }
  
  p <- p +
    coord_fixed(xlim = c(-1.3, 1.3), ylim = ylim_val) +
    theme_void() +
    theme(plot.background = element_rect(fill = NA, color = NA),
          plot.margin = margin(0, 0, 0, 0),
          text = element_text(size = rel(1)))
  
  return(p)
}

# Create all arc plots
cameroon_plot <- create_arc_plot(cameroon_data, "Cameroon")
central_african_republic_plot <- create_arc_plot(central_african_republic_data, 
                                  "Central African\nRepublic", is_tall = TRUE)
chad_plot <- create_arc_plot(chad_data, "Chad")
democratic_republic_of_congo_plot <- 
  create_arc_plot(democratic_republic_of_congo_data, 
                  "Democratic Rep.\nof the Congo", is_tall = TRUE)
ethiopia_plot <- create_arc_plot(ethiopia_data, "Ethiopia")
mali_plot <- create_arc_plot(mali_data, "Mali")
mozambique_plot <- create_arc_plot(mozambique_data, "Mozambique")
niger_plot <- create_arc_plot(niger_data, "Niger")
nigeria_plot <- create_arc_plot(nigeria_data, "Nigeria")
somalia_plot <- create_arc_plot(somalia_data, "Somalia")
south_sudan_plot <- create_arc_plot(south_sudan_data, "South Sudan")
sudan_plot <- create_arc_plot(sudan_data, "Sudan")
uganda_plot <- create_arc_plot(uganda_data, "Uganda")
burkina_faso_plot <- create_arc_plot(burkina_faso_data, "Burkina Faso")

Lines from arc plots and boxes

pixel_to_lonlat <- function(x_pixels, y_pixels, 
                            map_width = 2232, map_height = 1783,
                            lon_min = -27, lon_max = 64,
                            lat_min = -31, lat_max = 41) {
  x_prop <- x_pixels / map_width
  y_prop <- y_pixels / map_height
  lon <- lon_min + x_prop * (lon_max - lon_min)
  lat <- lat_max - y_prop * (lat_max - lat_min)
  return(list(lon = lon, lat = lat))
}

# ALL CONNECTION LINES (these are the pixel measurements discussed above. 
# You read them as pairs; distance from the keft edge of the map is the x and 
# from the top is the y. Taken together they define points for the lines 
# to be plotted to.)
connections <- list(
  #arc graphs
  mali = list(
    x = c(298, 298, 445, 445),
    y = c(212, 250, 250, 670)
  ),
  burkina_faso = list(
    x = c(586, 586),
    y = c(209, 714)
  ),
  niger = list(
    x = c(869, 869),
    y = c(209, 531)
  ),
  chad = list(
    x = c(1131, 1131),
    y = c(209, 600)
  ),
  sudan = list(
    x = c(1379, 1379),
    y = c(209, 576)
  ),
  south_sudan = list(
    x = c(1630, 1630, 1459, 1459),
    y = c(330, 519, 519, 886)
  ),
  ethiopia = list(
    x = c(1967, 1612, 1612),
    y = c(627, 627, 768)
  ),
  somalia = list(
    x = c(1967, 1871, 1871, 1800),
    y = c(810, 810, 887, 887)
  ),
  uganda = list(
    x = c(1967, 1450),
    y = c(992, 992)
  ),
  drc = list(
    x = c(1967, 1351),
    y = c(1185, 1185)
  ),
  mozambique = list(
    x = c(1967, 1477),
    y = c(1593, 1593)
  ),
  central_african_republic = list(
    x = c(1090, 1126, 1126, 1090),
    y = c(1197, 1197, 885, 885)
  ),
  cameroon = list(
    x = c(557, 874, 874, 955, 955),
    y = c(1051, 1051, 940, 940, 921)
  ),
  nigeria = list(
    x = c(308, 840, 840),
    y = c(924, 924, 807)
  ),
  #country/flag boxes
  the_sahel_left = list(
    x = c(613, 613),
    y = c(553, 600)
  ),
  the_sahel_middle = list(
    x = c(644, 644),
    y = c(553, 661)
  ),
  the_sahel_right = list(
    x = c(804, 804),
    y = c(553, 605)
  ),
  lake_chad_basin_top_left = list(
    x = c(975, 975, 882),
    y = c(664, 609, 609)
  ),
  lake_chad_basin_bottom_left = list(
    x = c(902, 902, 862),
    y = c(790, 822, 822)
  ),
  lake_chad_basin_bottom = list(
    x = c(971, 971),
    y = c(790, 841)
  ),
  lake_chad_basin_right = list(
    x = c(1129, 1157, 1157),
    y = c(721, 721, 662)
  ),
  great_lakes_region_bottom = list(
    x = c(1252, 1252),
    y = c(1061, 1084)
  ),
  great_lakes_region_right = list(
    x = c(1340, 1447),
    y = c(1023, 1023)
  ),
  rawanda_eu_sadcmm = list(
    x = c(1697, 1697, 1550),
    y = c(1370, 1437, 1437)
  ),
  turkey_uk_us_au_eu = list(
    x = c(1773, 1773),
    y = c(1022, 942)
  ),
  eritrea = list(
    x = c(1657, 1681),
    y = c(862, 862)
  ),
  un_top = list(
    x = c(1332, 1332),
    y = c(687, 653)
  ),
  un_bottom = list(
    x = c(1333, 1333),
    y = c(750, 850)
  ),
  russia_rwanda_un = list(
    x = c(1252, 1252, 1191),
    y = c(445, 785, 785)
  )
)

# white endpoints
# The lines used for the arc graphs and countries involved in the various 
# conflicts are differentiated by the colour of the end points they use.
white_endpoints <- c("the_sahel_left", 
                     "the_sahel_middle", 
                     "the_sahel_right", 
                     "lake_chad_basin_top_left", 
                     "lake_chad_basin_bottom_left", 
                     "lake_chad_basin_bottom",
                     "lake_chad_basin_right",
                     "great_lakes_region_bottom",
                     "great_lakes_region_right",
                     "rawanda_eu_sadcmm_bottom",
                     "rawanda_eu_sadcmm",
                     "turkey_uk_us_au_eu",
                     "eritrea",
                     "un_top",
                     "un_bottom",
                     "russia_rwanda_un")

# Lines
lines_df <- map_dfr(names(connections), function(country) {
  coords <- pixel_to_lonlat(
    x_pixels = connections[[country]]$x,
    y_pixels = connections[[country]]$y
  )
  data.frame(
    x = coords$lon,
    y = coords$lat,
    country = country
  )
})

# Endpoints
# This section makes a dataframe using the final coordinates of the lines as 
# the position of the endpoint.
endpoints_df <- map_dfr(names(connections), function(country) {
  n <- length(connections[[country]]$x)
  coords <- pixel_to_lonlat(
    x_pixels = connections[[country]]$x[n],
    y_pixels = connections[[country]]$y[n]
  )
  data.frame(
    x = coords$lon, 
    y = coords$lat, 
    country = country,
    is_white = country %in% white_endpoints
  )
})

# Split into black and white endpoints
black_endpoints <- endpoints_df |> filter(!is_white)
white_endpoints_df <- endpoints_df |> filter(is_white)

# Add lines and endpoints to map
base_map <- base_map +
  geom_path(data = lines_df,
            aes(x = x, y = y, group = country),
            color = "black",
            linewidth = 0.3) +
  # Black endpoints (fill and color both black)
  geom_point(data = black_endpoints,
             aes(x = x, y = y),
             color = "black",
             fill = "black",
             shape = 21,
             size = 1.5,
             stroke = 0.3) +
  # White endpoints
  geom_point(data = white_endpoints_df,
             aes(x = x, y = y),
             color = "black",
             fill = "white",
             shape = 21,
             size = 1.5,
             stroke = 0.3)

Arc graphs

# Standard arc box dimensions
# These dimensions applied for the majority of the arc graphs
arc_w <- 215 / 2232
arc_h <- 156 / 1783

# CAR and DRC dimensions
# I realised quite late that these graphs were subtly different in height due 
# to the length of the country name needing to fit in the box
car_drc_h <- 183 / 1783

# Map dimensions
map_width <- 2232
map_height <- 1783

# Helper function for positioning
# I reused the same method as for the lines, this time measuring to the left 
# edge of the plots and the bottom edge.
pixel_to_prop <- function(left_px, top_px, height_px = 156) {
  list(
    left = left_px / map_width,
    bottom = 1 - (top_px + height_px) / map_height
  )
}

# Helper function to add inset to map
add_inset <- function(map, plot, left, bottom, width, height) {
  map + inset_element(
    plot,
    left = left,
    right = left + width,
    bottom = bottom,
    top = bottom + height
  )
}

# Define all arc plot configurations
arc_configs <- list(
  list(plot = mali_plot, pos = pixel_to_prop(190, 77), w = arc_w, h = arc_h),
  list(plot = burkina_faso_plot, pos = pixel_to_prop(487, 77), 
       w = arc_w, h = arc_h),
  list(plot = niger_plot, pos = pixel_to_prop(754, 77), w = arc_w, h = arc_h),
  list(plot = chad_plot, pos = pixel_to_prop(1020, 77), w = arc_w, h = arc_h),
  list(plot = sudan_plot, pos = pixel_to_prop(1272, 77), w = arc_w, h = arc_h),
  list(plot = south_sudan_plot, pos = pixel_to_prop(1528, 196), 
       w = arc_w, h = arc_h),
  list(plot = ethiopia_plot, pos = pixel_to_prop(1947, 547), w = arc_w, h = arc_h),
  list(plot = somalia_plot, pos = pixel_to_prop(1947, 733), w = arc_w, h = arc_h),
  list(plot = uganda_plot, pos = pixel_to_prop(1947, 914), w = arc_w, h = arc_h),
  list(plot = democratic_republic_of_congo_plot, 
       pos = pixel_to_prop(1947, 1108, height_px = 183), 
       w = arc_w, h = car_drc_h),
  list(plot = mozambique_plot, pos = pixel_to_prop(1947, 1517), 
       w = arc_w, h = arc_h),
  list(plot = nigeria_plot, pos = pixel_to_prop(114, 836), 
       w = arc_w, h = arc_h),
  list(plot = cameroon_plot, pos = pixel_to_prop(364, 943), 
       w = arc_w, h = arc_h),
  list(plot = central_african_republic_plot, 
       pos = pixel_to_prop(897, 1104, height_px = 183), 
       w = arc_w, h = car_drc_h)
)

# Add all arc plots to map
for (config in arc_configs) {
  base_map <- add_inset(base_map, config$plot, config$pos$left, 
                        config$pos$bottom, config$w, config$h)
}

Boxes

Sahel box

create_sahel_box <- function() {
  france_flag <- readPNG("flags_and_logos/france.png")
  italy_flag <- readPNG("flags_and_logos/italy.png")
  usa_flag <- readPNG("flags_and_logos/us.png")
  eu_logo <- readPNG("flags_and_logos/eu.png")
  un_logo <- readPNG("flags_and_logos/un.png")
  au_logo <- readPNG("flags_and_logos/au.png")
  g5_logo <- readPNG("flags_and_logos/sahel_g5.png")
  
  flag_width <- 65 / 245
  flag_height <- 43 / 205
  logo_width <- 65 / 245
  logo_height <- 43 / 205
  logo_circle_width <- 43 / 245
  logo_circle_height <- 43 / 205
  
  p <- ggplot() +
    annotate("rect", xmin = 0, xmax = 1, ymin = 0, ymax = 1,
             fill = "white", color = NA) +
    annotate("text", x = 0.5, y = 0.92, label = "The Sahel",
             hjust = 0.5, fontface = "bold", size = 2.75, lineheight = 0.9) +
    annotation_raster(france_flag, 
                      xmin = 0.08, xmax = 0.08 + flag_width, 
                      ymin = 0.62, ymax = 0.62 + flag_height) +
    annotation_raster(italy_flag, 
                      xmin = 0.5 - (flag_width / 2), xmax = 0.5 + 
                        (flag_width / 2), 
                      ymin = 0.62, ymax = 0.62 + flag_height) +
    annotation_raster(usa_flag, 
                      xmin = 0.92 - flag_width, xmax = 0.92, 
                      ymin = 0.62, ymax = 0.62 + flag_height) +
    annotation_raster(eu_logo, 
                      xmin = 0.08, xmax = 0.08 + logo_width, 
                      ymin = 0.30, ymax = 0.30 + logo_height) +
    annotation_raster(un_logo, 
                      xmin = 0.5 - (logo_width / 2), xmax = 0.5 + 
                        (logo_width / 2), 
                      ymin = 0.30, ymax = 0.30 + logo_height) +
    annotation_raster(au_logo, 
                      xmin = 0.92 - logo_width, xmax = 0.92, 
                      ymin = 0.30, ymax = 0.30 + logo_height) +
    annotation_raster(g5_logo, 
                      xmin = 0.5 - (logo_circle_width / 2), 
                      xmax = 0.5 + (logo_circle_width / 2), 
                      ymin = 0.02, 
                      ymax = 0.02 + logo_circle_height) +
    coord_fixed(ratio = 205/245, xlim = c(0, 1), ylim = c(0, 1)) +
    theme_void() +
    theme(
      plot.background = element_rect(fill = "white", color = "black", 
                                     linewidth = 0.5, radius = unit(0, "pt")),
      plot.margin = margin(0, 0, 0, 0)
    )
  
  return(p)
}

Lake chad box

create_lake_chad_box <- function() {
  france_flag <- readPNG("flags_and_logos/france.png")
  uk_flag <- readPNG("flags_and_logos/uk.png")
  usa_flag <- readPNG("flags_and_logos/us.png")
  multi_jtf_logo <- readPNG("flags_and_logos/multi.png")
  
  left_margin <- 50 / 235
  flag_width <- 65 / 235
  gap <- 5 / 235
  flag_height <- 43 / 145
  logo_width <- 65 / 235
  logo_height <- 43 / 145
  
  p <- ggplot() +
    annotate("rect", xmin = 0, xmax = 1, ymin = 0, ymax = 1,
             fill = "white", color = NA) +
    annotate("text", x = 0.5, y = 0.88, label = "Lake Chad Basin",
             hjust = 0.5, fontface = "bold", size = 2.75, lineheight = 0.9) +
    annotation_raster(france_flag, 
                      xmin = left_margin, 
                      xmax = left_margin + flag_width, 
                      ymin = 0.4, 
                      ymax = 0.4 + flag_height) +
    annotation_raster(uk_flag, 
                      xmin = left_margin + flag_width + gap, 
                      xmax = left_margin + flag_width + gap + flag_width, 
                      ymin = 0.4, 
                      ymax = 0.4 + flag_height) +
    annotation_raster(usa_flag, 
                      xmin = left_margin, 
                      xmax = left_margin + flag_width, 
                      ymin = 0.06, 
                      ymax = 0.06 + flag_height) +
    annotation_raster(multi_jtf_logo, 
                      xmin = left_margin + flag_width + gap, 
                      xmax = left_margin + flag_width + gap + logo_width, 
                      ymin = 0.06, 
                      ymax = 0.06 + logo_height) +
    coord_fixed(ratio = 145/235, xlim = c(0, 1), ylim = c(0, 1)) +
    theme_void() +
    theme(
      plot.background = element_rect(fill = "white", color = "black", 
                                     linewidth = 0.5, radius = unit(0, "pt")),
      plot.margin = margin(0, 0, 0, 0)
    )
  
  return(p)
}

Great lakes box

create_great_lakes_box <- function() {
  un_logo <- readPNG("flags_and_logos/un.png")
  au_logo <- readPNG("flags_and_logos/au.png")
  rwanda_flag <- readPNG("flags_and_logos/rwanda.png")
  burundi_flag <- readPNG("flags_and_logos/burundi.png")
  
  box_width <- 177
  box_height <- 177
  left_margin <- 13 / box_width
  flag_width <- 65 / box_width
  gap <- 25 / box_width
  flag_height <- 43 / box_height
  logo_width <- 65 / box_width
  logo_height <- 43 / box_height
  
  p <- ggplot() +
    annotate("rect", xmin = 0, xmax = 1, ymin = 0, ymax = 1,
             fill = "white", color = NA) +
    annotate("text", x = 0.5, y = 0.88, label = "Great\nLakes Region",
             hjust = 0.5, fontface = "bold", size = 2.75, lineheight = 0.9) +
    annotation_raster(au_logo, 
                      xmin = left_margin, 
                      xmax = left_margin + flag_width, 
                      ymin = 0.4, 
                      ymax = 0.4 + flag_height) +
    annotation_raster(un_logo, 
                      xmin = left_margin + flag_width + gap, 
                      xmax = left_margin + flag_width + gap + flag_width, 
                      ymin = 0.4, 
                      ymax = 0.4 + flag_height) +
    annotation_raster(rwanda_flag, 
                      xmin = left_margin, 
                      xmax = left_margin + flag_width, 
                      ymin = 0.06, 
                      ymax = 0.06 + flag_height) +
    annotation_raster(burundi_flag, 
                      xmin = left_margin + flag_width + gap, 
                      xmax = left_margin + flag_width + gap + logo_width, 
                      ymin = 0.06, 
                      ymax = 0.06 + logo_height) +
    coord_fixed(ratio = box_height/box_width, xlim = c(0, 1), ylim = c(0, 1)) +
    theme_void() +
    theme(
      plot.background = element_rect(fill = "white", color = "black", 
                                     linewidth = 0.5, radius = unit(0, "pt")),
      plot.margin = margin(0, 0, 0, 0)
    )
  
  return(p)
}

Rwanda EU SADCMM Box

create_two_row_box <- function() {
  rwanda_flag <- readPNG("flags_and_logos/rwanda.png")
  eu_logo <- readPNG("flags_and_logos/eu.png")
  sadcmm_flag <- readPNG("flags_and_logos/sadcmm.png")
  
  box_width <- 195
  box_height <- 135
  top_margin <- 65 / box_width
  top_flag_width <- 65 / box_width
  bottom_left_margin <- 22 / box_width
  bottom_flag_width <- 65 / box_width
  bottom_gap <- 21 / box_width
  flag_height <- 43 / box_height
  
  p <- ggplot() +
    annotate("rect", xmin = 0, xmax = 1, ymin = 0, ymax = 1,
             fill = "white", color = NA) +
    annotation_raster(rwanda_flag, 
                      xmin = top_margin, 
                      xmax = top_margin + top_flag_width, 
                      ymin = 0.55, 
                      ymax = 0.55 + flag_height) +
    annotation_raster(eu_logo, 
                      xmin = bottom_left_margin, 
                      xmax = bottom_left_margin + bottom_flag_width, 
                      ymin = 0.15, 
                      ymax = 0.15 + flag_height) +
    annotation_raster(sadcmm_flag, 
                      xmin = bottom_left_margin + bottom_flag_width + 
                        bottom_gap, 
                      xmax = bottom_left_margin + bottom_flag_width + 
                        bottom_gap + bottom_flag_width, 
                      ymin = 0.15, 
                      ymax = 0.15 + flag_height) +
    coord_fixed(ratio = box_height/box_width, xlim = c(0, 1), ylim = c(0, 1)) +
    theme_void() +
    theme(
      plot.background = element_rect(fill = "white", color = "black", 
                                     linewidth = 0.5, radius = unit(0, "pt")),
      plot.margin = margin(0, 0, 0, 0)
    )
  
  return(p)
}

Turkey UK US African Union EU BOX

create_three_two_box <- function() {
  top_flag1 <- readPNG("flags_and_logos/turkey.png")
  top_flag2 <- readPNG("flags_and_logos/uk.png")
  top_flag3 <- readPNG("flags_and_logos/us.png")
  bottom_flag1 <- readPNG("flags_and_logos/au.png")
  bottom_flag2 <- readPNG("flags_and_logos/eu.png")
  
  box_width <- 245
  box_height <- 138
  top_space <- 12.5 / box_width
  top_flag_width <- 65 / box_width
  bottom_flag_width <- 65 / box_width
  bottom_gap <- 10 / box_width
  bottom_margin <- 52.5 / box_width
  flag_height <- 43 / box_height
  
  p <- ggplot() +
    annotate("rect", xmin = 0, xmax = 1, ymin = 0, ymax = 1,
             fill = "white", color = NA) +
    annotation_raster(top_flag1, 
                      xmin = top_space, 
                      xmax = top_space + top_flag_width, 
                      ymin = 0.55, 
                      ymax = 0.55 + flag_height) +
    annotation_raster(top_flag2, 
                      xmin = top_space * 2 + top_flag_width, 
                      xmax = top_space * 2 + top_flag_width * 2, 
                      ymin = 0.55, 
                      ymax = 0.55 + flag_height) +
    annotation_raster(top_flag3, 
                      xmin = top_space * 3 + top_flag_width * 2, 
                      xmax = top_space * 3 + top_flag_width * 3, 
                      ymin = 0.55, 
                      ymax = 0.55 + flag_height) +
    annotation_raster(bottom_flag1, 
                      xmin = bottom_margin, 
                      xmax = bottom_margin + bottom_flag_width, 
                      ymin = 0.15, 
                      ymax = 0.15 + flag_height) +
    annotation_raster(bottom_flag2, 
                      xmin = bottom_margin + bottom_flag_width + bottom_gap, 
                      xmax = bottom_margin + bottom_flag_width + bottom_gap + 
                        bottom_flag_width, 
                      ymin = 0.15, 
                      ymax = 0.15 + flag_height) +
    coord_fixed(ratio = box_height/box_width, xlim = c(0, 1), ylim = c(0, 1)) +
    theme_void() +
    theme(
      plot.background = element_rect(fill = "white", color = "black", 
                                     linewidth = 0.5, radius = unit(0, "pt")),
      plot.margin = margin(0, 0, 0, 0)
    )
  
  return(p)
}

Eritrea box

create_single_flag_box <- function() {
  eritrea_flag <- readPNG("flags_and_logos/eritrea.png")
  
  box_width <- 95
  box_height <- 65
  flag_margin <- 15 / box_width
  flag_width <- 65 / box_width
  flag_height <- 43 / box_height
  flag_y_margin <- 11 / box_height
  
  p <- ggplot() +
    annotate("rect", xmin = 0, xmax = 1, ymin = 0, ymax = 1,
             fill = "white", color = NA) +
    annotation_raster(eritrea_flag, 
                      xmin = flag_margin, 
                      xmax = flag_margin + flag_width, 
                      ymin = flag_y_margin, 
                      ymax = flag_y_margin + flag_height) +
    coord_fixed(ratio = box_height/box_width, xlim = c(0, 1), ylim = c(0, 1)) +
    theme_void() +
    theme(
      plot.background = element_rect(fill = "white", color = "black", 
                                     linewidth = 0.5, radius = unit(0, "pt")),
      plot.margin = margin(0, 0, 0, 0)
    )
  
  return(p)
}

UN box

create_single_flag_box_2 <- function() {
  un_logo <- readPNG("flags_and_logos/un.png")
  
  box_width <- 95
  box_height <- 65
  flag_margin <- 15 / box_width
  flag_width <- 65 / box_width
  flag_height <- 43 / box_height
  flag_y_margin <- 11 / box_height
  
  p <- ggplot() +
    annotate("rect", xmin = 0, xmax = 1, ymin = 0, ymax = 1,
             fill = "white", color = NA) +
    annotation_raster(un_logo, 
                      xmin = flag_margin, 
                      xmax = flag_margin + flag_width, 
                      ymin = flag_y_margin, 
                      ymax = flag_y_margin + flag_height) +
    coord_fixed(ratio = box_height/box_width, xlim = c(0, 1), ylim = c(0, 1)) +
    theme_void() +
    theme(
      plot.background = element_rect(fill = "white", color = "black", 
                                     linewidth = 0.5, radius = unit(0, "pt")),
      plot.margin = margin(0, 0, 0, 0)
    )
  
  return(p)
}

Russia Rwanda UN box

create_three_flag_row_box <- function() {
  russia_flag <- readPNG("flags_and_logos/russia.png")
  rwanda_flag <- readPNG("flags_and_logos/rwanda.png")
  un_logo <- readPNG("flags_and_logos/un.png")
  
  box_width <- 205
  box_height <- 84
  space <- 2.5 / box_width
  flag_width <- 65 / box_width
  flag_height <- 43 / box_height
  flag_y_margin <- (box_height - 43) / 2 / box_height
  
  p <- ggplot() +
    annotate("rect", xmin = 0, xmax = 1, ymin = 0, ymax = 1,
             fill = "white", color = NA) +
    annotation_raster(russia_flag, 
                      xmin = space, 
                      xmax = space + flag_width, 
                      ymin = flag_y_margin, 
                      ymax = flag_y_margin + flag_height) +
    annotation_raster(rwanda_flag, 
                      xmin = space * 2 + flag_width, 
                      xmax = space * 2 + flag_width * 2, 
                      ymin = flag_y_margin, 
                      ymax = flag_y_margin + flag_height) +
    annotation_raster(un_logo, 
                      xmin = space * 3 + flag_width * 2, 
                      xmax = space * 3 + flag_width * 3, 
                      ymin = flag_y_margin, 
                      ymax = flag_y_margin + flag_height) +
    coord_fixed(ratio = box_height/box_width, xlim = c(0, 1), ylim = c(0, 1)) +
    theme_void() +
    theme(
      plot.background = element_rect(fill = "white", color = "black", 
                                     linewidth = 0.5, radius = unit(0, "pt")),
      plot.margin = margin(0, 0, 0, 0)
    )
  
  return(p)
}

Create all box plots

sahel_box <- create_sahel_box()
lake_chad_box <- create_lake_chad_box()
great_lakes_box <- create_great_lakes_box()
two_row_box <- create_two_row_box()
three_two_box <- create_three_two_box()
single_flag_box <- create_single_flag_box()
single_flag_box_2 <- create_single_flag_box_2()
three_flag_row_box <- create_three_flag_row_box()

### DEFINE ALL BOX CONFIGURATIONS ###
# Here I position the boxes relative to the overall map
box_configs <- list(
  list(plot = sahel_box, left = 0.27, bottom = 0.68, w = 245/2232, 
       h = 205/1783),
  list(plot = lake_chad_box, left = 0.4, bottom = 0.55, w = 235/2232, 
       h = 145/1783),
  list(plot = great_lakes_box, left = 0.52, bottom = 0.4, w = 177/2232, 
       h = 177/1783),
  list(plot = two_row_box, left = 0.71, bottom = 0.22, w = 195/2232, 
       h = 135/1783),
  list(plot = three_two_box, left = 0.76, bottom = 0.35, w = 245/2232, 
       h = 138/1783),
  list(plot = single_flag_box, left = 0.70, bottom = 0.50, w = 95/2232, 
       h = 65/1783),
  list(plot = single_flag_box_2, left = 0.58, bottom = 0.577, w = 95/2232, 
       h = 65/1783),
  list(plot = three_flag_row_box, left = 0.515, bottom = 0.75, w = 205/2232, 
       h = 84/1783)
)

Add all boxes to the map

for (config in box_configs) {
  base_map <- add_inset(base_map, config$plot, config$left, config$bottom, 
                        config$w, config$h)
}

Key

Grey arc

grey_legend_data <- tibble(
  segment = 1:3,
  label = c("A", "B", "C"),
  value = c("A", "B", "C"),
  color = "#D7D7D5",
  original_segment = 1:3
) |>
  arrange(desc(row_number())) |>
  mutate(
    segment = row_number(),
    original_start = pi - ((original_segment - 1) / 3 * pi),
    original_end = pi - (original_segment / 3 * pi),
    start_angle = (pi - ((segment - 1) / 3 * pi)) - pi/2,
    end_angle = (pi - (segment / 3 * pi)) - pi/2,
    mid_angle = (original_start + original_end) / 2,
    label_x = cos(mid_angle) * 0.77,
    label_y = sin(mid_angle) * 0.77
  )

# Plotting the grey legend arc plot
grey_legend_arc <- ggplot(grey_legend_data) +
  annotate("rect", xmin = -1.2, xmax = 1.2, ymin = -0.6, ymax = 1,
           fill = "white", color = NA) +
  geom_arc_bar(aes(x0 = 0, y0 = -0.3, r0 = 0.6, r = 0.9,
                   start = start_angle, end = end_angle, fill = color),
               color = "white", linewidth = 0.2) +
  scale_fill_identity() +
  geom_text(aes(x = label_x * 1.05, y = (label_y - 0.3) * 1.05, label = value),
            fontface = "bold", size = 2.75, lineheight = 0.9) +
  coord_fixed(xlim = c(-1.3, 1.3), ylim = c(-0.6, 1.0)) +
  theme_void() +
  theme(plot.background = element_rect(fill = "white", color = NA),
        plot.margin = margin(0, 0, 0, 0),
        text = element_text(size = rel(1)))

Text

white_rectangle <- ggplot() +
  annotate("rect", xmin = 0, xmax = 1, ymin = 0, ymax = 1,
           fill = "white") +
  
  # Text - line 1
  annotate("text", x = 0.02, y = 1.00,
           label = "ACGRI pillars: IISS calculation based on multiple sources",
           hjust = 0, vjust = 1, size = 2.5) +
  
  # Text - line 2
  annotate("text", x = 0.02, y = 0.93,
           label = "for 2021 and January–April 2022 (scale: 0–100)",
           hjust = 0, vjust = 1, size = 2.5) +
  
  # Text to the right of arc - line 1
  annotate("text", x = 0.35, y = 0.80,
           label = "A: human impact, B: Incidence;",
           hjust = 0, vjust = 1, size = 2.5) +
  
  # Text to the right of arc - line 2
  annotate("text", x = 0.35, y = 0.73,
           label = "C: Geopolitical impact",
           hjust = 0, vjust = 1, size = 2.5) +
  
  # tText below arc
  annotate("text", x = 0.02, y = 0.59,
         label = "Number of fatalities, March 2021–April 2022",
         hjust = 0, vjust = 1, size = 2.5) +
  
  # Text left of bar
  annotate("text", x = 0.02, y = 0.49, label = "High", hjust = 0, vjust = 0.5, 
           size = 2.5) +
  
  # Text right of bar
  annotate("text", x = 0.62, y = 0.49, label = "Low", hjust = 0, vjust = 0.5, 
           size = 2.5) +
  
  # Text below bar
  annotate("text", x = 0.02, y = 0.41,
         label = "Involvement of third-party countries",
         hjust = 0, vjust = 1, size = 2.5) +
  
  # Peacekeeping text
  annotate("text", x = 0.02, y = 0.23, 
           label = "Peacekeeping, stabilisation and military-training 
           operations",
           hjust = 0, vjust = 1, size = 2.5) +
  
  coord_fixed(ratio = 582/833, xlim = c(0, 1), ylim = c(0, 1)) +
  theme_void() +
  theme(
    plot.background = element_rect(fill = "white", color = "black", 
                                   linewidth = 0.5),
    plot.margin = margin(0, 0, 0, 0)
  )

Position and add

white_rect_w <- 833 / 2232
white_rect_h <- 582 / 1783
white_rect_left <- 50 / 2232
white_rect_bottom <- 80 / 1783

# Add white rectangle to map
base_map <- base_map +
  inset_element(
    white_rectangle,
    left = white_rect_left,
    right = white_rect_left + white_rect_w,
    bottom = white_rect_bottom,
    top = white_rect_bottom + white_rect_h
  )

# Add grey arc
grey_arc_w_map <- 215 / 2232
grey_arc_h_map <- 156 / 1783

# Position it inside the white rectangle
grey_arc_left <- white_rect_left + (20 / 2232)
grey_arc_bottom <- white_rect_bottom + (345 / 1783)

base_map <- base_map +
  inset_element(
    grey_legend_arc,
    left = grey_arc_left,
    right = grey_arc_left + grey_arc_w_map,
    bottom = grey_arc_bottom,
    top = grey_arc_bottom + grey_arc_h_map
  )

Horizontal scale

# Colours
color_scale_data <- tibble(
  x = seq(0, 48, length.out = 100),
  y = 1,
  fill_color = map_chr(x, value_to_color)
)

# Colour bar
color_scale_bar_only <- ggplot(color_scale_data, aes(x = x, y = y, 
                                                     fill = fill_color)) +
  geom_tile(width = 0.5, height = 1) +
  scale_fill_identity() +
  scale_x_reverse(expand = c(0, 0)) +
  scale_y_continuous(expand = c(0, 0)) +
  coord_fixed(ratio = 5, xlim = c(0, 48), ylim = c(0.5, 1.5)) +
  theme_void() +
  theme(
    plot.background = element_rect(fill = "white", color = NA),
    plot.margin = margin(0, 0, 0, 0)
  )

Flags countries

create_involvement_flags <- function() {
  # Load flag images
  burundi_flag <- readPNG("flags_and_logos/burundi.png")
  eritrea_flag <- readPNG("flags_and_logos/eritrea.png")
  france_flag <- readPNG("flags_and_logos/france.png")
  italy_flag <- readPNG("flags_and_logos/italy.png")
  russia_flag <- readPNG("flags_and_logos/russia.png")
  rwanda_flag <- readPNG("flags_and_logos/rwanda.png")
  south_africa_flag <- readPNG("flags_and_logos/south_africa.png")
  turkey_flag <- readPNG("flags_and_logos/turkey.png")
  uk_flag <- readPNG("flags_and_logos/uk.png")
  us_flag <- readPNG("flags_and_logos/us.png")
  
  # Flag dimensions and spacing
  flag_width_px <- 54
  flag_height_px <- 37
  flag_width <- flag_width_px / 833
  flag_height <- flag_height_px / 582
  n_flags <- 10
  total_flags_width <- n_flags * flag_width
  spacing <- (1 - total_flags_width) / (n_flags + 1)
  
  # Helper function to calculate flag position
  flag_x <- function(i) spacing + (i * (flag_width + spacing))
  
  p <- ggplot() +
    annotate("rect", xmin = 0, xmax = 1, ymin = 0, ymax = 1, fill = NA, 
             color = NA) +
    
    # Burundi
    annotation_raster(burundi_flag,
                      xmin = flag_x(0), xmax = flag_x(0) + flag_width,
                      ymin = 0.6, ymax = 0.6 + flag_height) +
    annotate("text", x = flag_x(0) + flag_width/2, y = 0.58, 
             label = "Burundi", hjust = 0.5, vjust = 1, size = 2) +
    
    # Eritrea
    annotation_raster(eritrea_flag,
                      xmin = flag_x(1), xmax = flag_x(1) + flag_width,
                      ymin = 0.6, ymax = 0.6 + flag_height) +
    annotate("text", x = flag_x(1) + flag_width/2, y = 0.58, 
             label = "Eritrea", hjust = 0.5, vjust = 1, size = 2) +
    
    # France
    annotation_raster(france_flag,
                      xmin = flag_x(2), xmax = flag_x(2) + flag_width,
                      ymin = 0.6, ymax = 0.6 + flag_height) +
    annotate("text", x = flag_x(2) + flag_width/2, y = 0.58, 
             label = "France", hjust = 0.5, vjust = 1, size = 2) +
    
    # Italy
    annotation_raster(italy_flag,
                      xmin = flag_x(3), xmax = flag_x(3) + flag_width,
                      ymin = 0.6, ymax = 0.6 + flag_height) +
    annotate("text", x = flag_x(3) + flag_width/2, y = 0.58, 
             label = "Italy", hjust = 0.5, vjust = 1, size = 2) +
    
    # Russia
    annotation_raster(russia_flag,
                      xmin = flag_x(4), xmax = flag_x(4) + flag_width,
                      ymin = 0.6, ymax = 0.6 + flag_height) +
    annotate("text", x = flag_x(4) + flag_width/2, y = 0.58, 
             label = "Russia", hjust = 0.5, vjust = 1, size = 2) +
    
    # Rwanda
    annotation_raster(rwanda_flag,
                      xmin = flag_x(5), xmax = flag_x(5) + flag_width,
                      ymin = 0.6, ymax = 0.6 + flag_height) +
    annotate("text", x = flag_x(5) + flag_width/2, y = 0.58, 
             label = "Rwanda", hjust = 0.5, vjust = 1, size = 2) +
    
    # South Africa
    annotation_raster(south_africa_flag,
                      xmin = flag_x(6), xmax = flag_x(6) + flag_width,
                      ymin = 0.6, ymax = 0.6 + flag_height) +
    annotate("text", x = flag_x(6) + flag_width/2, y = 0.58, 
             label = "S.Africa", hjust = 0.5, vjust = 1, size = 2) +
    
    # Turkey
    annotation_raster(turkey_flag,
                      xmin = flag_x(7), xmax = flag_x(7) + flag_width,
                      ymin = 0.6, ymax = 0.6 + flag_height) +
    annotate("text", x = flag_x(7) + flag_width/2, y = 0.58, 
             label = "Turkey", hjust = 0.5, vjust = 1, size = 2) +
    
    # UK
    annotation_raster(uk_flag,
                      xmin = flag_x(8), xmax = flag_x(8) + flag_width,
                      ymin = 0.6, ymax = 0.6 + flag_height) +
    annotate("text", x = flag_x(8) + flag_width/2, y = 0.58, 
             label = "UK", hjust = 0.5, vjust = 1, size = 2) +
    
    # US
    annotation_raster(us_flag,
                      xmin = flag_x(9), xmax = flag_x(9) + flag_width,
                      ymin = 0.6, ymax = 0.6 + flag_height) +
    annotate("text", x = flag_x(9) + flag_width/2, y = 0.58, 
             label = "US", hjust = 0.5, vjust = 1, size = 2) +
    
    coord_fixed(ratio = 582/833, xlim = c(0, 1), ylim = c(0, 1)) +
    theme_void() +
    theme(
      plot.background = element_rect(fill = NA, color = NA),
      plot.margin = margin(0, 0, 0, 0)
    )
  
  return(p)
}

Add all parts to the white box

# Plot flags
flags_plot <- create_involvement_flags()

# Position the bar
bar_w_map <- 600 / 2232
bar_h_map <- 30 / 1783
bar_left <- white_rect_left + (0.02 * (833 / 2232)) + (5 / 2232)
bar_bottom <- white_rect_bottom + (260 / 1783)

# Add the bar
base_map <- base_map +
  inset_element(
    color_scale_bar_only,
    left = bar_left,
    right = bar_left + bar_w_map,
    bottom = bar_bottom,
    top = bar_bottom + bar_h_map
  )

# Add flags
flags_w_map <- 833 / 2232
flags_h_map <- 600 / 1783

base_map <- base_map +
  inset_element(
    flags_plot,
    left = white_rect_left,
    right = white_rect_left + flags_w_map,
    bottom = white_rect_bottom - (400 / 1783),
    top = white_rect_bottom + (50 / 1783) + flags_h_map
  )

Peacekeeping logos

create_peacekeeping_logos <- function() {
  # Load logo images
  un_logo <- readPNG("flags_and_logos/un.png")
  eu_logo <- readPNG("flags_and_logos/eu.png")
  au_logo <- readPNG("flags_and_logos/au.png")
  g5_logo <- readPNG("flags_and_logos/sahel_g5.png")
  multi_logo <- readPNG("flags_and_logos/multi.png")
  sadcmm_logo <- readPNG("flags_and_logos/sadcmm.png")
  
  # Logo dimensions
  logo_width_px <- 54
  logo_height_px <- 37
  logo_width <- logo_width_px / 833
  logo_height <- logo_height_px / 582
  
  # Manual positioning
  spacing_left <- 0.02
  logo_gap <- 0.12
  logo_x <- function(i) spacing_left + (i * logo_gap)
  last_logo_x <- 0.75
  
  p <- ggplot() +
    annotate("rect", xmin = 0, xmax = 1, ymin = 0, ymax = 1, fill = NA, 
             color = NA) +
    
    # UN
    annotation_raster(un_logo,
                      xmin = logo_x(0), xmax = logo_x(0) + logo_width,
                      ymin = 0.5, ymax = 0.5 + logo_height) +
    annotate("text", x = logo_x(0) + logo_width/2, y = 0.48, 
             label = "UN", hjust = 0.5, vjust = 1, size = 2, lineheight = 0.9) +
    
    # EU
    annotation_raster(eu_logo,
                      xmin = logo_x(1), xmax = logo_x(1) + logo_width,
                      ymin = 0.5, ymax = 0.5 + logo_height) +
    annotate("text", x = logo_x(1) + logo_width/2, y = 0.48, 
             label = "EU", hjust = 0.5, vjust = 1, size = 2, lineheight = 0.9) +
    
    # AU
    annotation_raster(au_logo,
                      xmin = logo_x(2), xmax = logo_x(2) + logo_width,
                      ymin = 0.5, ymax = 0.5 + logo_height) +
    annotate("text", x = logo_x(2) + logo_width/2, y = 0.48, 
             label = "African\nUnion", hjust = 0.5, vjust = 1, size = 2, 
             lineheight = 0.9) +
    
    # G5 Sahel
    annotation_raster(g5_logo,
                      xmin = logo_x(3), xmax = logo_x(3) + logo_width,
                      ymin = 0.5, ymax = 0.5 + logo_height) +
    annotate("text", x = logo_x(3) + logo_width/2, y = 0.48, 
             label = "G5\nSahel", hjust = 0.5, vjust = 1, size = 2, 
             lineheight = 0.9) +
    
    # MNJTF
    annotation_raster(multi_logo,
                      xmin = logo_x(4), xmax = logo_x(4) + logo_width,
                      ymin = 0.5, ymax = 0.5 + logo_height) +
    annotate("text", x = logo_x(4) + logo_width/2, y = 0.48, 
             label = "Multinational\nJoint Task\nForce", hjust = 0.5, vjust = 1, 
             size = 2, lineheight = 0.9) +
    
    # SADCMM
    annotation_raster(sadcmm_logo,
                      xmin = last_logo_x, xmax = last_logo_x + logo_width,
                      ymin = 0.5, ymax = 0.5 + logo_height) +
    annotate("text", x = last_logo_x + logo_width/2, y = 0.48, 
             label = "Southern African\nDevelopment Community\nMission in Mozambique", hjust = 0.5, vjust = 1, size = 2, lineheight = 0.9) +
    
    coord_fixed(ratio = 582/833, xlim = c(0, 1), ylim = c(0, 1)) +
    theme_void() +
    theme(
      plot.background = element_rect(fill = NA, color = NA),
      plot.margin = margin(0, 0, 0, 0)
    )
  
  return(p)
}

# Create logos plot
logos_plot <- create_peacekeeping_logos()

logos_w_map <- 833 / 2232
logos_h_map <- 200 / 1783

base_map <- base_map +
  inset_element(
    logos_plot,
    left = white_rect_left,
    right = white_rect_left + logos_w_map,
    bottom = white_rect_bottom - (620 / 1783),
    top = white_rect_bottom + (590 / 1783) + logos_h_map
  )

Plot final map

# after fiddling for a VERY LONG time, I realised I could define these as variables
fig.width = 11
fig.height = 1.75 * fig.width
base_map

Improvement

Rationale for improvement

After recreating the map I was ready for a simpler graph type. I thought the initial map was very cluttered and difficult to read, privileging the form of the map over the story the data was trying to tell. My approach was to strip away the map and focus on getting the data across as cleanly and clearly as possible. With no map, I judged the positions of the countries and relations with each other and the external parties to the conflicts of lesser importance than the impacts and fatalities. I iterated through different versions of bar/column graphs until I found one I feel communicates the data clearly.

Improvement graph

My initial idea was to plot the data for each metric as a separate graph, matching the scale on the x axis. I thought using the same colour scale as the original graph might relate the two, however I found it hard to read. I also realised that the statistic of total fatalities was important to the overall story the original told, so needed to include that as well.

bar_data_1 <- values_lookup |>
  pivot_longer(
    cols = c(human_impact, incidence, geopolitical_impact),
    names_to = "metric",
    values_to = "value"
  ) |>
  mutate(
    color = map_chr(value, value_to_color),
    metric = factor(metric, 
                    levels = c("human_impact", "incidence", "geopolitical_impact"),
                    labels = c("Human Impact", "Incidence", "Geopolitical Impact"))
  )

# Create the 3 stacked bar graphs
improvement_plot_1 <- ggplot(bar_data_1, aes(x = reorder(COUNTRY, value), y = value, fill = color)) +
  geom_col() +
  scale_fill_identity() +
  coord_flip() +
  facet_wrap(~metric, ncol = 1) +
  labs(x = NULL, y = "Impact") +
  theme_minimal() +
  theme(
    strip.text = element_text(face = "bold", size = 12),
    axis.text.y = element_text(size = 9),
    panel.grid.minor = element_blank(),
    panel.grid.major.y = element_blank()
  )

improvement_plot_1

I worked on a second graph, correcting the issues I had with the first attempt. As you can see below I didn’t account for the difference in scale of the data between fatalities and the other metrics.

bar_data_2 <- filtered_data |>
  pivot_longer(
    cols = c(human_impact, incidence, geopolitical_impact, total_fatalities),
    names_to = "metric",
    values_to = "value"
  ) |>
  mutate(
    metric = factor(metric, 
                    levels = c("human_impact", "incidence", "geopolitical_impact", "total_fatalities"),
                    labels = c("Human Impact", "Incidence", "Geopolitical Impact", "total_fatalities")),
    COUNTRY = factor(COUNTRY, levels = sort(unique(COUNTRY), decreasing = TRUE))
  )

# Create grouped graph
improvement_plot_2 <- ggplot(bar_data_2, aes(x = COUNTRY, y = value, fill = metric)) +
  geom_col(position = position_dodge(width = 0.8), width = 0.7) +
  scale_fill_manual(
    values = c(
      "Human Impact" = "#E63946",
      "Incidence" = "#457B9D",
      "Geopolitical Impact" = "#2A9D8F",
      "total_fatalities" = "black"
    ),
    name = NULL
  ) +
  coord_flip() +
  labs(x = NULL, y = "Impact") +
  theme_minimal() +
  theme(
    legend.position = "top",
    axis.text.y = element_text(size = 10),
    panel.grid.minor = element_blank(),
    panel.grid.major.y = element_blank()
  )

improvement_plot_2

To solve the scale problem, I decided to create two graphs and plot them side-by-side. I didn’t think about the position of the legends and later had to adjust them.

bar_data_3 <- filtered_data |>
  pivot_longer(
    cols = c(human_impact, incidence, geopolitical_impact, total_fatalities),
    names_to = "metric",
    values_to = "value"
  ) |>
  mutate(
    metric = factor(metric, 
                    levels = c("human_impact", "incidence", "geopolitical_impact", "total_fatalities"),
                    labels = c("Human Impact", "Incidence", "Geopolitical Impact", "total_fatalities")),
    COUNTRY = factor(COUNTRY, levels = sort(unique(COUNTRY), decreasing = TRUE))
  )
# Left plot
bar_data_left <- bar_data_3 |> filter(metric != "total_fatalities")

plot_left <- ggplot(bar_data_left, aes(x = COUNTRY, y = value, fill = metric)) +
  geom_col(position = position_dodge(width = 0.8), width = 0.7) +
  scale_fill_manual(
    values = c("Human Impact" = "#E63946", "Incidence" = "#457B9D", "Geopolitical Impact" = "#2A9D8F"),
    name = NULL) +
  coord_flip() +
  labs(x = NULL, y = "Impact") +
  theme_minimal() +
  theme(legend.position = "top", axis.text.y = element_text(size = 10))

# Right plot
bar_data_right <- bar_data_3 |> filter(metric == "total_fatalities")

plot_right <- ggplot(bar_data_right, aes(x = COUNTRY, y = value, fill = metric)) +
  geom_col(width = 0.7) +
  scale_fill_manual(
    values = c("total_fatalities" = "black"),
    name = NULL,
    labels = c("Total Fatalities")
  ) +
  coord_flip() +
  labs(x = NULL, y = "Fatalities") +
  theme_minimal() +
  theme(
    legend.position = "top",
    axis.text.y = element_blank(), 
    axis.ticks.y = element_blank()
  )

# Combine
plot_left + 
plot_right +
plot_layout(widths = c(2, 1)) +
  plot_annotation(
    title = "Regional Armed Conflict in Sub-Suharan Africa",
    theme = theme(plot.title = element_text(size = 14, face = "bold"))
  )

This is my final graph for the recreation. I realised that the colours I had chosen were not decided on with colour blindness in mind, so I updated the palette to accomodate that idea.I also corrected the spacing of the legends so they didnt overlap.

bar_data_3 <- filtered_data |>
  pivot_longer(
    cols = c(human_impact, incidence, geopolitical_impact, total_fatalities),
    names_to = "metric",
    values_to = "value"
  ) |>
  mutate(
    metric = factor(metric, 
                    levels = c("human_impact", "incidence", "geopolitical_impact", "total_fatalities"),
                    labels = c("Human Impact", "Incidence", "Geopolitical Impact", "total_fatalities")),
    COUNTRY = factor(COUNTRY, levels = sort(unique(COUNTRY), decreasing = TRUE))
  )
# Left plot
bar_data_left <- bar_data_3 |> filter(metric != "total_fatalities")

plot_left <- ggplot(bar_data_left, aes(x = COUNTRY, y = value, fill = metric)) +
  geom_col(position = position_dodge(width = 0.8), width = 0.7) +
  scale_fill_viridis_d(name = NULL, option = "viridis") +
  coord_flip() +
  labs(x = NULL, y = "Impact") +
  theme_minimal() +
  theme(legend.position = "top", axis.text.y = element_text(size = 10))

# Right plot
bar_data_right <- bar_data_3 |> filter(metric == "total_fatalities")

plot_right <- ggplot(bar_data_right, aes(x = COUNTRY, y = value, fill = metric)) +
  geom_col(width = 0.7) +
  scale_fill_manual(
    values = c("total_fatalities" = "black"),
    name = NULL,
    labels = c("Total Fatalities")
  ) +
  coord_flip() +
  labs(x = NULL, y = "Fatalities") +
  theme_minimal() +
  theme(
    legend.position = "top",
    legend.box.margin = margin(l = 5), # I had to plot the legends separately and combine them. This is to position Total fatalities so it doesn't overlap.
    axis.text.y = element_blank(), 
    axis.ticks.y = element_blank()
  )

# Combine
plot_left + 
plot_right +
plot_layout(widths = c(2, 2)) +
  plot_annotation(
    title = "Regional Armed Conflict in Sub-Suharan Africa",
    subtitle = "Impacts across 3 measures and Total fatalities",
    theme = theme(plot.title = element_text(size = 14, face = "bold"))
  )

Footnotes

    Reuse

    Text and figures are licensed under Creative Commons Attribution CC BY 4.0. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".

    Citation

    For attribution, please cite this work as

    Sam (2026, Jan. 19). Data Visualization | MSc CSS: Conflict in Sub-Suharan Africa. Retrieved from https://csslab.uc3m.es/dataviz/projects/2025/100577218/

    BibTeX citation

    @misc{sam2026conflict,
      author = {Sam, Benjamin},
      title = {Data Visualization | MSc CSS: Conflict in Sub-Suharan Africa},
      url = {https://csslab.uc3m.es/dataviz/projects/2025/100577218/},
      year = {2026}
    }