Chapter 5 Making Pretty Maps

Now that we’ve learned how to do some spatial analysis, you likely want to make a nice looking map to put in a manuscript or presentation.

Note: This workshop is currently a work in progress. All of the mapping elements are present, but the presentation and setup portion are yet to be added. I appreciate your patience as I finish this up in my free time.

library(sf)
library(tidyverse)
library(terra)

5.1 USA Population

# Load the US population shapefile
usa <- st_read("Data/Visualization/US_states", quiet = T)
head(usa)
## Simple feature collection with 6 features and 5 fields
## Geometry type: MULTIPOLYGON
## Dimension:     XY
## Bounding box:  xmin: -124.3834 ymin: 30.24071 xmax: -71.78015 ymax: 42.04937
## Geodetic CRS:  WGS 84
##         STATE  POP2020  POP2010           DIVISION    REGION
## 1     Alabama  5024279  4779736 East South Central     South
## 2     Arizona  7151502  6392017           Mountain      West
## 3    Arkansas  3011524  2915918 West South Central     South
## 4  California 39538223 37253956            Pacific      West
## 5    Colorado  5773714  5029196           Mountain      West
## 6 Connecticut  3605944  3574097        New England Northeast
##                         geometry
## 1 MULTIPOLYGON (((-87.46201 3...
## 2 MULTIPOLYGON (((-114.6374 3...
## 3 MULTIPOLYGON (((-94.05103 3...
## 4 MULTIPOLYGON (((-120.006 42...
## 5 MULTIPOLYGON (((-102.0552 4...
## 6 MULTIPOLYGON (((-73.49902 4...
# plot to see what it looks like
ggplot(usa) +
  geom_sf()

# Fill the states based on their population
ggplot(usa) +
  geom_sf(aes(fill = POP2020))

# Add Canada and Mexico to the map
# Adds context for the US (isn't floating in some void but exists in the world)

library(maps)

can_mex <- maps::map("world", plot = F, fill = T) %>%
  st_as_sf() %>%
  st_transform(4326) %>%
  filter(ID %in% c("Canada", "Mexico")) 
ggplot(can_mex) +
  geom_sf()

ggplot(usa) +
  # make the outline around the states white
  geom_sf(aes(fill = POP2020), col = "white") +
  # remove the outline around Canada and Mexico and grey them out
  # (focuses the eye towards what's important about this map ie US population)
  geom_sf(data = can_mex, col = NA, fill = "grey80")

# Add North American lakes to the map

lakes <- maps::map("lakes", plot = F, fill = T) %>%
  st_as_sf() %>%
  st_transform(4326)
st_bbox(bind_rows(usa, can_mex))
##       xmin       ymin       xmax       ymax 
## -179.23109   18.86546  -67.00742   71.43979
na_lakes <- lakes %>%
  st_crop(st_bbox(bind_rows(usa, can_mex)))
ggplot(na_lakes) +
  geom_sf()

ggplot(usa) +
  geom_sf(aes(fill = POP2020), col = "white") +
  geom_sf(data = can_mex, col = NA, fill = "grey80") +
  geom_sf(data = na_lakes, fill = "white", col = NA) +
  theme_bw()

# Filter out Alaska and Hawaii to create just the contiguous USA polygons
contig_usa <- usa %>%
  filter(!STATE %in% c("Alaska", "Hawaii"))
ggplot(contig_usa) +
  geom_sf()

# Project the contiguous USA to Conus Albers (EPSG:6350)
# Standard projection for US maps
contig_usa <- st_transform(contig_usa, 6350) 
ggplot(contig_usa) +
  geom_sf()

# Project Canada, Mexico, and the lakes to Conus Albers and add to the map
can_mex <- st_transform(can_mex, 6350)
lakes <- st_transform(lakes, 6350)
ggplot(contig_usa) +
  geom_sf(aes(fill = POP2020), col = "white") +
  geom_sf(data = can_mex, col = NA, fill = "grey80") +
  geom_sf(data = na_lakes, fill = "white", col = NA) +
  theme_bw()

# Zoom the view window to just the contiguous USA
st_bbox(contig_usa)
##       xmin       ymin       xmax       ymax 
## -2353797.2   321803.8  2254131.0  3163215.2
us_plot <- ggplot(contig_usa) +
  geom_sf(aes(fill = POP2020), col = "white") +
  geom_sf(data = can_mex, col = NA, fill = "grey80") +
  geom_sf(data = na_lakes, fill = "white", col = NA) +
  theme_bw() +
  xlim(c(st_bbox(contig_usa)["xmin"], st_bbox(contig_usa)["xmax"])) +
  ylim(c(st_bbox(contig_usa)["ymin"] - 1e5, st_bbox(contig_usa)["ymax"] + 1e6)) 
us_plot

# Filter to create an Alaska and a Hawaii polygon
# We'll add these to the map as inset maps
alaska <- filter(usa, STATE == "Alaska")
alaska <- st_transform(alaska, 3338)

ak_plot <- ggplot(alaska) +
  geom_sf(aes(fill = POP2020), col = "white") +
  geom_sf(data = can_mex, col = NA, fill = "grey80") +
  geom_sf(data = na_lakes, fill = "white", col = NA) +
  theme_bw() +
  xlim(c(st_bbox(alaska)["xmin"], st_bbox(alaska)["xmax"])) +
  ylim(c(st_bbox(alaska)["ymin"], st_bbox(alaska)["ymax"])) +
  coord_sf(datum = 3338) 
ak_plot

hawaii <- usa %>%
  filter(STATE == "Hawaii") %>%
  st_transform(st_crs("ESRI:102007"))

hi_plot <- ggplot(hawaii) +
  geom_sf(aes(fill = POP2020), col = "white") +
  theme_bw() +
  xlim(c(st_bbox(hawaii)["xmin"], st_bbox(hawaii)["xmax"])) +
  ylim(c(st_bbox(hawaii)["ymin"], st_bbox(hawaii)["ymax"])) 
hi_plot

# Use the library 'cowplot' to add figures on top of other figures
library(cowplot)
# 'ggdraw' is cowplot's version of 'ggplot'
# 'draw_plot' is cowplot's version of 'geom_sf'

