1. Introduction

This tutorial covers how to create interactive maps in R, focusing on two types of visualizations:

  1. Maps with markers, which are icons displayed at particular geographic locations
  2. Choropleth maps, sometimes called heat maps, where geographic units such as countries, states, or watersheds are colored according to a particular variable

There are many ways to create maps using R, but we will focus on leaflet, a popular open-source JavaScript library that is used by many professional organizations to create interactive maps.

In this tutorial we will require the following packages:

library(leaflet)    # The map-making package
library(geojsonio)  # A package for geographic and spatial data, requires the latest version of dplyr
library(dplyr)      # Used for data manipulation and merging
library(htmltools)  # Used for constructing map labels using HTML

In this tutorial we will use two datasets:

  1. The “quakes” dataset, which contains the locations of 1000 seismic events occuring near the Fiji islands since 1964.
data(quakes)
head(quakes)
##      lat   long depth mag stations
## 1 -20.42 181.62   562 4.8       41
## 2 -20.62 181.03   650 4.2       15
## 3 -26.00 184.10    42 5.4       43
## 4 -17.97 181.66   626 4.1       19
## 5 -20.42 181.96   649 4.0       11
## 6 -19.68 184.31   195 4.0       12
  1. The Happy Planet Index, a dataset of containing various measures of happiness and sustainability for countries around the globe.
happiness <- read.csv('https://remiller1450.github.io/data/HappyPlanet.csv', stringsAsFactors = FALSE)
head(happiness)
##     Country Region Happiness LifeExpectancy Footprint  HLY   HPI HPIRank
## 1   Albania      7       5.5           76.2       2.2 41.7 47.91      54
## 2   Algeria      3       5.6           71.7       1.7 40.1 51.23      40
## 3    Angola      4       4.3           41.7       0.9 17.8 26.78     130
## 4 Argentina      1       7.1           74.8       2.5 53.4 58.95      15
## 5   Armenia      7       5.0           71.7       1.4 36.1 48.28      48
## 6 Australia      2       7.9           80.9       7.8 63.7 36.64     102
##   GDPperCapita   HDI Population
## 1         5316 0.801       3.15
## 2         7062 0.733      32.85
## 3         2335 0.446      16.10
## 4        14280 0.869      38.75
## 5         4945 0.775       3.02
## 6        31794 0.962      20.40

2. Base Maps

Generally speaking, there are three steps to creating a map using leaflet:

  1. Creating a map widget by calling the leaflet function
  2. Adding layers to the map using functions like addTiles, addMarkers, and addPolygons
  3. Printing the map to display it

Layers are usually added by “piping” via the %>% operator. Put simply, this means passing the resulting object on the left of the %>% symbol as the first argument to the function on the right of the %>%, thereby chaining together a series of functions that each operate on the results of the prior step.

## Step 1 call leaflet
map <- leaflet() %>%
          addTiles() ## Step 2 add a layer using %>%

## Step 3 print the map
map

Leaflet constructs a base map using map tiles, an approach popularized by google maps that is now used by nearly every interactive map online.

Calling addTiles() with no arguments will generate a base map using OpenStreetMap tiles. For most purposes OpenStreetMap tiles are all you’ll need; however, the leaflet package also comes with dozens of different tile options created by third party providers and stored in an object named providers. Provider tile sets can be used to create a base map via the addProviderTiles function:

head(providers)  ## Providers contains a list of different tiles
## $OpenStreetMap
## [1] "OpenStreetMap"
## 
## $OpenStreetMap.Mapnik
## [1] "OpenStreetMap.Mapnik"
## 
## $OpenStreetMap.BlackAndWhite
## [1] "OpenStreetMap.BlackAndWhite"
## 
## $OpenStreetMap.DE
## [1] "OpenStreetMap.DE"
## 
## $OpenStreetMap.CH
## [1] "OpenStreetMap.CH"
## 
## $OpenStreetMap.France
## [1] "OpenStreetMap.France"
map <- leaflet() %>% 
          addProviderTiles(providers$Esri.WorldTopoMap)
map

3. Markers

The simplest addition to base map is a marker, or identifiable point. Markers can be added to the base map using addMarkers function. At minimum a marker requires a longitude and latitude, but it is useful to also include labels, which display text when you hover over the marker, or popups, which display a dialog box when you click on the marker.

fiji_reduced <- quakes[1:10,]

map <- leaflet(data = fiji_reduced) %>% 
        addTiles() %>% 
        addMarkers(lng = ~long, lat = ~lat, 
                   label = ~paste(depth, "meters"), 
                   popup = ~paste(mag, "Richter Magnitude", "<br/>", depth, "Depth"))

map

What is displayed by a popup or label is an interpretted HTML strings, meaning commands like “<br/>” can be used to separate the text onto a new line, and other HTML commands can be used to modify the text’s appearance.

