1 Libraries, IDs & shared spatial layers

library(tidyverse)
library(lubridate)
library(sf)
library(terra)
library(MASS)
library(adehabitatHR)
library(sp)
library(patchwork)
library(rnaturalearth)
library(ggnewscale)
library(smoothr)
library(units)
library(ctmm)
past_ids_vec <- c(
  "AG082", "AG083", "AG088", "AG089", "AG096", "AG432",
  "st2010-1048_2", "st2010-1052", "st2010-1053"
)
present_ids_vec <- c(
  "171604 - adWBV05", "171605 - Matira",     "171606 - Ilkeliani",
  "171607 - Baldrick", "171608 - adWBV06",   "171609 - Charlie",
  "171612 - Olarro",   "171613 - Tipilikwani"
)
all_ids <- c(past_ids_vec, present_ids_vec)
pas       <- sf::st_read(
  "/Users/oli/University/Zoology/5th_Year_MSc/MSC_Thesis/GIS/FINAL_OUTPUTS.gpkg",
  quiet = TRUE
)
snp       <- pas[pas$NAME == "Serengeti National Park", ]
mara      <- pas[pas$NAME == "Masai Mara", ]
other_pas <- pas[!pas$NAME %in% c("Serengeti National Park", "Masai Mara"), ]

gsme <- sf::st_read(
  "/Users/oli/University/Zoology/5th_Year_MSc/MSC_Thesis/GIS/V7_Serengeti_Ecosystem_Boundary.shp",
  quiet = TRUE
) %>% sf::st_transform(4326)

world <- ne_countries(scale = 50, returnclass = "sf")
dir.create("figures_final", showWarnings = FALSE)
gps <- readRDS("WBV_resampled_2h.rds") %>%
  rename(t_ = timestamp, x_ = longitude, y_ = latitude)

akdes               <- readRDS("WBV_akdes.rds")
akdes_wet           <- readRDS("WBV_std_akdes_wet.rds")
akdes_dry           <- readRDS("WBV_std_akdes_dry.rds")
ud_areas            <- readRDS("WBV_ud_areas.rds")
pa_overlap          <- readRDS("WBV_pa_overlap.rds")
all_contours        <- readRDS("WBV_pop_ud_contours.rds")
ind_smooth_contours <- readRDS("WBV_ind_seasonal_contours.rds")
centroid_dist       <- readRDS("WBV_centroid_dist.rds")
vul_nsd             <- readRDS("WBV_nsd.rds")
zone_pct            <- readRDS("WBV_zone_pct.rds")
wb_mig_past         <- readRDS("WB_mig_past_proxy.rds")

Figure 3 — Home range area across UD levels

ud_areas_plot <- ud_areas %>%
  mutate(
    period = factor(period, c("Past", "Present")),
    level  = factor(level,  c("25%", "50%", "75%", "95%"))
  )

fig3 <- ggplot(ud_areas_plot, aes(x = level, y = area, fill = period)) +
  geom_boxplot(alpha = 0.7, outlier.shape = 21,
               position = position_dodge(0.8), width = 0.6) +
  geom_point(position = position_jitterdodge(jitter.width = 0.1, dodge.width = 0.8),
             size = 1.8, alpha = 0.8) +
  scale_fill_manual(values = c(Past = "#E07B54", Present = "#5B8DB8"), name = "Period") +
  scale_y_log10(labels = scales::comma) +
  labs(x = "UD contour level",
       y = expression("AKDE area (km"^2*", log scale)")) +
  theme_classic(base_size = 13) +
  theme(legend.position = "bottom",
        panel.grid.major.y = element_line(colour = "grey88"))

ggsave("figures_final/WBV_ud_levels.png", fig3,
       width = 7, height = 5, dpi = 300, bg = "white")
print(fig3)


Figure 4 — Population-level UD contours

all_contours_plot <- all_contours %>%
  mutate(period = factor(period, c("Past", "Present")),
         level  = factor(level,  c("95%", "75%", "50%", "25%")))