ggdraw() +
  draw_plot(us_plot) +
  # the x and y arguments tell cowplot where these figures should go on
  #   the map (based on, I beleive, the bottom left corner)
  #   The units are in pixels from 0 (bottom/left) to 1 (top/right)
  # The 'height' argument tells cowplot how big/small this figure should be
  # NOTE: these values are very subjective to your plotting window. If you 
  #   are saving this as a picture file, be sure to check frequently that it looks
  #   right, because it will likely look different than in your plotting window 
  #   in R studio
  draw_plot(ak_plot, x = 0.035, y = 0.75, width = 0.25, height = 0.25) +
  draw_plot(hi_plot, x = 0.035, y = 0.015, width = 0.25, height = 0.25)

# Right now this doesn't look good. We'll need to remove the legend from the 
# Alaska and Hawaii maps. But we also want to be sure that AK and HI's population
# is shown visually on the same scale as the contiguous US (right now they're)
# on their own scales, so the color shown is mis-representing the population
# Let's bin the US populations in a way that makes more sense to look at 
# 'BAMMtools' has a function for Jenks Breaks, which is a standard way to bin
#   continuous values
library(BAMMtools)
(pop_breaks <- getJenksBreaks(var = usa$POP2020, k = 6))
## [1]   576851  3605944  7705281 13002700 29145505 39538223
(pop_breaks <- signif(pop_breaks, 2))
## [1] 5.8e+05 3.6e+06 7.7e+06 1.3e+07 2.9e+07 4.0e+07
# Add a flag for each state to mark which population bin it should go in
usa <- usa %>%
  mutate(POP2020_BREAKS = case_when(
    POP2020 >= min(POP2020) & POP2020 < pop_breaks[2] ~ "1",
    POP2020 >= pop_breaks[2] & POP2020 < pop_breaks[3] ~ "2",
    POP2020 >= pop_breaks[3] & POP2020 < pop_breaks[4] ~ "3",
    POP2020 >= pop_breaks[4] & POP2020 < pop_breaks[5] ~ "4",
    POP2020 >= pop_breaks[5] & POP2020 <= max(POP2020) ~ "5")) 

# Now we've binned population into categories
ggplot(usa) +
  geom_sf(aes(fill = POP2020_BREAKS)) 

# The base color palette can be a little ugly
# hcl.pals() gives you a list of pre-made palettes, but you could also
# make your own
hcl.pals()
##   [1] "Pastel 1"      "Dark 2"        "Dark 3"        "Set 2"        
##   [5] "Set 3"         "Warm"          "Cold"          "Harmonic"     
##   [9] "Dynamic"       "Grays"         "Light Grays"   "Blues 2"      
##  [13] "Blues 3"       "Purples 2"     "Purples 3"     "Reds 2"       
##  [17] "Reds 3"        "Greens 2"      "Greens 3"      "Oslo"         
##  [21] "Purple-Blue"   "Red-Purple"    "Red-Blue"      "Purple-Orange"
##  [25] "Purple-Yellow" "Blue-Yellow"   "Green-Yellow"  "Red-Yellow"   
##  [29] "Heat"          "Heat 2"        "Terrain"       "Terrain 2"    
##  [33] "Viridis"       "Plasma"        "Inferno"       "Rocket"       
##  [37] "Mako"          "Dark Mint"     "Mint"          "BluGrn"       
##  [41] "Teal"          "TealGrn"       "Emrld"         "BluYl"        
##  [45] "ag_GrnYl"      "Peach"         "PinkYl"        "Burg"         
##  [49] "BurgYl"        "RedOr"         "OrYel"         "Purp"         
##  [53] "PurpOr"        "Sunset"        "Magenta"       "SunsetDark"   
##  [57] "ag_Sunset"     "BrwnYl"        "YlOrRd"        "YlOrBr"       
##  [61] "OrRd"          "Oranges"       "YlGn"          "YlGnBu"       
##  [65] "Reds"          "RdPu"          "PuRd"          "Purples"      
##  [69] "PuBuGn"        "PuBu"          "Greens"        "BuGn"         
##  [73] "GnBu"          "BuPu"          "Blues"         "Lajolla"      
##  [77] "Turku"         "Hawaii"        "Batlow"        "Blue-Red"     
##  [81] "Blue-Red 2"    "Blue-Red 3"    "Red-Green"     "Purple-Green" 
##  [85] "Purple-Brown"  "Green-Brown"   "Blue-Yellow 2" "Blue-Yellow 3"
##  [89] "Green-Orange"  "Cyan-Magenta"  "Tropic"        "Broc"         
##  [93] "Cork"          "Vik"           "Berlin"        "Lisbon"       
##  [97] "Tofino"        "ArmyRose"      "Earth"         "Fall"         
## [101] "Geyser"        "TealRose"      "Temps"         "PuOr"         
## [105] "RdBu"          "RdGy"          "PiYG"          "PRGn"         
## [109] "BrBG"          "RdYlBu"        "RdYlGn"        "Spectral"     
## [113] "Zissou 1"      "Cividis"       "Roma"
# We'll use the palette 'Emrld' (but you could pick any you like)
pop_cols <- hcl.colors(5, palette = "Emrld")
ggplot(usa) +
  geom_sf(aes(fill = POP2020_BREAKS)) +
  scale_fill_manual(values = pop_cols,
                    labels = c(paste(pop_breaks[1], "-", pop_breaks[2]),
                               paste(pop_breaks[2], "-", pop_breaks[3]),
                               paste(pop_breaks[3], "-", pop_breaks[4]),
                               paste(pop_breaks[4], "-", pop_breaks[5]),
                               paste(pop_breaks[5], "-", pop_breaks[6])))

# Let's make the legend label into an object so we don't have to copy-paste
# this everytime we need to plot
pop_breaks_labs <- c(paste(pop_breaks[1], "-", pop_breaks[2]),
                     paste(pop_breaks[2], "-", pop_breaks[3]),
                     paste(pop_breaks[3], "-", pop_breaks[4]),
                     paste(pop_breaks[4], "-", pop_breaks[5]),
                     paste(pop_breaks[5], "-", pop_breaks[6]))
