ACLED Data: Protests and Civil Unrest

Reproducing maps from J. Luengo-Carbera on protests around different European Countries using ACLED Data

#TidyTuesday
Maps
Governance
{sf}
Author

Aditya Dahiya

Published

January 6, 2025

An attempt to re-create the protests size map from Jose Luengo-Cabrera’s post on X (Twitter) which he created using Mapbox.

Figure 1: A compilation graphic on protests in Spain, location and numbers.

How I made this graphic?

Loading required libraries, data import & creating custom functions. Downloading the logo.

Code
# Final plot tools
library(scales)               # Nice Scales for ggplot2
library(fontawesome)          # Icons display in ggplot2
library(ggtext)               # Markdown text support for ggplot2
library(showtext)             # Display fonts in ggplot2
library(colorspace)           # Lighten and Darken colours
library(patchwork)            # Compiling Plots

# Mapping Tools
library(sf)                   # Simple Features in R
library(terra)                # Rasters in R
library(tidyterra)            # Plotting rasters with ggplot2
library(osmdata)

# Data Import and Wrangling Tools
library(tidyverse)            # All things tidy

# Fetch data from https://acleddata.com/data-export-tool/
# Data not uploaded or attached here respecting Copyrights
raw_data <- read_csv("acled_data_europe_central_asia.csv")
elevation_raster <- rast("temp.tif")

Getting raster and vector data for the country

Code
selected_country = "Spain"

europe_bbox <- c(-20, 40, 30, 70)
names(europe_bbox) <- c("xmin", "xmax", "ymin", "ymax")

country_map <- rnaturalearth::ne_countries(
  country = selected_country,
  scale = "large"
) |> 
  select(name, geounit, iso_a3, geometry) |> 
  st_crop(europe_bbox)

country_bbox <- st_bbox(country_map)

islas_canarias_bbox <- c(-20, -12, 25, 30)
names(islas_canarias_bbox) <- c("xmin", "xmax", "ymin", "ymax")

islas_canarias <- rnaturalearth::ne_countries(
  country = selected_country,
  scale = "large"
) |> 
  st_crop(islas_canarias_bbox)

# Taking a smaller country to save data download time
# elevation_raster <- geodata::elevation_30s(
#   country = country_map$iso_a3 |> unique(), 
#   path = tempdir()
#   )

# country_osm_bbox <- osmdata::getbb(selected_country)

# roads_1 <- opq(
#   bbox = country_osm_bbox,
#   timeout = 100
#   ) |> 
#   add_osm_feature(
#     key = "highway", 
#     value = c("motorway", "trunk")
#   ) |> 
#   osmdata_sf()
# 
# object.size(roads_1) |> print(units = "Mb")

# raster::writeRaster(
#   elevation_raster,
#   filename = here::here("data_vizs", "images", "temp.tif")
# )

Visualization Parameters. Extracting colour palette.

Code
# Font for titles
font_add_google("Noto Sans",
  family = "title_font"
) 

# Font for the caption
font_add_google("PT Sans Narrow",
  family = "caption_font"
) 

# Font for plot text
font_add_google("Merriweather Sans",
  family = "body_font"
) 

showtext_auto()

# Credits: paletteer::paletteer_d("PrettyCols::TangerineBlues")
mypal <- c("#F5F5F5FF", "#93C6E1FF", "#5F93ACFF", "#2E627AFF", "#00344AFF")

mypal |> 
  seecolor::print_color()

# A base Colour
bg_col <- mypal[1]
seecolor::print_color(bg_col)

# Colour for highlighted text
text_hil <- "grey50"
seecolor::print_color(text_hil)

# Colour for the text
text_col <- "grey20"
seecolor::print_color(text_col)


# Define Base Text Size
bts <- 70 

# Caption stuff for the plot
sysfonts::font_add(
  family = "Font Awesome 6 Brands",
  regular = here::here("docs", "Font Awesome 6 Brands-Regular-400.otf")
)
github <- "&#xf09b"
github_username <- "aditya-dahiya"
xtwitter <- "&#xe61b"
xtwitter_username <- "@adityadahiyaias"
social_caption_1 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{github};</span> <span style='color: {text_hil}'>{github_username}  </span>")
social_caption_2 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{xtwitter};</span> <span style='color: {text_hil}'>{xtwitter_username}</span>")
plot_caption <- paste0(
  "**Data:** ACLED; acleddata.com", 
  " |  **Code:** ", 
  social_caption_1, 
  " |  **Graphics:** ", 
  social_caption_2
  )
rm(github, github_username, xtwitter, 
   xtwitter_username, social_caption_1, 
   social_caption_2)