fig4 <- ggplot() +
  geom_sf(data = world,     fill = "grey93", colour = "grey70", linewidth = 0.3) +
  geom_sf(data = other_pas, fill = "#d9ead3", colour = "grey60", linewidth = 0.2) +
  geom_sf(data = mara,      fill = "#d9ead3", colour = "#3a7d44", linewidth = 0.5) +
  geom_sf(data = snp,       fill = "#d9ead3", colour = "#3a7d44", linewidth = 0.7) +
  geom_sf(data = gsme,      fill = NA, colour = "grey20", linewidth = 0.5,
          linetype = "dashed") +
  geom_sf(data = all_contours_plot,
          aes(fill = level, colour = level), alpha = 0.4, linewidth = 0.5) +
  scale_fill_manual(
    values = c("95%" = "#fddbc7", "75%" = "#f4a582",
               "50%" = "#d6604d", "25%" = "#b2182b"), name = "UD level") +
  scale_colour_manual(
    values = c("95%" = "#fddbc7", "75%" = "#f4a582",
               "50%" = "#d6604d", "25%" = "#b2182b"), name = "UD level") +
  coord_sf(xlim = c(33.6, 36.4), ylim = c(-4.0, 1.0), expand = FALSE) +
  facet_wrap(~ period) +
  labs(x = NULL, y = NULL) +
  theme_bw(base_size = 12) +
  theme(strip.background = element_rect(fill = "grey20"),
        strip.text       = element_text(colour = "white", face = "bold", size = 11),
        legend.position  = "bottom",
        panel.grid       = element_line(colour = "grey88", linewidth = 0.2))

ggsave("figures_final/WBV_ud_contours_map_pa.png", fig4,
       width = 10, height = 6, dpi = 300, bg = "white")
print(fig4)


Figure 5 — Protected area overlap

pa_plot <- pa_overlap %>%
  mutate(period = factor(period, c("Past", "Present")),
         level  = factor(level,  c("50%", "95%")))

fig5 <- ggplot(pa_plot, aes(x = level, y = pct_in_pa, fill = period)) +
  geom_boxplot(outlier.shape = 16, outlier.size = 2, width = 0.5, alpha = 0.7,
               position = position_dodge(0.6)) +
  geom_point(position = position_jitterdodge(jitter.width = 0.08, dodge.width = 0.6),
             size = 2, alpha = 0.8) +
  scale_fill_manual(values = c(Past = "#E07B54", Present = "#5B8DB8"), name = "Period") +
  labs(x = "UD contour level", y = "Space use within protected areas (%)") +
  theme_classic(base_size = 13) +
  theme(legend.position = "bottom")

ggsave("figures_final/WBV_pa_overlap_boxplot.png", fig5,
       width = 6, height = 5, dpi = 300, bg = "white")
print(fig5)


Figure 6 — Seasonal population space use vs wildebeest density

make_kde_df <- function(lon, lat, h = 0.3, thresh_q = 0.35) {
  k  <- kde2d(lon, lat, h = h, n = 300, lims = c(33.5, 36.5, -4.0, -0.8))
  m  <- t(k$z)[nrow(t(k$z)):1, ]
  r  <- rast(m, extent = c(33.5, 36.5, -4.0, -0.8), crs = "EPSG:4326")
  r  <- log10(r + 1e-10)
  df <- as.data.frame(r, xy = TRUE)
  names(df)[3] <- "logdens"
  df$logdens[df$logdens < quantile(df$logdens, thresh_q, na.rm = TRUE)] <- NA
  df
}

make_contour_lines_sf <- function(lon, lat, h = 0.12, ud_levels = c(0.75, 0.95)) {
  k        <- kde2d(lon, lat, h = h, n = 200, lims = c(33.5, 36.5, -4.0, -0.8))
  z        <- k$z
  cell     <- diff(k$x[1:2]) * diff(k$y[1:2])
  z_norm   <- z / (sum(z) * cell)
  z_sorted <- sort(as.vector(z_norm), decreasing = TRUE)
  z_cum    <- cumsum(z_sorted) * cell
  thresholds <- sapply(ud_levels, function(lev) z_sorted[which(z_cum >= lev)[1]])

  line_list <- lapply(seq_along(ud_levels), function(i) {
    cl <- contourLines(x = k$x, y = k$y, z = z_norm, levels = thresholds[i])
    if (length(cl) == 0) return(NULL)
    cl <- cl[sapply(cl, function(s) length(s$x)) > 30]
    if (length(cl) == 0) return(NULL)
    lines <- lapply(cl, function(seg) st_linestring(cbind(seg$x, seg$y)))
    sf::st_sf(level = paste0(ud_levels[i] * 100, "%"),
              geometry = st_sfc(lines, crs = 4326))
  })
  do.call(rbind, Filter(Negate(is.null), line_list))
}
wb_sf <- sf::st_as_sf(
  wb_mig_past %>% filter(!is.na(UTM_X)),
  coords = c("UTM_X", "UTM_Y"), crs = 32736
) %>% sf::st_transform(4326)
wb_coords       <- sf::st_coordinates(wb_sf)
wb_mig_past$lon <- wb_coords[, 1]
wb_mig_past$lat <- wb_coords[, 2]