# Let's make the map of the contiguous US as before
# (we need to re-make teh contig_usa object since it doesn't have the population
# breaks attributes)
contig_usa <- usa %>%
  filter(!STATE %in% c("Alaska", "Hawaii")) %>%
  st_transform(6350)
us_pop_plot <- ggplot() +
  geom_sf(aes(fill = POP2020_BREAKS), col = "white",
          data = contig_usa) +
  geom_sf(data = can_mex, col = NA, fill = "grey80") +
  geom_sf(data = na_lakes, fill = "white", col = NA) +
  theme_bw() +
  xlim(c(st_bbox(contig_usa)["xmin"], st_bbox(contig_usa)["xmax"])) +
  ylim(c(st_bbox(contig_usa)["ymin"] - 1e5, st_bbox(contig_usa)["ymax"] + 1e6)) +
  scale_fill_manual(values = pop_cols,
                    labels = pop_breaks_labs) 
us_pop_plot

# Let's also remake the AK and HI inset maps
# (AK and HI need to be re-made because they don't have the population breaks
# attributes)
alaska <- usa %>%
  filter(STATE == "Alaska") %>%
  bind_rows(data.frame(POP2020_BREAKS = as.character(1:5))) %>%
  st_transform(3338)
ak_pop_plot <-  ggplot() +
  geom_sf(aes(fill = POP2020_BREAKS), col = "white",
          data = alaska) +
  geom_sf(data = can_mex, col = NA, fill = "grey80") +
  geom_sf(data = na_lakes, fill = "white", col = NA) +
  theme_bw() +
  xlim(c(st_bbox(alaska)["xmin"], st_bbox(alaska)["xmax"])) +
  ylim(c(st_bbox(alaska)["ymin"], st_bbox(alaska)["ymax"])) +
  coord_sf(datum = 3338)  +
  scale_fill_manual(values = pop_cols,
                    labels = pop_breaks_labs)
ak_pop_plot

hawaii <- usa %>%
  filter(STATE == "Hawaii") %>%
  st_transform(st_crs("ESRI:102007")) %>%
  bind_rows(data.frame(POP2020_BREAKS = as.character(1:5)))
hi_pop_plot <-  ggplot() +
  geom_sf(aes(fill = POP2020_BREAKS), col = "white",
          data = hawaii) +
  theme_bw() +
  xlim(c(st_bbox(hawaii)["xmin"], st_bbox(hawaii)["xmax"])) +
  ylim(c(st_bbox(hawaii)["ymin"], st_bbox(hawaii)["ymax"]))   +
  scale_fill_manual(values = pop_cols,
                    labels = pop_breaks_labs)
hi_pop_plot

# Now add everything together
us_pop_map <- ggdraw() +
  draw_plot(us_pop_plot + labs(fill = "Population")) +
  # legend.position = 'none' removes the legend
  draw_plot(ak_pop_plot + theme(legend.position = 'none',
                                axis.text = element_blank(),
                                axis.ticks = element_blank(),
                                plot.background = element_blank()), 
           x = 0.0375, y = 0.707, width = 0.25, height = 0.25) +
  draw_plot(hi_pop_plot + theme(legend.position = 'none',
                                axis.text = element_blank(),
                                axis.ticks = element_blank(),
                                plot.background = element_blank()),
            x = 0.0375, y = 0.0351, width = 0.2, height = 0.25)
us_pop_map

# Reminder that the xy and width/height for cowplot plots are very subjective,
# So you'll have to check and adjust for your viewing window

5.1.1 Change in Population

For the last plot we visually showed the current US population with colors We can also visually convey information through points and their size For this plot, let’s combine the two to create a map showwing the current US population and it’s change from the last census count

# Check the dataframe
head(usa)
## Simple feature collection with 6 features and 6 fields
## Geometry type: MULTIPOLYGON
## Dimension:     XY
## Bounding box:  xmin: -124.3834 ymin: 30.24071 xmax: -71.78015 ymax: 42.04937
## Geodetic CRS:  WGS 84
##         STATE  POP2020  POP2010           DIVISION    REGION
## 1     Alabama  5024279  4779736 East South Central     South
## 2     Arizona  7151502  6392017           Mountain      West
## 3    Arkansas  3011524  2915918 West South Central     South
## 4  California 39538223 37253956            Pacific      West
## 5    Colorado  5773714  5029196           Mountain      West
## 6 Connecticut  3605944  3574097        New England Northeast
##                         geometry POP2020_BREAKS
## 1 MULTIPOLYGON (((-87.46201 3...              2
## 2 MULTIPOLYGON (((-114.6374 3...              2
## 3 MULTIPOLYGON (((-94.05103 3...              1
## 4 MULTIPOLYGON (((-120.006 42...              5
## 5 MULTIPOLYGON (((-102.0552 4...              2
## 6 MULTIPOLYGON (((-73.49902 4...              2
# we have POP2020 and POP2010, so we can calculate the percent change in population
usa <- usa %>%
  mutate(pop_perc_change = ((POP2020 - POP2010)/POP2010) * 100)
summary(usa$pop_perc_change)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  -3.199   2.902   5.654   7.018  10.410  18.370
# Plot just to check what it looks like. Reminder that our final plot will 
# have the color-coded current population (the previous map) and the percent
# change in population represented by different size points
ggplot(usa) +
  geom_sf(aes(fill = pop_perc_change))

# Find the centeroid of each state for the point to fall on
state_center <- st_centroid(usa)
ggplot(usa) +
  geom_sf(data = usa) +
  geom_sf(data = state_center)

# Show the percent change in population based on the point size
ggplot(usa) +
  geom_sf(data = usa) +
  geom_sf(data = state_center,
          aes(size = pop_perc_change))