# Add text to plot-------------------------------------------------
plot_title <- paste0(selected_country)

plot_subtitle <- "Protests and Riots\n(Jan 2020 - Nov 2024)"

Exploratory Data Analysis and Wrangling

Code
df <- raw_data |> 
  filter(country == selected_country) |> 
  filter(event_date > as_date("2020-01-01")) |> 
  filter(event_type == "Protests") |> 
  filter(sub_event_type %in% c("Peaceful protest", "Violent demonstration")) |> 
  filter(str_detect(tags, "crowd size")) |> 
  filter(tags != "crowd size=no report") |>
  mutate(
    crowd_size = case_when(
      str_detect(tags, "dozen") ~ 20,
      str_detect(tags, "fifty") ~ 50,
      str_detect(tags, "hundred") ~ 100,
      str_detect(tags, "thousand") ~ 1000,
      .default = parse_number(tags)
    )
  ) |> 
  select(
    event_id_cnty, event_date, year, 
    latitude, longitude,
    crowd_size
  ) |> 
  st_as_sf(coords = c("longitude", "latitude")) |> 
  st_set_crs(value = 4326) |> 
  st_crop(europe_bbox)

# Labels for the main hotspots
city_labels <- raw_data |> 
  filter(country == selected_country) |> 
  filter(event_date > as_date("2020-01-01")) |> 
  count(admin2, sort = T) |> 
  slice_max(order_by = n, n = 15) |> 
  # Get Data on locations of these cities from ChatGPT
  # Prompt: Please write the coordinates for the following 
  # cities in Spain. Give output as an sf object in R:---
  left_join(
     tibble(
        admin2 = c(
          "Barcelona", "Madrid", "A Coruna", 
          "Bizkaia", "Asturias", "Cadiz", 
          "Murcia", "Pontevedra", "Sevilla", 
          "Gipuzkoa", "Valencia", "Islas Baleares", 
          "Alicante", "Girona", "Malaga"
        ),
        lon = c(
          2.1734, -3.7038, -8.4078, -2.9335, -5.8618, -6.2926, 
          -1.1307, -8.6444, -5.9845, -1.9765, -0.3763, 
          3.0176, -0.4907, 2.8249, -4.4214
        ),
        lat = c(
          41.3851, 40.4168, 43.3713, 43.2630, 43.3619, 36.5164, 
          37.9847, 42.4305, 37.3891, 43.3128, 39.4699, 
          39.5696, 38.3452, 41.9794, 36.7213
        )
      )
  ) |> 
  st_as_sf(coords = c("lon", "lat"), crs = 4326)


# Data for 1st inset:
incidents_by_region <- raw_data |> 
  filter(country == selected_country) |> 
  filter(event_date > as_date("2020-01-01")) |> 
  count(admin1, sort = T) |> 
  mutate(prop = n / sum(n))
# Generate the layout. Sizetype can be area or radius
# Credits: https://r-graph-gallery.com/306-custom-circle-packing-with-one-level.html
library(packcircles)
packing <- circleProgressiveLayout(incidents_by_region$n, sizetype = 'area')
incidents_by_region <- cbind(incidents_by_region, packing)
incidents_circles <- circleLayoutVertices(packing, npoints = 50)
rm(packing)

# Data for the 2nd inset
df2 <- raw_data |> 
  filter(country == selected_country) |> 
  filter(event_date > as_date("2020-01-01")) |> 
  mutate(
    event_month = month(event_date, label = TRUE),
    event_year = year(event_date)
  ) |> 
  count(event_year, event_month) |> 
  mutate(x_var = make_date(event_year, event_month))

# Data for inset on Islas Canarias
df_islas_canarias <- raw_data |> 
  filter(country == selected_country) |> 
  filter(event_date > as_date("2020-01-01")) |> 
  filter(str_detect(tags, "crowd size")) |> 
  filter(tags != "crowd size=no report") |>
  mutate(
    crowd_size = case_when(
      str_detect(tags, "dozen") ~ 20,
      str_detect(tags, "fifty") ~ 50,
      str_detect(tags, "hundred") ~ 100,
      str_detect(tags, "thousand") ~ 1000,
      .default = parse_number(tags)
    )
  ) |> 
  select(
    event_id_cnty, event_date, year, 
    latitude, longitude,
    crowd_size, admin1, admin2
  ) |> 
  st_as_sf(coords = c("longitude", "latitude")) |> 
  st_set_crs(value = 4326) |> 
  st_crop(islas_canarias_bbox)