combos <- list(
  list(period = "Past",    season = "Wet"),
  list(period = "Past",    season = "Dry"),
  list(period = "Present", season = "Wet"),
  list(period = "Present", season = "Dry")
)

wb_df_list <- list(); cont_sf_list <- list()

for (x in combos) {
  key    <- paste(x$period, x$season)
  wb_sub <- wb_mig_past %>% filter(season == x$season)
  df     <- make_kde_df(wb_sub$lon, wb_sub$lat)
  df$period <- x$period; df$season <- x$season
  wb_df_list[[key]] <- df

  gps_sub <- gps %>% filter(period == x$period, season == x$season)
  cont    <- tryCatch(
    make_contour_lines_sf(gps_sub$x_, gps_sub$y_, h = 0.12),
    error = function(e) NULL
  )
  if (!is.null(cont)) {
    cont$period <- x$period; cont$season <- x$season
    cont_sf_list[[key]] <- cont
  }
}

wb_df_all <- do.call(rbind, wb_df_list) %>%
  mutate(period = factor(period, c("Past","Present")),
         season = factor(season,  c("Wet","Dry")))

cont_all <- do.call(rbind, cont_sf_list) %>%
  mutate(period = factor(period, c("Past","Present")),
         season = factor(season,  c("Wet","Dry")))
fig6 <- ggplot() +
  geom_raster(data = wb_df_all %>% filter(!is.na(logdens)),
              aes(x = x, y = y, fill = logdens), alpha = 0.85) +
  scale_fill_gradientn(
    colours  = c("#FFF5E0","#FDCC8A","#FC8D59","#D7301F","#7F0000"),
    name     = "Wildebeest\nlog density", na.value = "transparent") +
  geom_sf(data = gsme,      fill = NA, colour = "black",   linewidth = 0.5, linetype = "dashed") +
  geom_sf(data = other_pas, fill = NA, colour = "#3a7d44", linewidth = 0.4) +
  geom_sf(data = mara,      fill = NA, colour = "#3a7d44", linewidth = 0.7) +
  geom_sf(data = snp,       fill = NA, colour = "#3a7d44", linewidth = 0.9) +
  geom_sf(data = cont_all %>% filter(level == "95%"),
          colour = "#1a1a2e", linewidth = 0.6, alpha = 0.6) +
  geom_sf(data = cont_all %>% filter(level == "75%"),
          colour = "#1a1a2e", linewidth = 1.2, alpha = 1.0) +
  facet_grid(period ~ season) +
  coord_sf(xlim = c(33.5, 36.5), ylim = c(-4.0, -0.8), expand = FALSE) +
  labs(x = NULL, y = NULL) +
  theme_classic(base_size = 12) +
  theme(strip.background = element_rect(fill = "grey20"),
        strip.text       = element_text(colour = "white", face = "bold", size = 12),
        legend.position  = "bottom",
        axis.text        = element_text(size = 8),
        panel.spacing    = unit(0.4, "lines"))

ggsave("figures_final/WBV_seasonal_vs_wildebeest_pop_v7.png", fig6,
       width = 10, height = 10, dpi = 300, bg = "white")
print(fig6)


Figure 7a & 7b — Individual seasonal home ranges vs wildebeest

make_wb_rast <- function(df, h = 5000, grid = 300) {
  coords <- df %>% filter(!is.na(UTM_X), !is.na(UTM_Y))
  sp_obj <- SpatialPoints(cbind(coords$UTM_X, coords$UTM_Y),
                          proj4string = CRS("+proj=utm +zone=36 +south +datum=WGS84"))
  ud   <- kernelUD(sp_obj, h = h, grid = grid)
  spdf <- as(ud, "SpatialPixelsDataFrame")
  r    <- terra::rast(spdf)
  terra::crs(r) <- "+proj=utm +zone=36 +south +datum=WGS84"
  r_ll <- terra::project(r, "EPSG:4326")
  r_ll / terra::global(r_ll, "max", na.rm = TRUE)$max
}