# Put the percent change into bins 
(pop_change_breaks <- getJenksBreaks(var = usa$pop_perc_change, k = 4))
## [1] -3.199039  5.654072 11.881774 18.370193
# There are some negative percent change values (ie the population decreased)
# So let's filter out the negative changes
# (If a state had negative population growth, we will just mark that it was 
# less than 0)
pop_change_grtr_0 <- filter(usa, pop_perc_change > 0)$pop_perc_change
summary(pop_change_grtr_0)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.8911  3.2743  6.5566  7.5301 10.5802 18.3702
(pop_change_breaks <- getJenksBreaks(var = pop_change_grtr_0, k = 4))
## [1]  0.8910502  6.1316092 11.8817738 18.3701927
# Mark which bin the state should fall in based on its change in population
state_center <- state_center %>%
  mutate(pop_perc_change_breaks = case_when(
    pop_perc_change <= 0 ~ "1",
    pop_perc_change >= pop_change_breaks[1] & pop_perc_change < pop_change_breaks[2] ~ "2",
    pop_perc_change >= pop_change_breaks[2] & pop_perc_change < pop_change_breaks[3] ~ "3",
    pop_perc_change >= pop_change_breaks[3] & pop_perc_change <= pop_change_breaks[4] ~ "4"))
head(state_center)
## Simple feature collection with 6 features and 8 fields
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: -119.6015 ymin: 32.80316 xmax: -72.72598 ymax: 41.62566
## Geodetic CRS:  WGS 84
##         STATE  POP2020  POP2010           DIVISION    REGION
## 1     Alabama  5024279  4779736 East South Central     South
## 2     Arizona  7151502  6392017           Mountain      West
## 3    Arkansas  3011524  2915918 West South Central     South
## 4  California 39538223 37253956            Pacific      West
## 5    Colorado  5773714  5029196           Mountain      West
## 6 Connecticut  3605944  3574097        New England Northeast
##                     geometry POP2020_BREAKS pop_perc_change
## 1 POINT (-86.83042 32.80316)              2       5.1162449
## 2  POINT (-111.6679 34.3006)              2      11.8817738
## 3 POINT (-92.44013 34.90418)              1       3.2787616
## 4 POINT (-119.6015 37.26901)              5       6.1316092
## 5 POINT (-105.5525 38.99797)              2      14.8039170
## 6 POINT (-72.72598 41.62566)              2       0.8910502
##   pop_perc_change_breaks
## 1                      2
## 2                      4
## 3                      2
## 4                      3
## 5                      4
## 6                      2
# Check map
ggplot(usa) +
  geom_sf(data = usa) +
  geom_sf(data = state_center,
          aes(size = pop_perc_change_breaks))

# Let's manually adjust the sizes to be more clear how the sizes differentiate
ggplot(usa) +
  geom_sf(data = usa) +
  geom_sf(data = state_center,
          aes(size = pop_perc_change_breaks)) +
  scale_size_manual(values = seq(1, 8, by = 2))

# Now let's set up state_center like we did with the states themselves
# ie filter out AK and HI to make the contiguous USA and AK & HI into their own
# objects
contig_state_center <- state_center %>%
  filter(!STATE %in% c("Alaska", "Hawaii")) %>%
  st_transform(6350)

us_pop_plot +
  geom_sf(aes(size = pop_perc_change_breaks),
          data = contig_state_center) +
  scale_size_manual(values = seq(1, 8, by = 2)) 

# Make labels for the percent change legend
(pop_change_breaks_labs <- signif(pop_change_breaks, 1))
## [1]  0.9  6.0 10.0 20.0
min(state_center$pop_perc_change)
## [1] -3.199039
(pop_change_labs <- c(paste0("-3% - ", pop_change_breaks_labs[1], "%"),
                      paste0(pop_change_breaks_labs[1], "% - ", pop_change_breaks_labs[2], "%"),
                      paste0(pop_change_breaks_labs[2], "% - ", pop_change_breaks_labs[3], "%"),
                      paste0(pop_change_breaks_labs[3], "% - ", pop_change_breaks_labs[4], "%")))
## [1] "-3% - 0.9%" "0.9% - 6%"  "6% - 10%"   "10% - 20%"
us_pop_change_plot <- us_pop_plot +
  geom_sf(aes(size = pop_perc_change_breaks),
          data = contig_state_center) +
  scale_size_manual(values = seq(0.5, 6, length.out = 4),
                    labels = pop_change_labs) 
us_pop_change_plot

# Repeat for AK & HI
ak_center <- state_center %>%
  filter(STATE == "Alaska") %>%
  bind_rows(data.frame(pop_perc_change_breaks = as.character(1:4))) %>%
  st_transform(3338)

ak_pop_change_plot <- ak_pop_plot +
  geom_sf(aes(size = pop_perc_change_breaks),
          data = ak_center) +
  scale_size_manual(values = seq(1, 8, by = 2)) 
ak_pop_change_plot

hi_center <- state_center %>%
 filter(STATE == "Hawaii") %>%
  bind_rows(data.frame(pop_perc_change_breaks = as.character(1:4))) %>%
  st_transform(st_crs("ESRI:102007"))

hi_pop_change_plot <- hi_pop_plot +
  geom_sf(aes(size = pop_perc_change_breaks),
          data = hi_center) +
  scale_size_manual(values = seq(1, 8, by = 2)) 

hi_pop_change_plot

# Add everything together
us_pop_change_map <- ggdraw() +
  draw_plot(us_pop_change_plot + labs(fill = "Population",
                                      size = "Percent Population Change from 2010")) +
  draw_plot(ak_pop_change_plot + theme(legend.position = 'none',
                            axis.text = element_blank(),
                            axis.ticks = element_blank(),
                            plot.background = element_blank()), 
            x = 0.03, y = 0.65, width = 0.25, height = 0.25) +
  draw_plot(hi_pop_change_plot + theme(legend.position = 'none',
                            axis.text = element_blank(),
                            axis.ticks = element_blank(),
                            plot.background = element_blank()),
             x = 0.03, y = 0.089, width = 0.2, height = 0.25)
us_pop_change_map

##           used  (Mb) gc trigger  (Mb)  max used  (Mb)
## Ncells 2289172 122.3    5164370 275.9   5164370 275.9
## Vcells 4245892  32.4  120616660 920.3 123932011 945.6

5.2 Yellowstone

Let’s make a map of species and their general distribution within Yellowstone

ynp <- st_read("Data/Visualization/Yellowstone", quiet = T) %>%
  # be sure it's projected to WGS 84 (EPSG:4326)
  st_transform(4326)