text_labels_islas_canarias <- tibble(
  city = c("Tenerife", "Gran Canaria"),
  latitude = c(28.2935, 28.1248),  # Approximate latitudes
  longitude = c(-16.6218, -15.4363) # Approximate longitudes
  ) |> 
  st_as_sf(coords = c("longitude", "latitude"), crs = 4326)

Get Inset Road Network Map of Spain

Code
library(ggmap)
# Register your Stadia Map key
# ggmap::register_stadiamaps()

stadia_bbox <- country_bbox
names(stadia_bbox) <- c("left", "bottom", "right", "top")
stadia_bbox

country_roads <- get_stadiamap(
  bbox = stadia_bbox,
  zoom = 7,
  maptype = "stamen_toner_lines"
)

Custom Theme for all plots

Code
theme_custom <- function(...){
  theme(
    plot.margin = margin(0,0,0,0, "mm"),
    plot.title.position = "plot",
    text = element_text(
      colour = text_col,
      margin = margin(0,0,0,0, "mm"),
      hjust = 0
    ),
    plot.title = element_text(
      size = 1.5 * bts,
      margin = margin(0,0,0,0, "mm"),
      hjust = 0.5
    ),
    ...
  )
}

The Base Plots

Code
# BASE MAP ------------------------------------------------------
# Test out the map
g_map <- ggplot() +
  geom_spatraster(data = elevation_raster) +
  geom_sf(
    data = country_map, 
    fill = "transparent",
    linewidth = 0.7,
    colour = text_col
  )  +
  geom_sf(
    data = df,
    mapping = aes(size = crowd_size),
    alpha = 0.2,
    colour = mypal[3]
  ) +
  scale_size_continuous(
    range = c(5, 40)
  ) +
  scale_fill_gradient2(
    low = "white", high = "grey50",
    na.value = "white"
  ) +
  ggnewscale::new_scale("size") +
  geom_sf_text(
    data = city_labels,
    mapping = aes(label = admin2, size = n),
    colour = text_col,
    family = "body_font"
  ) +
  scale_size_continuous(
    range = c(bts / 5, bts / 2)
  ) +
  guides(
    size = "none",
    fill = "none",
    new_size = "none",
    size_new = "none"
  ) +
  coord_sf(
    crs = "EPSG:3857"
  ) +
  ggthemes::theme_map(
    base_family = "body_font",
    base_size = bts
  ) +
  theme(
    plot.margin = margin(10,0,0,0, "mm"),
    legend.position = "none"
  )


# INSET 1:  PACK CIRCLES -----------------------------
g_inset_1 <- ggplot() +
  geom_polygon(
    data = incidents_circles,
    mapping = aes(x, y, group = id),
    fill = mypal[3],
    colour = bg_col
  ) +
  geom_text(
    data = incidents_by_region |> slice_max(order_by = n, n = 6),
    mapping = aes(
      x, y, 
      label = paste0(str_wrap(admin1, 7), "\n", 
                     round(prop * 100, 0), " %"), 
      size = prop
    ),
    colour = bg_col,
    lineheight = 0.3,
    family = "body_font"
  ) +
  scale_size_continuous(range = c(bts/10, bts/5)) +
  coord_equal() +
  guides(size = "none") +
  labs(title = "% incidents by region") +
  theme_void(
    base_size = bts,
    base_family = "body_font"
  ) +
  theme_custom()


# INSET 2: BAR CHART ------------------------------
g_inset_2 <- df2 |> 
  ggplot(
    mapping = aes(
      x_var, n
    )
  ) +
  geom_col(
    fill = mypal[2],
    colour = "transparent"
  ) +
  geom_col(
    fill = mypal[4],
    colour = "transparent",
    width = 10
  ) +
  geom_text(
    data = slice_max(df2, order_by = n, n = 5),
    mapping = aes(
      label = paste0(event_month, "\n", event_year)
    ),
    lineheight = 0.3,
    family = "body_font",
    nudge_y = 10,
    vjust = 0,
    colour = text_col,
    size = bts / 4
  ) +
  scale_x_date(
    breaks = make_date(year = 2020:2024, month = 6),
    date_labels = "%Y",
    date_minor_breaks = "1 month",
    # minor_breaks = make_date(
    #   year = 2020:2024, 
    #   month = rep(1:12, 5),
    #   day = 15
    #   )
  ) +
  scale_y_continuous(
    breaks = seq(0, 800, 200),
    expand = expansion(c(0,0.1))
  ) +
  labs(
    x = NULL, y = NULL,
    title = "Monthly Incidents"
  ) +
  theme_minimal(
    base_family = "body_font",
    base_size = bts
  ) +
  theme(
    panel.grid.major.x = element_blank(),
    panel.grid.minor.x = element_blank(),
    axis.line = element_line(colour = text_hil, linewidth = 1),
    panel.grid.minor.y = element_blank(),
    panel.grid.major.y = element_line(
      linewidth = 0.2,
      colour = text_hil
    ),
    axis.ticks.x = element_line(
      colour = text_hil
    ),
    axis.ticks.length.x = unit(2, "mm")
  ) +
  theme_custom() +
  theme(
    axis.text = element_text(
      margin = margin(0,0,0,0, "mm"),
      hjust = 0.5
    ),
    axis.ticks.length.y = unit(0, "mm")
  )