wb_past_wet <- make_wb_rast(wb_mig_past %>% filter(season == "Wet"))
wb_past_dry <- make_wb_rast(wb_mig_past %>% filter(season == "Dry"))

wb_df <- bind_rows(
  as.data.frame(wb_past_wet, xy = TRUE) %>% rename(dens = 3) %>% mutate(period = "Past",    season = "Wet"),
  as.data.frame(wb_past_dry, xy = TRUE) %>% rename(dens = 3) %>% mutate(period = "Past",    season = "Dry"),
  as.data.frame(wb_past_wet, xy = TRUE) %>% rename(dens = 3) %>% mutate(period = "Present", season = "Wet"),
  as.data.frame(wb_past_dry, xy = TRUE) %>% rename(dens = 3) %>% mutate(period = "Present", season = "Dry")
)
cont95 <- ind_smooth_contours %>%
  filter(level == "95%") %>%
  smoothr::drop_crumbs(threshold = units::set_units(300, km^2)) %>%
  smoothr::smooth(method = "ksmooth", smoothness = 3) %>%
  mutate(id_short = factor(gsub("^[0-9]+ - ", "", id)))

make_period_plot <- function(period_val, ids, strip_col, wb_season, wb_colours) {
  sub_kde <- cont95 %>% filter(id %in% ids)
  wb_bg   <- wb_df %>% filter(period == period_val, season == wb_season, dens > 0.1)

  ggplot() +
    geom_sf(data = world, fill = "grey97", colour = "grey75", linewidth = 0.15) +
    geom_raster(data = wb_bg, aes(x = x, y = y, fill = dens),
                interpolate = TRUE, alpha = 0.75) +
    scale_fill_gradientn(colours = wb_colours, limits = c(0, 1), guide = "none") +
    geom_sf(data = other_pas, fill = NA, colour = "grey65", linewidth = 0.15) +
    geom_sf(data = mara, fill = NA, colour = "#3a7d44", linewidth = 0.3) +
    geom_sf(data = snp,  fill = NA, colour = "#3a7d44", linewidth = 0.4) +
    geom_sf(data = gsme, fill = NA, colour = "grey35",  linewidth = 0.4, linetype = "dashed") +
    geom_sf(data = sub_kde %>% filter(season == "Wet"),
            fill = NA, colour = "#2166ac", linewidth = 0.8) +
    geom_sf(data = sub_kde %>% filter(season == "Dry"),
            fill = NA, colour = "#b2182b", linewidth = 0.8) +
    geom_hline(yintercept = -1.5, colour = "grey60", linewidth = 0.2, linetype = "longdash") +
    coord_sf(xlim = c(33.6, 36.4), ylim = c(-4.0, 1.0), expand = FALSE) +
    facet_wrap(~ id_short, nrow = 2) +
    labs(title = period_val, x = NULL, y = NULL) +
    theme_bw(base_size = 9) +
    theme(
      plot.title       = element_text(colour = "white", face = "bold", hjust = 0.5,
                                      size = 12, margin = margin(t = 4, b = 4)),
      strip.background = element_rect(fill = "grey88"),
      strip.text       = element_text(size = 7.5, face = "bold"),
      panel.grid       = element_blank(),
      axis.text        = element_text(size = 5),
      plot.background  = element_rect(fill = strip_col, colour = strip_col),
      plot.margin      = margin(6, 4, 4, 4)
    )
}
wet_colours <- c("#ffe0b2", "#ff8c00", "#bf360c")
p7a_past    <- make_period_plot("Past",    past_ids_vec,    "#2d4a6e", "Wet", wet_colours)
p7a_present <- make_period_plot("Present", present_ids_vec, "#2d5a2d", "Wet", wet_colours)

fig7a <- (p7a_past / p7a_present) +
  plot_annotation(
    title    = "Individual seasonal home ranges vs migratory wildebeest",
    subtitle = "Blue outline = Wet KDE (95%)  |  Red outline = Dry KDE (95%)  |  Orange = wet-season wildebeest density",
    theme    = theme(plot.title    = element_text(face = "bold", size = 12),
                     plot.subtitle = element_text(size = 9, colour = "grey40")))

