Homework 4
sf
Maps; Animation Plots
Direction
Please submit your R script for Homework 4 to Brightspace with the name below:
danl-310-hw4-LASTNAME-FIRSTNAME.R
( e.g.,danl-310-hw4-choe-byeonghak.R
)
The due is May 5, 2025, 2:00 P.M.
Please send Byeong-Hak an email (
bchoe@geneseo.edu
) if you have any questions.
Part 1. ggmap
visualization
Question 1
Replicate the following map using Classwork 10 example.
Below Google font families are used:
- Title and Label: Pacifico
- Subtitle and Legend: Annie Use Your Telescope
- Caption: Alegreya Sans
scale_*_colorblind()
is used to make a plot colorblind-friendly.The following option is used to create the figure.
ggsave("PATHNAME_OF_GGPLOT_OUTPUT.png",
plot = p, height = 12, width = 8, units = "in")
Click to Check the Answer!
# install.packages(c("osmdata", "osrm")) # ← run once, then comment-out
library(sf) # simple-features objects & spatial ops
library(ggmap) # basemap tiles + ggplot2 integration
library(tidyverse)
library(ggthemes) # extra ggplot2 themes (incl. theme_map)
library(osmdata) # bounding boxes
library(tidygeocoder) # tidy-style geocoding (Nominatim by default)
library(ggrepel)
library(ggimage) # drop-in PNG icons
library(showtext) # Google-font rendering inside R graphics
#–– fonts ––-------------------------------------------------------------------
font_add_google("Roboto", "roboto")
font_add_google("Lato", "lato")
font_add_google("Poppins", "poppins")
font_add_google("Nunito", "nunito")
font_add_google("Annie Use Your Telescope", "annie")
font_add_google("Pacifico", "pacifico")
font_add_google("Alegreya Sans", "aleg")
showtext_auto() # enable showtext globally
#–– Stadia Maps key ––----------------------------------------------------------
<- "YOUR_STADIA_API_KEY" # replace w/ your key
stadia_api register_stadiamaps(stadia_api, write = TRUE) # store in env file
#–– Geocode landmarks ––--------------------------------------------------------
<-
locations tibble(address = c("SUNY Geneseo", "Mount Morris Dam, NY")) |>
geocode(address) |> # adds long / lat columns (CRS 4326)
mutate(address = str_replace_all(address, ", NY", ""))
# osrm & ggmap both attach a function named “mutate()”; avoid a clash by
# not attaching osrm – instead, call it with osrm:: prefix.
# (If you *must* attach osrm, load it *before* ggmap.)
# Pull lon/lat into a plain matrix expected by osrmRoute()
<- as.matrix(locations[, c("long","lat")]) # 2×2 matrix
coords_mat
#–– Bike route ––------------------------------------------------------------
<- osrm::osrmRoute(
route_bike src = coords_mat[1, ], # SUNY Geneseo (lon, lat)
dst = coords_mat[2, ], # Mount Morris Dam, NY (lon, lat)
overview = "full", # full-resolution polyline
osrm.profile = "bike" # routing profile: foot / bike / car
|>
) mutate(mode = "Bike") # tag for legend
#–– Foot route ––------------------------------------------------------------
<- osrm::osrmRoute(
route_foot src = coords_mat[1, ], # SUNY Geneseo (lon, lat)
dst = coords_mat[2, ], # Mount Morris Dam, NY (lon, lat)
overview = "full", # full-resolution polyline
osrm.profile = "foot" # routing profile: foot / bike / car
|>
) mutate(mode = "Foot") # tag for legend
#–– Driving route ––------------------------------------------------------------
<- osrm::osrmRoute(
route_car src = coords_mat[1, ],
dst = coords_mat[2, ],
overview = "full",
osrm.profile = "car"
|>
) mutate(mode = "Car")
# combine & prettify
<- rbind(route_bike, route_foot, route_car) |>
routes mutate(duration = round(duration, 2), # minutes
mode = str_c(mode, ": ", duration, " min.")) # legend label
#–– Build plotting extent ––----------------------------------------------------
<- st_bbox(routes) # xmin, ymin, xmax, ymax (degrees)
bb_routes <- (bb_routes$xmax - bb_routes$xmin) * 0.4 # 40 % padding
pad_x <- (bb_routes$ymax - bb_routes$ymin) * 0.4
pad_y
<- c(bb_routes$xmin - pad_x, bb_routes$xmax + pad_x)
xlim <- c(bb_routes$ymin - pad_y, bb_routes$ymax + pad_y)
ylim
#–– Fetch basemap tiles ––------------------------------------------------------
<- matrix(
bb_from_limits c(xlim["xmin"], xlim["xmax"], # x-row (min, max)
"ymin"], ylim["ymax"]), # y-row (min, max)
ylim[nrow = 2, byrow = TRUE,
dimnames = list(c("x", "y"), c("min", "max"))
)
# bb_from_limits
<- get_stadiamap(
man_map
bb_from_limits,zoom = 13, # ↑ zoom = ↑ detail (and ↑ tiles & time)
maptype = "stamen_terrain" # other options: stamen_terrain_labels, stamen_toner, stamen_watercolor, …
)
#–– Plot ––---------------------------------------------------------------------
<- "https://bcdanl.github.io/lec_figs/marker-icon-red.png" # pin PNG
icon_url
<- ggmap(man_map) +
p geom_sf(
data = routes,
aes(color = mode),
inherit.aes = FALSE, # don’t map long/lat twice
linewidth = rel(2),
alpha = 0.75
+
) geom_image(
data = locations,
aes(x = long, y = lat, image = icon_url),
size = .025
+
) geom_label_repel(
data = locations,
aes(long, lat, label = address),
size = rel(12),
box.padding = 1.75,
family = "pacifico"
+
) coord_sf( # keep CRS 4326; clamp to padded bbox
crs = st_crs(4326),
xlim = xlim,
ylim = ylim,
expand = FALSE
+
) scale_color_colorblind() +
guides(
color = guide_legend(
title.position = "top",
label.position = "bottom",
keywidth = rel(10.5)
)+
) labs(
color = "",
title = "Bike vs. Car vs. Foot",
subtitle = "How would you like to go from SUNY Geneseo to Mount Morris Dam?",
caption = "Source: OpenStreetMap (OSM) and Open Source Routing Machine (OSRM)"
+
) theme_map() +
theme(
legend.position = "top",
plot.title = element_text(face = "bold",
size = rel(9),
family = 'pacifico',
hjust = 0),
plot.subtitle = element_text(face = "bold.italic",
size = rel(7),
family = 'annie',
lineheight = .25,
hjust = 0),
plot.caption = element_text(face = "italic",
size = rel(4),
family = 'aleg'),
legend.text = element_text(face = "bold",
family = 'annie',
size = rel(5.5)),
legend.background = element_rect(fill = NA, color = NA),
legend.box.margin = margin(-15,0,-7.5,0),
)
# set a plotting device size manually for forcing the right canvas size
dev.new(width = 8, height = 12)
p
ggsave("/Users/bchoe/Documents/websites/bcdanl.github.io/lec_figs/map_geneseo_mtmorrisdam3.png",
plot = p, height = 12, width = 8, units = "in")
Part 2. Simple Feature (sf
) and Aninimation Plots
Consider the county-level climate opinion data from Yale Program on Climate Change Communication:
<- read_csv(
climate_opinion 'https://bcdanl.github.io/data/climate-change-public-opinion.csv')
Question 2
- Replicate the following map about
variable
human:- human: Estimated percentage who think that global warming is caused mostly by human activities
- For R packages and map data, use the following:
# Load required packages ---------------------------------------------------
library(tidyverse)
library(sf) # simple-features objects for spatial data
library(tigris) # fast access to Census TIGER/Line shapefiles
library(ggthemes)
library(gganimate) # turn static ggplots into animations
library(showtext) # Google-font rendering inside R graphics
# Register Google fonts so they render the same on any machine -------------
font_add_google("Roboto", "roboto") # body text
font_add_google("Alegreya Sans","aleg") # title text
showtext_auto() # activate showtext globally
# 1) Download US state & county geometries (simplified “cartographic”
# boundaries) at 1:5 million scale, then shift Alaska, Hawai‘i & PR closer ---
<- states(
us_states cb = TRUE, # use generalized cartographic boundary files
resolution = "5m", # 1 : 5 000 000
year = 2024 # 2024 vintage
|>
) shift_geometry() # moves AK/HI & PR into a CONUS-friendly layout
<- counties(
us_counties state = NULL, # all states + DC + territories
cb = TRUE,
resolution = "5m",
year = 2024
|>
) shift_geometry()
# 2) Build a bounding box that covers only the 50 states -------------------
# (exclude DC + territories so the map crops nicely)
<- states(cb = TRUE, resolution = "5m", year = 2024)
main_states
<- state.abb # built-in vec of 50 state abbr.
keep <- main_states |>
main_states filter(STUSPS %in% keep) |>
shift_geometry()
<- sf::st_bbox(main_states) # xmin / ymin / xmax / ymax vector
bb
# 3) Quick static map -------------------------------------------------------
ggplot() +
geom_sf(data = us_counties,
fill = NA, # counties as thin outlines
color = "grey80",
linewidth = 0.01) +
geom_sf(data = us_states,
fill = NA, # thicker state borders
color = "black") +
coord_sf( # crop to 50-state bounding box
xlim = c(bb["xmin"], bb["xmax"]),
ylim = c(bb["ymin"], bb["ymax"]),
expand = FALSE
+
) theme_map() # clean, border-free background
Click to Check the Answer!
library(tidyverse)
library(sf)
library(tigris)
library(ggthemes)
library(gganimate)
library(showtext)
font_add_google("Roboto", "roboto")
font_add_google("Lato", "lato")
font_add_google("Poppins", "poppins")
font_add_google("Nunito", "nunito")
font_add_google("Annie Use Your Telescope", "annie")
font_add_google("Pacifico", "pacifico")
font_add_google("Alegreya Sans", "aleg")
showtext_auto() # Enables Google fonts in R plots.
# 1) pull every U.S. state/county as a simplified (“cartographic boundary”) file:
<- states(
us_states cb = TRUE, # cartographic boundary (generalized)
resolution = "5m", # 1:5 million scale (also "500k" or "20m")
year = 2024 # vintage
|>
) shift_geometry()
<- counties(
us_counties state = NULL,
cb = TRUE,
resolution = "5m",
year = 2024) |>
shift_geometry()
# 1. get the box:
# pull everything that states() gives us
<- states(cb = TRUE, resolution = "5m", year = 2024)
main_states
# keep only the 50 US states
<- c(state.abb)
keep <- main_states |>
main_states filter(STUSPS %in% keep) |>
shift_geometry()
<- sf::st_bbox(main_states)
bb
# climate data for human belief
<- read_csv(
climate_opinion 'https://bcdanl.github.io/data/climate-change-public-opinion.csv')
<- climate_opinion |>
climate_opinion_human filter(variable %in% c("human")) |>
select(GeoID, GeoName,
starts_with("x")) |>
pivot_longer(cols = x2014:x2024,
names_to = "year",
values_to = "belief") |>
mutate(year = str_replace_all(year, "x", ""),
year = as.integer(year)) |>
rename(GEOID = GeoID)
unique(climate_opinion_human$year)
# us_counties6 <- rbind(us_counties |> mutate(year = 2014),
# us_counties |> mutate(year = 2016),
# us_counties |> mutate(year = 2018),
# us_counties |> mutate(year = 2020),
# us_counties |> mutate(year = 2022),
# us_counties |> mutate(year = 2024)) |>
# left_join(climate_opinion_human) |>
# filter(STUSPS != "PR")
<- us_counties |>
us_counties_join left_join(climate_opinion_human) |>
filter(STUSPS != "PR") |>
filter(year %in% seq(2014,2024, 2))
# facet
ggplot() +
geom_sf(data = us_counties_join,
aes(geometry = geometry, fill = belief/100),
color = NA, size = 0.01) +
geom_sf(data = us_states |> filter(STUSPS != "PR"),
fill = NA, color = "grey", size = .25) +
coord_sf(
xlim = c(bb["xmin"], bb["xmax"]),
ylim = c(bb["ymin"], bb["ymax"]),
expand = FALSE
+
) scale_fill_viridis_c(
option = "inferno",
limits = c(min((us_counties6$belief)/100), max((us_counties6$belief)/100)),
breaks = seq(.4, 1, .1),
labels = scales::percent_format(accuracy = 1),
guide = guide_colorbar(
title = "% Yes",
title.position = "left",
barwidth = unit(17.5, "cm"),
# barheight = unit(0.5, "cm"),
direction = "horizontal",
nbin = 500,
raster = FALSE,
ticks = TRUE,
draw.llim = FALSE,
reverse = FALSE
)+
) facet_wrap(~year) +
labs(
title = "Is global warming caused mostly by human activities?",
caption = "Source: Yale Climate Change Communication, 2025"
+
) theme_map() +
theme(plot.title = element_text(hjust = .5,
margin = margin(0,0,0,0),
family = 'aleg',
face = 'bold',
size = rel(2.75)),
plot.subtitle = element_text(hjust = .75,
vjust = -87.5,
family = 'roboto',
face = 'bold',
size = rel(1.75)),
plot.caption = element_text(family = 'roboto',
face = 'italic'),
strip.background = element_rect(fill = NA,
color = NA),
strip.text = element_text(
family = 'roboto',
face = 'bold',
size = rel(1.75)),
legend.position = 'top',
legend.justification = 'center',
legend.title = element_text(family = 'roboto',
face = 'bold',
vjust = .75)
)
Question 3
- Animate the maps for the even-numbered years used in Question 2:
To view this animation bigger, click this.
For
animate()
, use the following parameter setting:
ggplot() +
...labs(subtitle = "Year: {floor(frame_time)}") +
...
<- animate(p_anim,
anim nframes = 20,
fps = 5,
width = 8, height = 5,
units = "in",
res = 900) # resolution can be reduced
# To save animation as PATHNAME_FOR_YOUR_FILE.gif
anim_save("PATHNAME_FOR_YOUR_FILE.gif", anim)
You can open “
PATHNAME_FOR_YOUR_FILE.gif
” in any web browser (Chrome is recommended).transition_time(year)
treats year as a continuous numeric variable.After you call
animate()
,gganimate
:
- Spans the full numeric range of the variable — here 2014 → 2024, a width of 10 years.
- Slices that span into
nframes
equally spaced instants.- Here
nframes = 20
, so the frame-to-frame step is \[ \begin{align} \Delta = \frac{2024 - 2014}{20 - 1} \;=\; \frac{10}{19} \;\approx\; 0.526 \text{ years}. \end{align} \]
- Here
- Assigns each frame its own “virtual” time stamp:
- \(t_0 = 2014\), \(t_1 = 2014.526\), \(t_2 = 2015.053\), …
Click to Check the Answer!
<- ggplot() +
p geom_sf(data = us_counties_join,
aes(geometry = geometry, fill = belief/100),
color = NA, size = 0.01) +
geom_sf(data = us_states |> filter(STUSPS != "PR"),
fill = NA, color = "grey", size = .25) +
coord_sf(
xlim = c(bb["xmin"], bb["xmax"]),
ylim = c(bb["ymin"], bb["ymax"]),
expand = FALSE
+
) scale_fill_viridis_c(
option = "inferno",
limits = c(min((us_counties6$belief)/100), max((us_counties6$belief)/100)),
breaks = seq(.4, 1, .1),
labels = scales::percent_format(accuracy = 1),
guide = guide_colorbar(
title = "% Yes",
title.position = "left",
barwidth = unit(17.5, "cm"),
# barheight = unit(0.5, "cm"),
direction = "horizontal",
nbin = 500,
raster = FALSE,
ticks = TRUE,
draw.llim = FALSE,
reverse = FALSE
)+
) labs(
title = "Is global warming caused mostly by human activities?",
subtitle = "Year: {floor(frame_time)}",
caption = "Source: Yale Climate Change Communication, 2025"
+
) theme_map() +
theme(plot.title = element_text(hjust = .5,
margin = margin(0,0,-10,0),
family = 'aleg',
face = 'bold',
size = rel(2.75)),
plot.subtitle = element_text(hjust = .75,
vjust = -87.5,
family = 'roboto',
face = 'bold',
size = rel(1.75)),
plot.caption = element_text(family = 'roboto',
face = 'italic'),
legend.position = 'top',
legend.justification = 'center',
legend.title = element_text(family = 'roboto',
face = 'bold',
vjust = .75)
)# p
<- p +
p_anim transition_time(year) + # animate through year
ease_aes('linear')
<- animate(p_anim,
anim # nframes = length(years)*18,
nframes = 20,
fps = 5,
width = 8, height = 5,
units = "in",
res = 900)
anim_save("/Users/bchoe/Downloads/sf_cc_human2025.gif", anim)