# INSET 3 - ISLAS CANARIAS -------------------
g_inset_3 <- ggplot() +
  geom_sf(
    data = islas_canarias,
    fill = "transparent"
  )  +
  geom_sf(
    data = df_islas_canarias,
    mapping = aes(size = crowd_size),
    alpha = 0.2,
    colour = mypal[3]
  ) +
  scale_size_continuous(
    range = c(1, 30)
  ) +
  geom_sf_text(
    data = text_labels_islas_canarias,
    mapping = aes(label = city),
    colour = text_hil,
    family = "body_font",
    size = bts / 5
  ) +
  scale_fill_gradient2(
    low = "white", high = "grey50",
    na.value = "white"
  ) +
  guides(
    size = "none",
    fill = "none"
  ) +
  coord_sf(
    crs = "EPSG:3857"
  ) +
  labs(
    title = "Islas Canarias"
  ) +
  ggthemes::theme_map(
    base_size = bts,
    base_family = "body_font"
  ) +
  theme_custom()

# INSET 4: Road Network --------------------------------
g_inset_4 <- ggmap(country_roads) +
  ggthemes::theme_map(
    base_size = bts,
    base_family = "body_font"
  ) +
  labs(title = paste0("Road Network in ", selected_country)) +
  theme_custom()

Compiling plots with patchwork

Code
library(patchwork)

plot_design <- "
AAA
AAA
AAA
BEE
BCC
DDD
"

g <- g_map + g_inset_1 + g_inset_3 + g_inset_2 + g_inset_4 +
  plot_layout(
    design = plot_design
  ) +
  plot_annotation(
    title = plot_title,
    subtitle = plot_subtitle,
    caption = plot_caption,
    theme = theme(
      plot.title = element_text(
        hjust = 1,
        size = bts * 6,
        colour = text_hil,
        margin = margin(0,0,-10,0, "mm")
      ),
      plot.subtitle = element_text(
        hjust = 1,
        size = bts,
        colour = text_hil,
        margin = margin(15,0,-40,0, "mm"),
        lineheight = 0.3
      ),
      plot.caption = element_textbox(
        margin = margin(10,0,5,0, "mm"),
        family = "caption_font",
        colour = text_hil,
        hjust = 0.5,
        size = bts
      ),
      plot.title.position = "plot",
      plot.margin = margin(5,5,5,5, "mm")
    )
  )


ggsave(
  filename = here::here(
    "projects",
    "acled_data_maps.png"
  ),
  plot = g,
  width = 400,
  height = 600,
  units = "mm",
  bg = bg_col
)

Savings the thumbnail for the webpage

Code
# Saving a thumbnail

library(magick)
# Saving a thumbnail for the webpage
image_read(here::here("projects", 
                      "acled_data_maps.png")) |> 
  image_resize(geometry = "x400") |> 
  image_write(
    here::here(
      "projects", "images",
      "acled_data_maps_logo.png"
    )
  )

Session Info

Code
# Data Import and Wrangling Tools
library(tidyverse)            # All things tidy

# Final plot tools
library(scales)               # Nice Scales for ggplot2
library(fontawesome)          # Icons display in ggplot2
library(ggtext)               # Markdown text support for ggplot2
library(showtext)             # Display fonts in ggplot2
library(colorspace)           # Lighten and Darken colours
library(patchwork)            # Compiling Plots

# Mapping Tools
library(sf)                   # Simple Features in R
library(terra)                # Rasters in R
library(tidyterra)            # Plotting rasters with ggplot2


sessioninfo::session_info()$packages |> 
  as_tibble() |> 
  select(package, 
         version = loadedversion, 
         date, source) |> 
  arrange(package) |> 
  janitor::clean_names(
    case = "title"
  ) |> 
  gt::gt() |> 
  gt::opt_interactive(
    use_search = TRUE
  ) |> 
  gtExtras::gt_theme_espn()
Table 1: R Packages and their versions used in the creation of this page and graphics