# Check that everything looks right
head(ynp)
## Simple feature collection with 1 feature and 13 fields
## Geometry type: POLYGON
## Dimension:     XY
## Bounding box:  xmin: -111.1543 ymin: 44.13245 xmax: -109.8339 ymax: 45.10785
## Geodetic CRS:  WGS 84
##         AREA PERIMETER AB67_ AB67_ID TYPE1           ADMN_TYPE1 TYPE2
## 1 8842796032  447129.5     2     649     1 Park, monument, etc.     0
##   ADMN_TYPE2 TYPE3 ADMN_TYPE3 STATE_FIPS STATE_NAME SUB_REGION
## 1       <NA>     0       <NA>         56    Wyoming        Mtn
##                         geometry
## 1 POLYGON ((-110.0112 45.0478...
plot(st_geometry(ynp))

# Load the GPS locations of four different species of wildlife in Yellowstone
ynp_animals <- read.csv("Data/Visualization/yellowstone_animals.csv")

ggplot() +
  geom_sf() +
  geom_sf(data = ynp) +
  # add the GPS points to the maps, color based on the species
  geom_point(aes(x = Longitude, y = Latitude, col = Species),
             data = ynp_animals)

# Load the ranges of the wildlife
range <- st_read("Data/Visualization/Species_Range", quiet = T)

# The attribute 'level' indicates more (smaller percentages) or less (higher percentages) intensely used spaces
head(range)
## Simple feature collection with 6 features and 2 fields
## Geometry type: MULTIPOLYGON
## Dimension:     XY
## Bounding box:  xmin: -111.1123 ymin: 43.30636 xmax: -109.8502 ymax: 45.31349
## Geodetic CRS:  WGS 84
##          Use    Species                       geometry
## 1 0.00000000 Black Bear MULTIPOLYGON (((-110.5497 4...
## 2 0.04545455 Black Bear MULTIPOLYGON (((-110.504 45...
## 3 0.09090910 Black Bear MULTIPOLYGON (((-110.4584 4...
## 4 0.13636364 Black Bear MULTIPOLYGON (((-110.504 45...
## 5 0.18181819 Black Bear MULTIPOLYGON (((-110.4584 4...
## 6 0.22727274 Black Bear MULTIPOLYGON (((-110.5192 4...
plot(st_geometry(range))

# Add the ranges to the yellowstone
ynp_plot <- ggplot() +
  # change the linetype to dotted
  geom_sf(data = ynp, linetype = 2, linewidth = 1) +
  # add the ranges to the map, fill color based on the intesnity of use
  geom_sf(aes(fill = Use), col = NA, alpha = 0.5,
          data = range) +
  geom_point(aes(x = Longitude, y = Latitude, col = Species),
             data = ynp_animals, alpha = 0.5) +
  # Give an outline to the points
  # (Note: this can also be done by changing the shape type to '21', 
  # which gives a point a fill and a color. 
  # The reason why I don't have that here is because the fill colors for the 
  # species ranges and points would be the same, and we want to give them 
  # different color palettes)
  geom_point(aes(x = Longitude, y = Latitude), data = ynp_animals, 
             shape = 21, col = "grey30", alpha = 0.5)
ynp_plot

# Right now all the GPS points and ranges are on top of each other in one plot
# So it's difficult to see what's happening
ynp_plot <- ynp_plot +
  # facet_wrap creates different facets of the figure (in this case based on the species)
  facet_wrap(~Species, nrow = 2, ncol = 2)

ynp_plot

This plot is more clear. Let’s change the colors of the species points

# We'll use the palette "Green-Brown" for the animal points
animal_cols <- hcl.colors(n = 4, palette = "Green-Brown")
names(animal_cols) <- unique(ynp_animals$Species)
animal_cols
##          Elk   Black Bear Grizzly Bear         Wolf 
##    "#004B40"    "#89D9CF"    "#E4C6A1"    "#533600"
ynp_plot <- ynp_plot +
  # We could pick another palette for the ranges, or we could use a built-in viridis palette
  scale_fill_viridis_c(option = "B") +
  scale_color_manual(values = animal_cols) +
  theme_bw()
ynp_plot

# Finally, let's adjust the x-coordinate labels, since they're printing on top of each other
x_min <- signif(min(ynp_animals$Longitude), 3)
x_max <- signif(max(ynp_animals$Longitude), 3)
seq(x_min, x_max, by = 1)
## [1] -111 -110 -109
ynp_plot <- ynp_plot +
  scale_x_continuous(breaks = seq(x_min, x_max, by = 1)) +
  labs(x = "", y = "", fill = "Intensity of Use") 

ynp_plot 

5.2.1 Hillshade

This plot is fine. But if we wanted, we could make the background a bit more interesting. Let’s add a hillshade, or an elevation raster that adds a shadow effect to look 3D

# Load the DEM (Digital Elevation Model)
# We will derive everything from this raster to create the hillshade raster
ynp_dem <- rast("Data/Visualization/Yellowstone_DEM.tif")

# Check
ynp_dem
## class       : SpatRaster 
## dimensions  : 783, 781, 1  (nrow, ncol, nlyr)
## resolution  : 0.002777778, 0.002777778  (x, y)
## extent      : -111.3775, -109.2081, 43.30056, 45.47556  (xmin, xmax, ymin, ymax)
## coord. ref. : lon/lat NAD83 (EPSG:4269) 
## source      : Yellowstone_DEM.tif 
## name        :      DEM 
## min value   : 1291.389 
## max value   : 4047.209
plot(ynp_dem)
plot(ynp$geometry, add = T)

# To create a hillshade, we need both slope and aspect
# which can be derived using the function 'terrain'
ynp_slope <- terrain(ynp_dem, v = "slope", unit = "radians")
ynp_aspect <- terrain(ynp_dem, v = "aspect", unit = "radians")