ggsave("figures_final/WBV_ind_seasonal_vs_wildebeest_v9.png", fig7a,
       width = 12, height = 10, dpi = 300, bg = "white")
print(fig7a)

dry_colours <- c("#e8f5e9", "#4caf50", "#1b5e20")
p7b_past    <- make_period_plot("Past",    past_ids_vec,    "#2d4a6e", "Dry", dry_colours)
p7b_present <- make_period_plot("Present", present_ids_vec, "#2d5a2d", "Dry", dry_colours)

fig7b <- (p7b_past / p7b_present) +
  plot_annotation(
    title    = "Individual seasonal home ranges vs migratory wildebeest",
    subtitle = "Blue outline = Wet KDE (95%)  |  Red outline = Dry KDE (95%)  |  Green = dry-season wildebeest density",
    theme    = theme(plot.title    = element_text(face = "bold", size = 12),
                     plot.subtitle = element_text(size = 9, colour = "grey40")))

ggsave("figures_final/WBV_ind_seasonal_vs_wildebeest_dry_v1.png", fig7b,
       width = 12, height = 10, dpi = 300, bg = "white")
print(fig7b)


Figure 8 — Seasonal centroid displacement map

centroid_dist_plot <- centroid_dist %>%
  mutate(period = factor(period, c("Past", "Present")))

fig8 <- ggplot() +
  geom_sf(data = world,     fill = "grey97", colour = "grey80", linewidth = 0.2) +
  geom_sf(data = other_pas, fill = "#e8f4e8", colour = "grey70", linewidth = 0.25) +
  geom_sf(data = mara, fill = "#e8f4e8", colour = "#3a7d44", linewidth = 0.4) +
  geom_sf(data = snp,  fill = "#e8f4e8", colour = "#3a7d44", linewidth = 0.5) +
  geom_sf(data = gsme, fill = NA, colour = "grey40", linewidth = 0.5, linetype = "dashed") +
  geom_segment(
    data = centroid_dist_plot,
    aes(x = wet_lon, y = wet_lat, xend = dry_lon, yend = dry_lat, colour = period),
    arrow = arrow(length = unit(0.18, "cm"), type = "closed"),
    linewidth = 0.7, alpha = 0.85) +
  geom_point(data = centroid_dist_plot,
             aes(x = wet_lon, y = wet_lat, colour = period),
             shape = 21, fill = "white", size = 3, stroke = 1.2) +
  geom_point(data = centroid_dist_plot,
             aes(x = dry_lon, y = dry_lat, colour = period),
             shape = 22, fill = "white", size = 3, stroke = 1.2) +
  geom_hline(yintercept = -1.5, colour = "grey60", linewidth = 0.2, linetype = "longdash") +
  scale_colour_manual(values = c(Past = "#2d4a6e", Present = "#b2182b"), guide = "none") +
  coord_sf(xlim = c(33.8, 36.2), ylim = c(-3.8, 0.8), expand = FALSE) +
  facet_wrap(~ period) +
  labs(subtitle = "Circle = wet centroid  |  Square = dry centroid", x = NULL, y = NULL) +
  theme_bw(base_size = 12) +
  theme(strip.background = element_rect(fill = "grey88"),
        strip.text       = element_text(face = "bold", size = 12),
        panel.grid       = element_blank(),
        plot.subtitle    = element_text(size = 9, colour = "grey40"))

ggsave("figures_final/WBV_seasonal_centroid_map_v2.png", fig8,
       width = 9, height = 6, dpi = 300, bg = "white")
print(fig8)


Figure 9 — Net squared displacement trajectories

vul_nsd_plot <- vul_nsd %>%
  mutate(id_clean = gsub("^[0-9]+ - ", "", individual.local.identifier),
         period   = factor(period, c("Past", "Present")))

fig9 <- ggplot(
  vul_nsd_plot,
  aes(x = day_of_track, y = nsd_km2, colour = season,
      group = interaction(individual.local.identifier, bout_id))
) +
  geom_line(linewidth = 0.4, alpha = 0.8) +
  scale_colour_manual(values = c(Dry = "#E07B54", Wet = "#5B8DB8"), name = "Season") +
  scale_y_continuous(labels = scales::comma) +
  facet_wrap(~ id_clean, scales = "free", ncol = 3) +
  labs(x = "Days since first fix",
       y = expression("Net squared displacement (km"^2*")")) +
  theme_classic(base_size = 10) +
  theme(strip.background = element_rect(fill = "grey20"),
        strip.text       = element_text(colour = "white", size = 8),
        legend.position  = "bottom",
        panel.spacing    = unit(0.3, "lines"))