Additionally, in the example above, you should notice the \(\sim\) character that is used in front of the inputs to lng, lat, label, and popup. This tells addMarkers to look for those variable within the data object specified in the call to leaflet. So, in this example, leaflet looks for the variables “lat” and “lat” in fiji_reduced. Later in this tutorial we’ll see examples that don’t do this.

Question #1

Construct a base map using “Esri.OceanBasemap” tiles. Then, filter the quakes dataset to include only observations reported by more than 40 stations and plot the location of these earthquakes on your map with hoverable labels that display the number of stations reporting (ie: text like “47 Stations Reporting”).

Marker Clusters

Adding a large number of markers to a map is computationally demanding, and it also makes the map difficult to read. To address these and other concerns, leaflet includes a suite of built in functions includingmarkerClusterOptions, which aggregates nearby markers into a group that will split when you click on it to zoom in.

map <- leaflet(data = quakes) %>% 
        addTiles() %>% 
        addMarkers(lng = ~long, lat = ~lat,
                   label = ~paste(depth, "meters"),
                   clusterOptions = markerClusterOptions())

map

Circle Markers

The appearance of markers can be changed in two ways. The most straightfoward is to use addCircleMarkers, which plots circular points at the marker locations. These markers have attributes like color, opacity, and fill that can be used to add information to the map. The addCircleMarkers function uses syntax similar to addMarkers, a few of its options are demonstrated in the example below:

quakes$col <- ifelse(quakes$mag > 5, "red", "blue")

map <- leaflet(data = quakes) %>% 
        addTiles() %>% 
        addCircleMarkers(lng = ~long, lat = ~lat, 
                   label = ~paste(depth, "meters"),
                   color = ~col,
                   fillOpacity = 0.25,
                   stroke = FALSE)

map

Here, we color earthquakes with magnitudes over 5.0 red, while coloring less intense earthquakes blue. We use the argument fillOpacity = 0.25 to make the markers more transparent, allowing viewers to see relative concentrations, and we add the argument stroke = FALSE to remove the solid outline that by default surrounds circular markers.

Custom Markers

Custom markers can be added to a map as icons. Icons are created from an image using its web url or local file path. The example below sets up two icons, a ferry icon and a skull with crossbones icon, using images from an online repository.

  oceanIcons <- iconList(
    ferry = makeIcon(
      "http://globetrotterlife.org/blog/wp-content/uploads/leaflet-maps-marker-icons/ferry-18.png"),
    danger = makeIcon(
      "http://globetrotterlife.org/blog/wp-content/uploads/leaflet-maps-marker-icons/danger-24.png"))

iconid <- ifelse(quakes$mag < 5, "ferry", "danger")

leaflet(data = quakes[1:4,]) %>% addTiles() %>%
  addMarkers(~long, ~lat, icon = oceanIcons[iconid])

To use an icon, it must first be setup using makeIcon, and then stored in a special list using iconList before being passed to addMarkers.

In this example you should notice that we do not use a \(\sim\) in front of oceanIcons. That is because oceanIcons exists outside of the quakes data frame.

Question #2

Construct a base map using the default map tiles. Then, using your filtered dataset (which contains earthquakes reported by more than 40 stations), construct a map displaying these earthquakes using clustered circle markers which are colored yellow if the earthquake was reported by more than 70 stations and black otherwise. You do not need to add any labels or popups to this map.

4. Choropleths

A choropleth is a type of map that displays geographic units like countries, states, census areas, or watersheds with each unit colored based upon a particular variable. The first step in creating a choropleth is to define the boundaries of each geographic unit using spatial polygons.

In this example, we load the spatial polygons (an sp object) of country boundaries from a web url using the geojson_read function.

shapeurl <- "https://raw.githubusercontent.com/johan/world.geo.json/master/countries.geo.json"
WorldCountry <- geojson_read(shapeurl, what = "sp")

Map <- leaflet(WorldCountry) %>% addTiles() %>% addPolygons()
Map

Learning how to create your own spatial polygons is beyond the scope of this tutorial, but for most applications you can find a shapefile that someone else has created which contains the spatial polygon boundaries you need. Unfortunately I’m not aware of a single repository of different geo.json files, so finding the boundaries you need can sometimes require a bit of searching.

The addPolygons function will add the area boundaries for each geographic unit defined in sp file. To modify the color, or other attributes, of these polygons we can supply additional arguments:

Map <- leaflet(WorldCountry) %>% addTiles() %>% 
  addPolygons(
  fillColor = "red",
  weight = 2,
  opacity = 1,
  color = "white",
  fillOpacity = 0.7)

Map

You should recognize that the arguments within addPolygons expects you to provide a vector of length 1 (like we did in the example above) or a vector that is the same length as the number of spatial polygons (what is needed to make a choropleth). In the example below, we merge data from the Happy Planet with the countries in our sp file, and then use the variable LifeExpectancy to create a choropleth:

## Read in the Happy Planet data
happiness <- read.csv('https://remiller1450.github.io/data/HappyPlanet.csv', stringsAsFactors = FALSE)