# Now derive hillshade
ynp_hillshade <- shade(ynp_slope, ynp_aspect)
ynp_hillshade
## class       : SpatRaster 
## dimensions  : 783, 781, 1  (nrow, ncol, nlyr)
## resolution  : 0.002777778, 0.002777778  (x, y)
## extent      : -111.3775, -109.2081, 43.30056, 45.47556  (xmin, xmax, ymin, ymax)
## coord. ref. : lon/lat NAD83 (EPSG:4269) 
## source(s)   : memory
## name        :   hillshade 
## min value   : -0.04331134 
## max value   :  0.99961031
plot(ynp_hillshade)
plot(ynp$geometry, add = T)

# We need to turn the raster into a dataframe so that ggplot can handle it
names(ynp_hillshade) <- "hillshade"
ynp_hill_df <- as.data.frame(ynp_hillshade, xy = T)
head(ynp_hill_df)
##             x        y hillshade
## 783 -111.3733 45.47139 0.7732976
## 784 -111.3706 45.47139 0.7462534
## 785 -111.3678 45.47139 0.7070008
## 786 -111.3650 45.47139 0.6975097
## 787 -111.3622 45.47139 0.7025581
## 788 -111.3594 45.47139 0.7414129
# Check
ggplot() +
  geom_raster(data = ynp_hill_df,
              aes(x = x, y = y, fill = hillshade))

When we plot the hillshade raster with other spatial objects, ggplot is able to tell that the raster is a spatial object too, and plots it with the correct projection

ggplot() +
  geom_raster(data = ynp_hill_df,
              aes(x = x, y = y, fill = hillshade)) +
  geom_sf(data = ynp, linetype = 2, linewidth = 1, fill = NA) +
  scale_x_continuous(breaks = seq(x_min, x_max, by = 1))

Right now we can’t see anything, so let’s adjust the hillshade colors

ynp_plot <- ggplot() +
  geom_raster(data = ynp_hill_df,
              aes(x = x, y = y, fill = hillshade),
              show.legend = FALSE) + # remove hillshade legend
  # Let's change the hillshade to a greyscale gradient
  scale_fill_gradient2("hillshade_lower_res",
                       low = "grey0", mid = "grey0", high = "grey100",
                       na.value = "transparent") +
  geom_sf(data = ynp, linetype = 2, linewidth = 1, fill = NA, col = "white") +
  scale_x_continuous(breaks = seq(x_min, x_max, by = 1))
ynp_plot

Neat!

Now let’s add the animal data back to the map

 ynp_plot +
  geom_sf(aes(fill = Use), col = NA, alpha = 0.5,
          data = range) +
  geom_point(aes(x = Longitude, y = Latitude, col = Species),
             data = ynp_animals, alpha = 0.5) +
  geom_point(aes(x = Longitude, y = Latitude), data = ynp_animals, 
             shape = 21, col = "grey30", alpha = 0.5) +
  scale_fill_viridis_c(option = "B") +
  scale_color_manual(values = animal_cols) +
  facet_wrap(~Species, nrow = 2, ncol = 2) +
  theme_bw()

Oops, ggplot as a default can’t handle more than one palette for ‘fill’ so it changed the hillshade to the same gradient as the animal ranges.

To fix this, we can use the package ggnewscale

library(ggnewscale)

 ynp_plot <- ynp_plot +
  new_scale("fill") + # let's you add a new fill scale
   geom_sf(aes(fill = Use), col = NA, alpha = 0.5,
          data = range) +
  geom_point(aes(x = Longitude, y = Latitude, col = Species),
             data = ynp_animals, alpha = 0.5) +
  geom_point(aes(x = Longitude, y = Latitude), data = ynp_animals, 
             shape = 21, col = "grey30", alpha = 0.5) +
  scale_fill_viridis_c(option = "B") +
  scale_color_manual(values = animal_cols) +
  facet_wrap(~Species, nrow = 2, ncol = 2) +
  theme_bw() +
  labs(x = "", y = "", fill = "Intensity of Use") 
 ynp_plot

Yay!

5.2.2 Inset

Let’s add an inset map so we know where in the US Yellowstone is located

# Let's make a map of the contiguous US with Yellowstone
us_inset <- ggplot() +
  geom_sf(data = contig_usa, col = "white", fill = "grey") +
  geom_sf(data = ynp) +
  # this theme option voids out all backgrounds and labels
  theme_void()
us_inset

# Cowplot both plots together
check_ynp <- ggdraw() +
  draw_plot(ynp_plot)  +
  draw_plot(us_inset, 
            x = 0.675, y = 0.05, width = 0.2, height = 0.2)
check_ynp

Right now, it’s not immediately clear where Yellowstone is so let’s make the outline red

This works fine, but maybe we want to make a red box around the general area where Yellowstone is located

# Now we have a map of yellowstone and where it's located
# (We could also adjust the inset map's location if needed)
ynp_map <- ggdraw() +
  draw_plot(ynp_plot)  +
  draw_plot(us_inset, 
            x = 0.675, y = 0.05, width = 0.2, height = 0.2)
ynp_map

##            used  (Mb) gc trigger   (Mb)  max used   (Mb)
## Ncells  2314521 123.7    8141808  434.9  10224022  546.1
## Vcells 10390475  79.3  149906832 1143.7 156086283 1190.9

5.3 Landcover

Let’s make a landcover map of the ecorgions in the Four Corners and adjust colors that make sense to the landcover type

# Load the boundaries of different landcover types within the Four Corners
regions <- st_read("Data/Visualization/Landcover/Regions") %>%
  st_transform(st_crs(contig_usa))
## Reading layer `Regions' from data source 
##   `C:\Users\RonanHart\Documents\Projects\R_Spatial_Visualization_Workshop\Data\Visualization\Landcover\Regions' 
##   using driver `ESRI Shapefile'
## Simple feature collection with 3 features and 1 field
## Geometry type: POLYGON
## Dimension:     XY
## Bounding box:  xmin: -2155845 ymin: 681645 xmax: -445725 ymax: 2612715
## Projected CRS: NAD_1983_Albers
regions
## Simple feature collection with 3 features and 1 field
## Geometry type: POLYGON
## Dimension:     XY
## Bounding box:  xmin: -2155845 ymin: 681645 xmax: -445725 ymax: 2612715
## Projected CRS: NAD83(2011) / Conus Albers
##             Region                       geometry
## 1      Great Basin POLYGON ((-1393095 2612715,...
## 2 Southeast Desert POLYGON ((-1866735 1896405,...
## 3 Southern Rockies POLYGON ((-1268685 2285745,...
# add the land boundaries to the map and make it slightly transparent
ggplot() +
  geom_sf(data = contig_usa) +
  geom_sf(aes(fill = Region), alpha = 0.5,
          data = regions)

