Aquí comparto código y algunas recomendaciones para crear figuras espaciales de puntos y polígonos usando R y los paquetes ggplot y sf.

En este ejemplo, supongamos que queremos crear un mapa con división política y datos puntuales sobre observaciones para dos especies de animales. Sin necesidad de descargar Shapefiles o leer archivos externos, vamos a usar datos proporcionados por el paquete rnaturalearth (una librería que almacena e interactúa con datos de Natural Earth, un conjunto flexible de capas espaciales de libre uso), y vamos a generar puntos aleatorios como si fueran registros puntuales para las dos especies de animales.

Para esta demostración elegí Senegal. Podemos generar un objecto sf (simple features) para el continente africano, y filtrar este mismo usando nombres de países. Despueś, la función st_sample sirve para generar puntos aleatorios para dos especies hipotéticas dentro del polígono de Senegal.

# cargar librerías (instalar primero si hace falta)
library(rnaturalearth)
library(sf)
library(dplyr)
library(purrr)
library(ggplot2)
library(pointdensityP)

# mapa de África
Africa <- ne_countries(continent = "Africa", returnclass = "sf", scale = "medium")
# filtrar por país
Senegal <- Africa %>% filter(sovereignt=="Senegal")

# generar los puntos aleatorios
spA <- st_sample(Senegal,189) %>% st_sf() %>% mutate(sp="spA")
spB <- st_sample(Senegal,103) %>% st_sf() %>% mutate(sp="spB")
pts <- rbind(spA,spB)

Estos dos objetos ya se pueden dibujar con ggplot. El mapa no se ve mal, y aún no hemos cambiado los parámetros gráficos.

# figura 
antes <- 
  ggplot()+
  geom_sf(data=Senegal)+
  geom_sf(data=pts,aes(shape=sp,color=sp))+
  theme_bw()
antes
# exportar al disco duro (opcional)
#ggsave(antes,filename = "01_bfr.png",width = 6, height = 5,units = "in", dpi=300,device = "png")

Si cambiamos el tamaño de los puntos y los hacemos más grandes, algunos van a quedar encimados y no se van a ver bien. Para resolver ésto, tomé las sugerencias de éste tutorial de Simon Jackson para que los puntos tengan transparencia según la densidad local (inversamente proporcional). Si hay muchos puntos juntos, les damos transparencia para que se vean, y los que están solos espacialmente se mantienen opacos.

Para hacer ésto, primero obtenemos las coordenadas del objeto sf (están guardadas en una columna-lista) y calculamos la densidad espacial de los puntos con el paquete pointdensityP. Escogí un tamaño de gradilla chico, pero este parámetro se puede cambiar. Después unimos las densidades con el objeto sf y estandarizamos sus valores entre 0 y 1.

# densidad espacial
ptsMat <- st_coordinates(pts)
ptdens <- pointdensity(ptsMat,lat_col = "Y",
                       lon_col = "X", grid_size = 2,radius = 8)
ptsmerged <- bind_cols(pts,data.frame(ptsMat)) %>% left_join(ptdens,by=c("X"="lon","Y"="lat")) %>% 
  rename(ptdensities=count) %>%
  mutate(ptdensitiesSc=scales::rescale(ptdensities,c(0.01,1)))

OJO, que la función pointdensity acomoda los resultados por densidad (de menor a mayor) y por eso hay que hacer una unión (join) para que no se pierda el orden original de los datos.

Otra cosa que podemos hacer es darle más contexto geográfico a nuestro mapa. Para ésto, podemos dibujar los polígonos de los países aledaños a Senegal. La función st_touches nos dice cuáles polígonos (del objeto para todo el continente) comparten frontera con Senegal. De esta manera podemos asignar un nuevo objeto sf con estos polígonos vecinos a partir del objeto para todo África.

# contexto geográfico
adjSen <- st_touches(Senegal,Africa)
neighbours <- Africa %>% slice(pluck(adjSen,1))
limsSen <- st_buffer(Senegal,dist = 0.5) %>% st_bbox()

Finalmente, vamos a personalizar la forma, color, los bordes, y la transparencia de los puntos. Usamos st_bbox y st_buffer para acotar el mapa a Senegal usando los argumentos de límite dentro de coord_sf, escondemos la gradilla, y agregamos títulos informativos. El resultado final se ve bastante nítido.

nitido <- 
  ggplot()+
  geom_sf(data = neighbours)+
  geom_sf(data=Senegal,fill="white")+
  geom_sf(data=ptsmerged,aes(shape=sp,fill=sp,size=3,alpha=1/ptdensitiesSc),color="black")+
  scale_shape_manual(values = c(21,24),guide=FALSE)+
  scale_fill_manual(values = c("#ff8c42","#320d6d"),name="Especie")+
  scale_alpha_continuous(range = c(.6, 1),guide=FALSE)+
  scale_size_identity(guide = FALSE)+
  coord_sf(xlim = c(limsSen["xmin"], limsSen["xmax"]), 
           ylim = c(limsSen["ymin"], limsSen["ymax"]),
  )+
  labs(title="Registros en Senegal")+
  theme(plot.background = element_rect(color = "black",size=0.5)) +
  theme(panel.background = element_rect(fill = "#D3E0F8", color = "#D3E0F8"))+
  theme(
    panel.grid = element_line(colour = 'transparent'), 
    line = element_blank(), 
    rect = element_blank())
nitido
# exportar, opcional
#ggsave(nitido,filename = "02_aftr.png",width = 6, height = 5,units = "in", dpi=300,device = "png")

Para comparar los dos, esta secuencia de funciones usan la magia de purrr, fs, y magick para leer las imágenes que exportamos y animar una transición gradual entre las dos en formato .gif.

# para la animación
library(magick)
library(fs)
# lee todos los archivos png en el directorio de trabajo
dir_ls(glob = "*.png") %>% map(image_read) %>% 
  image_join() %>% image_morph(frames = 20) %>%
  image_animate(fps = 5) %>% 
  image_write("mapas.gif")

gif anim

Con algunos cambios, ya tenemos un mapa presentable. ¡Suerte!