ggsave("figures_final/WBV_nsd_trajectories_v3.png", fig9,
       width = 14, height = 16, dpi = 300, bg = "white")
print(fig9)


Figure 10 — Southern zone use by season and period

zone_plot <- zone_pct %>%
  mutate(period = factor(period, c("Past", "Present")),
         season = factor(season, c("Wet", "Dry")))

fig10 <- ggplot(zone_plot, aes(x = season, y = pct_south, fill = season)) +
  geom_boxplot(alpha = 0.7, outlier.shape = 21, width = 0.5) +
  geom_jitter(aes(colour = season), width = 0.12, size = 2, alpha = 0.8) +
  scale_fill_manual(values  = c(Wet = "#5B8DB8", Dry = "#E07B54"), guide = "none") +
  scale_colour_manual(values = c(Wet = "#2166ac", Dry = "#b2182b"), guide = "none") +
  facet_wrap(~ period) +
  labs(x = NULL, y = "Fixes in southern zone (%)") +
  theme_classic(base_size = 13) +
  theme(strip.background = element_rect(fill = "grey88"),
        strip.text       = element_text(face = "bold", size = 12),
        panel.grid.minor = element_blank())

ggsave("figures_final/WBV_zone_season.png", fig10,
       width = 7, height = 5, dpi = 300, bg = "white")
print(fig10)


Session info

sessionInfo()
## R version 4.6.0 (2026-04-24)
## Platform: aarch64-apple-darwin23
## Running under: macOS Sequoia 15.7.4
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib 
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
## 
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
## 
## time zone: Europe/London
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] ctmm_1.3.0          units_1.0-1         smoothr_1.3.0      
##  [4] ggnewscale_0.5.2    rnaturalearth_1.2.0 patchwork_1.3.2    
##  [7] adehabitatHR_0.4.22 adehabitatLT_0.3.29 adehabitatMA_0.3.17
## [10] ade4_1.7-24         sp_2.2-1            MASS_7.3-65        
## [13] terra_1.9-27        sf_1.1-1            lubridate_1.9.5    
## [16] forcats_1.0.1       stringr_1.6.0       dplyr_1.2.1        
## [19] purrr_1.2.2         readr_2.2.0         tidyr_1.3.2        
## [22] tibble_3.3.1        ggplot2_4.0.3       tidyverse_2.0.0    
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6            raster_3.6-32           xfun_0.58              
##  [4] bslib_0.11.0            lattice_0.22-9          tzdb_0.5.0             
##  [7] vctrs_0.7.3             tools_4.6.0             generics_0.1.4         
## [10] proxy_0.4-29            pkgconfig_2.0.3         KernSmooth_2.23-26     
## [13] RColorBrewer_1.1-3      S7_0.2.2                lifecycle_1.0.5        
## [16] compiler_4.6.0          farver_2.1.2            textshaping_1.0.5      
## [19] codetools_0.2-20        htmltools_0.5.9         class_7.3-23           
## [22] sass_0.4.10             yaml_2.3.12             pillar_1.11.1          
## [25] jquerylib_0.1.4         classInt_0.4-11         cachem_1.1.0           
## [28] wk_0.9.5                rnaturalearthdata_1.0.0 tidyselect_1.2.1       
## [31] digest_0.6.39           stringi_1.8.7           labeling_0.4.3         
## [34] fastmap_1.2.0           grid_4.6.0              cli_3.6.6              
## [37] magrittr_2.0.5          e1071_1.7-17            withr_3.0.2            
## [40] scales_1.4.0            timechange_0.4.0        rmarkdown_2.31         
## [43] otel_0.2.0              ragg_1.5.2              hms_1.1.4              
## [46] evaluate_1.0.5          knitr_1.51              s2_1.1.11              
## [49] rlang_1.3.0             Rcpp_1.1.1-1.1          glue_1.8.1             
## [52] DBI_1.3.0               rstudioapi_0.18.0       jsonlite_2.0.0         
## [55] R6_2.6.1                systemfonts_1.3.2