# Create a polygon of just the Four Corners region
corners_4 <- contig_usa %>%
  filter(STATE %in% c("Utah", "Colorado", "Arizona", "New Mexico"))
ggplot(corners_4) +
  geom_sf()

# add the landcover boundaries to this map
ggplot() +
  geom_sf(data = corners_4) +
  geom_sf(data = regions,
          aes(fill = Region), alpha = 0.5)

# Make some mapping adjustments
ggplot() +
  # add the US underlayer
  geom_sf(data = contig_usa, linetype = 2) +
  geom_sf(data = regions,
          aes(fill = Region)) +
  geom_sf(data = corners_4, col = "grey10", fill = NA) +
  # Zoom the plot window to the 4 Corners region
  xlim(c(st_bbox(corners_4)["xmin"], st_bbox(corners_4)["xmax"])) +
  ylim(c(st_bbox(corners_4)["ymin"], st_bbox(corners_4)["ymax"])) +
  theme_bw()

# Load the landcover rasters
basin <- rast("Data/Visualization/Landcover/Great_Basin.tif")
desert <- rast("Data/Visualization/Landcover/Southeast_Desert.tif")
rockies <- rast("Data/Visualization/Landcover/Southern_Rockies.tif")

basin
## class       : SpatRaster 
## dimensions  : 3197, 3199, 1  (nrow, ncol, nlyr)
## resolution  : 300, 300  (x, y)
## extent      : -2155875, -1196175, 1653645, 2612745  (xmin, xmax, ymin, ymax)
## coord. ref. : NAD_1983_Albers 
## source      : Great_Basin.tif 
## name        : Great_Basin 
## min value   :          42 
## max value   :         584
desert
## class       : SpatRaster 
## dimensions  : 4050, 5304, 1  (nrow, ncol, nlyr)
## resolution  : 300, 300  (x, y)
## extent      : -2036715, -445515, 681405, 1896405  (xmin, xmax, ymin, ymax)
## coord. ref. : NAD_1983_Albers 
## source      : Southeast_Desert.tif 
## name        : Southeast_Desert 
## min value   :               39 
## max value   :              584
rockies
## class       : SpatRaster 
## dimensions  : 3596, 3072, 1  (nrow, ncol, nlyr)
## resolution  : 300, 300  (x, y)
## extent      : -1616265, -694665, 1206945, 2285745  (xmin, xmax, ymin, ymax)
## coord. ref. : NAD_1983_Albers 
## source      : Southern_Rockies.tif 
## name        : Southern_Rockies 
## min value   :               46 
## max value   :              584

# Merge into one raster
land <- merge(basin, desert, rockies)
land
## class       : SpatRaster 
## dimensions  : 6438, 5701, 1  (nrow, ncol, nlyr)
## resolution  : 300, 300  (x, y)
## extent      : -2155875, -445575, 681405, 2612805  (xmin, xmax, ymin, ymax)
## coord. ref. : NAD_1983_Albers 
## source(s)   : memory
## name        : Great_Basin 
## min value   :          39 
## max value   :         584
plot(land)

# Project the 4 corners polygon to the CRS of the landcover raster
# (it's easier to project a vector than a raster)
st_crs(land)
## Coordinate Reference System:
##   User input: NAD_1983_Albers 
##   wkt:
## PROJCRS["NAD_1983_Albers",
##     BASEGEOGCRS["NAD83",
##         DATUM["North American Datum 1983",
##             ELLIPSOID["GRS 1980",6378137,298.257222101004,
##                 LENGTHUNIT["metre",1]]],
##         PRIMEM["Greenwich",0,
##             ANGLEUNIT["degree",0.0174532925199433]],
##         ID["EPSG",4269]],
##     CONVERSION["Albers Equal Area",
##         METHOD["Albers Equal Area",
##             ID["EPSG",9822]],
##         PARAMETER["Latitude of false origin",23,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8821]],
##         PARAMETER["Longitude of false origin",-96,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8822]],
##         PARAMETER["Latitude of 1st standard parallel",29.5,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8823]],
##         PARAMETER["Latitude of 2nd standard parallel",45.5,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8824]],
##         PARAMETER["Easting at false origin",0,
##             LENGTHUNIT["metre",1],
##             ID["EPSG",8826]],
##         PARAMETER["Northing at false origin",0,
##             LENGTHUNIT["metre",1],
##             ID["EPSG",8827]]],
##     CS[Cartesian,2],
##         AXIS["easting",east,
##             ORDER[1],
##             LENGTHUNIT["metre",1,
##                 ID["EPSG",9001]]],
##         AXIS["northing",north,
##             ORDER[2],
##             LENGTHUNIT["metre",1,
##                 ID["EPSG",9001]]]]
corners_4 <- st_transform(corners_4, st_crs(land))
plot(land)
plot(corners_4$geometry, add = T)

# Crop and mask the landcover raster to just the 4 Corner states
land <- crop(land, corners_4) %>%
  mask(corners_4)
plot(land)
plot(corners_4$geometry, add = T)

# Right now, the landcover raster is labeled by numbers, where each number corresponds to a land type. 
# We don't know what the numbers on their own mean, but we have a dataframe that has this information
land_info <- read.csv("Data/Visualization/Landcover/Landcover_Attributes.csv") 
head(land_info[,1:5])
##   Value         NVC_CLASS  SC                  NVC_SUBCL
## 1     0                                                 
## 2     1 Forest & Woodland 1.A Tropical Forest & Woodland
## 3     2 Forest & Woodland 1.A Tropical Forest & Woodland
## 4     3 Forest & Woodland 1.A Tropical Forest & Woodland
## 5     4 Forest & Woodland 1.A Tropical Forest & Woodland
## 6     5 Forest & Woodland 1.A Tropical Forest & Woodland
##                          NVC_FORM
## 1                                
## 2 Tropical Flooded & Swamp Forest
## 3 Tropical Flooded & Swamp Forest
## 4 Tropical Flooded & Swamp Forest
## 5                        Mangrove
## 6  Tropical Dry Forest & Woodland
# the column 'Value' has numbers that correspond to the values in the landcover raster
# Change the landcover raster into a dataframe
names(land) <- "land_class"

