Creating Maps with R
First, let’s load packages necessary for this activity using the code
below. Note: you may need to install packages beforehand by using the
install.packages() function and the package name in
quotes.
library(tidyverse)
library(stringr)
library(lubridate)
library(ggpubr)
library(leaflet)
library(sf)
library(tigris)
library(plotly)Fetching and importing data
First, let’s import the shape files which define the boundaries for the states and counties.
# Downloading state-level shape files from US Census Bureau
if(!file.exists("cb_2018_us_state_500k.zip")) {
download.file(url = "https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_state_500k.zip",
destfile = "cb_2018_us_state_500k.zip")
}
# Create directory for geospatial files
if(!dir.exists("GeoFiles")) {
dir.create("GeoFiles")
}
# Unzipping files
utils::unzip("cb_2018_us_state_500k.zip",
exdir = "GeoFiles")
# Loading the shapefiles
state_shape <- st_read("GeoFiles//cb_2018_us_state_500k.shp",
quiet = TRUE)# Alternatively using the tigris package to fetch shape file (but water makes Michigan look strange!)
state_shape_tigris <- tigris::states(year = 2021)# Downloading county-level shape files from US Census Bureau
if(!file.exists("cb_2018_us_county_500k.zip")) {
download.file(url = "https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_county_500k.zip",
destfile = "cb_2018_us_county_500k.zip")
}
if(!dir.exists("GeoFiles")) {
dir.create("GeoFiles/")
}
# Unzipping files
utils::unzip("cb_2018_us_county_500k.zip",
exdir = "GeoFiles")
# Loading the shapefiles
county_shape <- st_read("GeoFiles//cb_2018_us_county_500k.shp",
quiet = TRUE)Next, let’s download and import data containing state FIPS (Federal Information Processing Standards) codes, which are unique identifiers used by federal departments and agencies for each state and county in the United States .
# Downloading and importing state FIPS codes
if(!file.exists("state-geocodes-v2020.xlsx")) {
download.file("https://www2.census.gov/programs-surveys/popest/geographies/2020/state-geocodes-v2020.xlsx",
destfile = "state-geocodes-v2020.xlsx", mode = "wb")
}
state_fips <- readxl::read_excel("state-geocodes-v2020.xlsx", skip = 5) %>%
dplyr::transmute(state_fips = `State (FIPS)`,
state = Name)And let’s also import the state-level elections data from the course GitHub page:
# Importing state-level elections data
elections <- read_csv("https://raw.githubusercontent.com/dilernia/STA418-518/main/Data/1976-2020-president.csv") Then, let’s create a new variable, party, that
simplifies the political parties even further to “Democrats”,
“Republicans”, and “Other”.
# Creating simplified political party definition, and calculating percent of vote earned
elections <- elections %>%
dplyr::mutate(party = case_when(party_simplified == "DEMOCRAT" ~ "democrat",
party_simplified == "REPUBLICAN" ~ "republican",
TRUE ~ "other"),
prop_vote = candidatevotes / totalvotes)Next, let’s merge the elections data with the shape file data
obtained from the Census Bureau to obtain an sf (simple
feature) object which we will use to create a choropleth map.
# Merging elections data with shape file data
state_shape_full <- state_shape %>%
dplyr::left_join(elections %>%
dplyr::mutate(GEOID = str_pad(state_fips, width = 2, side = "left", pad = "0")) %>%
group_by(year, state, party) %>%
slice_max(order_by = prop_vote, n = 1) %>%
ungroup() %>%
pivot_wider(
id_cols = c(year:office, GEOID),
names_from = party,
values_from = c(candidate, prop_vote),
names_sep = "_",
names_glue = "{party}_{.value}") %>% rowwise() %>%
mutate(across(c(democrat_candidate, other_candidate, republican_candidate),
~ str_to_title(str_c(rev(str_split(., ", ")[[1]]), collapse = " ")))),
by = c("GEOID" = "GEOID"))Choropleth maps
Choropleth maps are a popular and effective way to visualize spatial data by displaying thematic maps 🗺📍️, where areas are shaded or colored according to the value of a variable being represented. For example, a choropleth map can be used to display differences in population density, the unemployment rate, crime rates, or other metrics across a spatial grid partitioned into distinct areal units such as countries or states.
Choropleth maps are commonly used in data journalism 📰, public health ⚕️, urban planning 🏙️, and numerous other fields to illustrate patterns and trends in geospatial data. Geospatial data is increasingly available, contributing to the wide-spread use of choropleth maps since they facilitate communication of geospatial information to a broad audience in an efficient and understandable manner.
Leaflet
State-level choropleth map
Let’s first create a choropleth map with Leaflet, looking at state-level election results for the 2012 election.
# Subset data to only for 2012 and Democratic party. Note that mapData should be an 'sf' object
mapData <- state_shape_full %>%
dplyr::filter(year == 2012)
# Quantitative variable to color areal units (states) by
myVariable <- round(mapData$democrat_prop_vote, 4)*100
# Variable defining areal units (states)
myRegions <- mapData$state
# Defining pop-up labels
labels <- mapData %>%
dplyr::mutate(labels = paste0("<strong>", stringr::str_to_title(state), "</strong><br/>",
democrat_candidate, ": ", round(democrat_prop_vote, 4)*100, "% <br/>",
republican_candidate, ": ", round(republican_prop_vote, 4)*100, "% <br/>",
other_candidate, ": ", round(other_prop_vote, 4)*100, "%") %>%
lapply(htmltools::HTML)) %>%
dplyr::pull(labels)
# Defining nice cut points for coloring
bins <- seq(20, 80, by = 10)
# Selecting color palette
pal <- colorBin("RdBu", domain = myVariable, bins = bins)
# Create the choropleth map
myLeaflet <- leaflet(mapData) %>%
setView(lng = -96, lat = 37.8, zoom = 3.5) %>%
addProviderTiles("OpenStreetMap.Mapnik",
options = providerTileOptions(
id = "mapbox.light",
accessToken = Sys.getenv('MAPBOX_ACCESS_TOKEN'))) %>%
addPolygons(
fillColor = ~pal(myVariable),
weight = 0.80,
opacity = 1,
color = "black",
fillOpacity = 1,
highlightOptions = highlightOptions(
color = "#666"),
label = labels,
labelOptions = labelOptions(textsize = "15px")) %>%
addLegend(htmltools::div(style="text-align: left;"),
pal = pal,
values = ~ myVariable,
opacity = 0.7,
title = "Percent Democrat, 2012 Election",
position = "bottomright")
# Left-aligning text in legend
myLeaflet <- htmltools::browsable(
htmltools::tagList(
list(
htmltools::tags$head(
htmltools::tags$style(
".leaflet .legend {
text-align: left;
}",
".leaflet .legend i{
text-align: left;
}"
)
),
myLeaflet)))
myLeafletTile layers
Note that the base map style, called the tile layer, can be specified
in the provider argument in the
addProviderTiles() function. Possible base maps to use are
stored in the object leaflet::providers, and extra tile
layer styles are available on Leaflet’s GitHub site here https://leaflet-extras.github.io/leaflet-providers/preview/
and here https://github.com/leaflet-extras/leaflet-providers.
Some tile layers are freely available with no restrictions. However, to use certain tile providers in Leaflet, you may need to obtain an access token or API key. This is typically required for providers that have usage limits or require authentication. The process for obtaining an access token varies depending on the provider, but often does not cost any money and simply requires creating a free account through the provider’s website.
# Create the choropleth map
myLeaflet <- leaflet(mapData) %>%
setView(lng = -96, lat = 37.8, zoom = 3.5) %>%
addProviderTiles(provider = "Stamen.TonerLite",
options = providerTileOptions(
id = "mapbox.light",
accessToken = Sys.getenv('MAPBOX_ACCESS_TOKEN'))) %>%
addPolygons(
fillColor = ~pal(myVariable),
weight = 0.80,
opacity = 1,
color = "black",
fillOpacity = 1,
highlightOptions = highlightOptions(
color = "#666"),
label = labels,
labelOptions = labelOptions(textsize = "15px")) %>%
addLegend(htmltools::div(style="text-align: left;"),
pal = pal,
values = ~ myVariable,
opacity = 0.7,
title = "Percent Democrat, 2012 Election",
position = "bottomright")
# Left-aligning text in legend
myLeaflet <- htmltools::browsable(
htmltools::tagList(
list(
htmltools::tags$head(
htmltools::tags$style(
".leaflet .legend {
text-align: left;
}",
".leaflet .legend i{
text-align: left;
}"
)
),
myLeaflet)))
myLeafletColor palettes
Color palettes are another way in which choropleth maps are commonly customized. R provides many color palettes that can be used in data visualizations. More color palettes that are available in R can be found here: https://www.nceas.ucsb.edu/sites/default/files/2020-04/colorPaletteCheatsheet.pdf.
# Selecting color palette
pal <- colorBin("Blues", domain = myVariable, bins = bins)
# Create the choropleth map
myLeaflet <- leaflet(mapData) %>%
setView(lng = -96, lat = 37.8, zoom = 3.5) %>%
addProviderTiles("OpenStreetMap.Mapnik",
options = providerTileOptions(
id = "mapbox.light",
accessToken = Sys.getenv('MAPBOX_ACCESS_TOKEN'))) %>%
addPolygons(
fillColor = ~pal(myVariable),
weight = 0.80,
opacity = 1,
color = "black",
fillOpacity = 1,
highlightOptions = highlightOptions(
color = "#666"),
label = labels,
labelOptions = labelOptions(textsize = "15px")) %>%
addLegend(htmltools::div(style="text-align: left;"),
pal = pal,
values = ~ myVariable,
opacity = 0.7,
title = "Percent Democrat, 2012 Election",
position = "bottomright")
# Left-aligning text in legend
myLeaflet <- htmltools::browsable(
htmltools::tagList(
list(
htmltools::tags$head(
htmltools::tags$style(
".leaflet .legend {
text-align: left;
}",
".leaflet .legend i{
text-align: left;
}"
)
),
myLeaflet)))
myLeafletAdding markers
We can also add markers to a leaflet map given latitude and longitude values. For example, we can plot a marker indicating the location of the capitals of each of the 50 states using their latitude and longitude. To do so, we first import data into R containing the capitals and their coordinates using the code below.
# Importing data on U.S. capitals
usCapitals <- read_csv("https://raw.githubusercontent.com/jasperdebie/VisInfo/master/us-state-capitals.csv") %>%
dplyr::mutate(city = str_squish(str_to_title(str_remove_all(description, pattern = "<br>"))),
city_state = str_c(city, ", ", name))
# Creating map with markers to show location of state capitals
leaflet(data = usCapitals) %>% addTiles() %>%
addMarkers(~longitude, ~latitude, popup = ~as.character(city_state), label = ~as.character(city_state))
It is possible there are so many markers than the map is too crowded. One solution is to use aesthetics to highlight certain points / markers more than others. For example, let’s visualize data on earthquakes from the U.S. Geological Survey using the size and opacity of the markers to display the magnitude of each earthquake.
# Importing data on earth quakes
earthquakes <- read_csv("https://raw.githubusercontent.com/dilernia/STA418-518/main/Data/usgs_main.csv")
# Creating map with markers to show location of state capitals
earthLeaflet <- leaflet(data = earthquakes) %>%
addTiles() %>%
setView(lng = -96, lat = 37.8, zoom = 3.5) %>%
addCircleMarkers(
lng = ~longitude,
lat = ~latitude,
weight = 1,
radius = ~mag,
color = "forestgreen",
popup = ~paste0("Magnitude: ", as.character(mag)),
label = ~paste0("Magnitude: ", as.character(mag)),
stroke = FALSE,
fillOpacity = ~mag/10,
)
earthLeaflet
When there is a large number of points, we can also automatically cluster the markers using leaflet.
# Creating map with markers to show location of state capitals
earthLeaflet %>%
addCircleMarkers(
lng = ~longitude,
lat = ~latitude,
stroke = FALSE,
clusterOptions = markerClusterOptions()
)
Leaflet permits extensive customization of different aspects of your map as well. For example, you can use custom icons / markers on the plot, rather than just circles, following this demo here https://leafletjs.com/examples/custom-icons/. For a more thorough overview of the capabilities of leaflet in R, see this site: http://rstudio.github.io/leaflet/.
ggplot2
State-level choropleth map
# Fixing issue with Alaska and Hawaii
states_alaska_hawaii <- state_shape_full %>%
dplyr::filter(year == 2012,
!is.na(democrat_prop_vote)) %>%
tigris::shift_geometry(geoid_column = "GEOID")
# Adding labels column for interactivity
states_alaska_hawaii <- states_alaska_hawaii %>%
dplyr::mutate(Result = paste0("<b>", stringr::str_to_title(state), "<b><br>",
democrat_candidate, ": ", round(democrat_prop_vote, 4)*100, "% <br>",
republican_candidate, ": ", round(republican_prop_vote, 4)*100, "% <br>",
other_candidate, ": ", round(other_prop_vote, 4)*100, "%") %>%
lapply(htmltools::HTML))
# Plot it
electionGG <- states_alaska_hawaii %>%
ggplot(aes(fill = democrat_prop_vote,
text = Result)) +
geom_sf() +
scale_fill_gradient2(low = "#DC3220",
mid = "#ffffff",
high = "#005AB5",
midpoint = 0.50) +
labs(title = "United States 2012 Presidential Election",
fill = "Percent Democrat") +
theme_void()
electionGG
Lastly, we can make the choropleth map created with
ggplot2 interactive using the plotly R
package.
library(plotly)
# Convert to an interactive plot using ggplotly()
electionGG_interactive <- ggplotly(electionGG,
tooltip = c("text"))
electionGG_interactive