## Join the Happy Planet data to the countries in WorldCountry
CountryHappy <- left_join(data.frame(Name = WorldCountry$name), happiness, by = c("Name" ="Country"))

## Create a color palette (using the "magma" color scheme)
pal <- colorBin("magma", domain = CountryHappy$LifeExpectancy)


## Use the palette and LifeExpectancy to color the spatial polygons
Map <- leaflet(WorldCountry) %>% addTiles() %>% 
  addPolygons(
  fillColor = pal(CountryHappy$LifeExpectancy),
  weight = 2,
  opacity = 1,
  color = "white",
  fillOpacity = 0.7)
Map

In this example you should notice a few things:

  1. We use left_join because every spatial polygon (country) needs to have a fillColor.
  2. Not every spatial polygon defined in WorldCountry had a matching entry in the Happy Planet data, but addPolygons will use grey to color spatial polygons with an NA value for their fill color.
  3. We must supply fill color via a color palette function, here we create this function using colorBin.

Highlights

To add interactivity to our map we can use the highlight argument to indicate when the mouse is hovering over a particular spatial polygon.

Map <- leaflet(WorldCountry) %>% addTiles() %>% 
  addPolygons(
    fillColor = pal(CountryHappy$LifeExpectancy),
    weight = 2,
    opacity = 1,
    color = "white",
    fillOpacity = 0.7,
    highlight = highlightOptions(
                   weight = 3,
                   color = "grey",
                   fillOpacity = 0.7,
                   bringToFront = TRUE))

Map

Labels and Popups

In addition to adding highlights, we can add labels and popups to our spatial polygons.

myLabels <- paste("<strong>", CountryHappy$Name, "</strong>", "<br/>", 
                   "Life Expectancy:", CountryHappy$LifeExpectancy)
myPopups <- paste("Happy Planet Rank", CountryHappy$HPIRank)

Map <- leaflet(WorldCountry) %>% addTiles() %>% 
  addPolygons(
    fillColor = pal(CountryHappy$LifeExpectancy),
    weight = 2,
    opacity = 1,
    color = "white",
    fillOpacity = 0.7,
    highlight = highlightOptions(
                   weight = 3,
                   color = "grey",
                   fillOpacity = 0.7,
                   bringToFront = TRUE),
    label = lapply(myLabels, HTML),
    popup = myPopups)

Map

In this example you should notice:

  1. The labels include HTML commands, but to get addPolygons to read the HTML commands properly we need to apply (using lapply) the function HTML from the htmltools package to each element of the vector we supply to the label argument.
  2. If we aren’t using any HTML commands, like in our popups, we don’t need to worry about telling addPolygons that we’re using HTML.

Legends

As a final step, we might consider adding a legend to our choropleth. The addLegend function will add a legend as a new layer:

myLabels <- paste("<strong>", CountryHappy$Name, "</strong>", "<br/>", 
                   "Life Expectancy:", CountryHappy$LifeExpectancy)
myPopups <- paste("Happy Planet Rank", CountryHappy$HPIRank)

Map <- leaflet(WorldCountry) %>% addTiles() %>% 
  addPolygons(
    fillColor = pal(CountryHappy$LifeExpectancy),
    weight = 2,
    opacity = 1,
    color = "white",
    fillOpacity = 0.7,
    highlight = highlightOptions(weight = 3,
                   color = "grey",
                   fillOpacity = 0.7,
                   bringToFront = TRUE),
    label = lapply(myLabels, HTML),
    popup = myPopups) %>%
  addLegend(pal = pal, values = CountryHappy$LifeExpectancy,
            title = "Life Expectancy", position = "bottomright")

Map

5. Practice

Question #3:

The us_cities data set in the geojsonio package contains state capitals and all cities in the United States with populations larger than 40,000. The file located at “https://raw.githubusercontent.com/PublicaMundi/MappingAPI/master/data/geojson/us-states.json” contains spatial polygon boundaries for all 50 states (and the District of Columbia and Puerto Rico). The USArrests data set in the datasets package contains violant crime rates by state.

For this question you should create a map with the following features:

  1. Spatial polygons for each state filled according to colors from the “viridis” color scheme and based upon that state’s murder rate in the USArrests data set.
  2. Black outlines for each spatial polygon.
  3. A legend in the bottom left that depicts the color scheme with an informative title.
  4. A highlight that outlines a state in white when a user hovers over it.
  5. Popups that show each state’s name in bold, along with its UrbanPop below the name on a new line.
  6. Circle markers (with radius = 2) for each city in the us_cities data set where state capitals are colored “red” and all other cities are colored yellow
  7. Labels for each city that display the city’s name (it is okay to keep the state abbreviation) and the city’s population

Hints:

shapeurl <- "https://raw.githubusercontent.com/PublicaMundi/MappingAPI/master/data/geojson/us-states.json"
UnitedStates <- geojson_read(shapeurl, what = "sp")
data("USArrests")
data("us_cities")