land_cover <- as.data.frame(land, xy = T) %>%
  mutate(land_class = round(land_class)) %>%
  # join with the dataframe of the land classes
  left_join(land_info, by = c("land_class" = "Value")) %>%
  # remove NA cells
  filter(!is.na(NVC_CLASS)) 
  
head(land_cover[,1:5])
##          x       y land_class               NVC_CLASS   SC
## 1 -1475625 2251755        489    Desert & Semi-Desert  3.B
## 2 -1475325 2251755        489    Desert & Semi-Desert  3.B
## 3 -1475025 2251755        244       Forest & Woodland  1.B
## 4 -1474725 2251755        428 Shrub & Herb Vegetation 2.C.
## 5 -1474425 2251755        489    Desert & Semi-Desert  3.B
## 6 -1475925 2251455        489    Desert & Semi-Desert  3.B
# Check
ggplot() +
  geom_raster(data = land_cover, aes(x = x, y = y, fill = land_class))

Now we can plot based on the land classifications instead of the numbers.

ggplot() +
  geom_raster(data = land_cover,
          aes(x = x, y = y, fill = NVC_CLASS)) +
  geom_sf(data = corners_4, fill = NA)

Again, the default colors look a little ugly. We could pick colors with hcl.pals() again. But since these colors represent land types, let’s pick colors that would describe each land type.

I used the website Colors in R to get these colors.

sort(unique(land_cover$NVC_CLASS))
##  [1] "Agricultural & Developed Vegetation"            
##  [2] "Aquatic Vegetation"                             
##  [3] "Desert & Semi-Desert"                           
##  [4] "Developed & Other Human Use"                    
##  [5] "Forest & Woodland"                              
##  [6] "Introduced & Semi Natural Vegetation"           
##  [7] "Nonvascular & Sparse Vascular Rock Vegetation"  
##  [8] "Open Rock Vegetation"                           
##  [9] "Open Water"                                     
## [10] "Polar & High Montane Scrub, Grassland & Barrens"
## [11] "Recently Disturbed or Modified"                 
## [12] "Shrub & Herb Vegetation"
# Assign colors to each class name
land_cols <- data.frame(
  cols = c("Agricultural & Developed Vegetation" = "#88b66c", 
          "Aquatic Vegetation" = "#b0e2ff",
           "Desert & Semi-Desert" = "#f9d896",
          "Developed & Other Human Use" = "#874e5c",
          "Forest & Woodland" = "#376822",
          "Introduced & Semi Natural Vegetation" = "#8f8228",
          "Nonvascular & Sparse Vascular Rock Vegetation" = "#e8f2d4",
          "Open Rock Vegetation" = "#d7b895",
          "Open Water" = "#a8c6fc",
          "Polar & High Montane Scrub, Grassland & Barrens" = "#fffafb",
          "Recently Disturbed or Modified" = "#8c451a",
          "Shrub & Herb Vegetation" = "#dae8b0")
  )

# Adjust the dataframe to be more clear
land_cols$land_type = row.names(land_cols)
row.names(land_cols) <- NULL
land_cols
##       cols                                       land_type
## 1  #88b66c             Agricultural & Developed Vegetation
## 2  #b0e2ff                              Aquatic Vegetation
## 3  #f9d896                            Desert & Semi-Desert
## 4  #874e5c                     Developed & Other Human Use
## 5  #376822                               Forest & Woodland
## 6  #8f8228            Introduced & Semi Natural Vegetation
## 7  #e8f2d4   Nonvascular & Sparse Vascular Rock Vegetation
## 8  #d7b895                            Open Rock Vegetation
## 9  #a8c6fc                                      Open Water
## 10 #fffafb Polar & High Montane Scrub, Grassland & Barrens
## 11 #8c451a                  Recently Disturbed or Modified
## 12 #dae8b0                         Shrub & Herb Vegetation

A test to see what these colors look like

(Hint: I picked my colors and then checked them with “Coblis”, a color-blind simulator, to see if the colors are distinguishable even for different types of colorblindness. I then adjusted to get a palette with colors that go well together.)

# Add everything together
landcover_plot <- ggplot() +
  geom_sf(data = st_transform(can_mex, st_crs(corners_4)), col = NA) +
  geom_sf(data = st_transform(contig_usa, st_crs(corners_4)),
          col = "white") +
  geom_raster(data = land_cover,
          aes(x = x, y = y, fill = NVC_CLASS)) +
  geom_sf(data = corners_4, fill = NA, size = 0.75, col = "white") +
  scale_fill_manual(values = land_cols$cols,
                    breaks = land_cols$land_type) +
  # add the land boundaries, change the linetype to dotted and make the fill transparent
  geom_sf(data = regions, 
          fill = NA, col = "grey20", linetype = 2, linewidth = 0.75) +
  theme_bw() +
  labs(fill = "Land Cover Classification", x = "", y = "") +
  xlim(c(st_bbox(corners_4)["xmin"], st_bbox(corners_4)["xmax"])) +
  ylim(c(st_bbox(corners_4)["ymin"], st_bbox(corners_4)["ymax"]))
landcover_plot

5.3.1 Map Elements

Let’s add classic map elements to this map, like a north arrow, scale bar, etc.

# We need the package 'ggsn' for these elements
library(ggsn)
landcover_plot +
  north(corners_4)  # north arrow for map

# we can use this function to see the different options for the north arrow
northSymbols()

landcover_plot <- landcover_plot +
  # let's choose option 14
  north(corners_4, symbol = 14) 
landcover_plot

# Check the arguments for the scalebar function
?scalebar
landcover_plot <- landcover_plot +
  scalebar(corners_4, location = "bottomleft", 
           dist = 150, dist_unit = "km", transform = FALSE,
           st.dist = 0.03, st.bottom = FALSE, st.size = 3) 
landcover_plot