Module 3.2

Leaflet Maps

Prework

install.packages(c("states", "leaflet", "sf", "htmltools))

Overview

This module is going to introduce you to how to make maps with markers and pop-ups using leaflet. Markers are icons or symbols that show where something is located. Pop-ups are fields that display information about a location on a map. Together, pop-ups and markers allow you to show information related to a particular point or feature on a map without having to navigate away from the current view.

Markers and pop-ups can be used to display information such as an address or the name of a city or town. You can customize how pop-ups look, choose what data to display in them. As you get more advanced, you can do even more cool things like link them to other pages or external websites.

Working with UCDP data

Our running example in this module is going to involve mapping conflict events for Yemen from the Uppsala Conflict Data Program UCDP. So while we will be building on some of our earlier knowledge, this module is going to depart a bit from previous ones in that we are using an entirely new dataset. Specifically, we will be using the UCDP georeferenced event dataset, which you can download from here.

After you have downloaded the data and saved it in your modules folder, go ahead and, read it in and have a look at its contents with gplimpse().

Note

I am working with a truncated version of the UCDP GED data so that I can upload everything to GitHub. Consequently, my glimpse() output may look slightly different from yours if you are using the full data.

library(readr)
library(dplyr)

ged_data <- read_csv("data/GEDEvent_v22_1.csv")

glimpse(ged_data)
Rows: 16,609
Columns: 49
$ id                <dbl> 412700, 413023, 412909, 374227, 374229, 374396, 3749…
$ relid             <chr> "IRQ-2021-1-524-145", "IRQ-2021-1-524-143", "IRQ-202…
$ year              <dbl> 2021, 2021, 2021, 2021, 2021, 2021, 2021, 2021, 2021…
$ active_year       <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ code_status       <chr> "Clear", "Clear", "Clear", "Clear", "Clear", "Clear"…
$ type_of_violence  <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ conflict_dset_id  <dbl> 259, 259, 259, 333, 333, 333, 333, 333, 333, 333, 33…
$ conflict_new_id   <dbl> 259, 259, 259, 333, 333, 333, 333, 333, 333, 333, 33…
$ conflict_name     <chr> "Iraq: Government", "Iraq: Government", "Iraq: Gover…
$ dyad_dset_id      <dbl> 524, 524, 524, 735, 735, 735, 735, 735, 735, 735, 73…
$ dyad_new_id       <dbl> 524, 524, 524, 735, 735, 735, 735, 735, 735, 735, 73…
$ dyad_name         <chr> "Government of Iraq - IS", "Government of Iraq - IS"…
$ side_a_dset_id    <dbl> 116, 116, 116, 130, 130, 130, 130, 130, 130, 130, 13…
$ side_a_new_id     <dbl> 116, 116, 116, 130, 130, 130, 130, 130, 130, 130, 13…
$ side_a            <chr> "Government of Iraq", "Government of Iraq", "Governm…
$ side_b_dset_id    <dbl> 234, 234, 234, 303, 303, 303, 303, 303, 303, 303, 30…
$ side_b_new_id     <dbl> 234, 234, 234, 303, 303, 303, 303, 303, 303, 303, 30…
$ side_b            <chr> "IS", "IS", "IS", "Taleban", "Taleban", "Taleban", "…
$ number_of_sources <dbl> 15, 5, 8, 1, 1, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, …
$ source_article    <chr> "\"BBC News,2021-08-26,Explosion at Kabul airport\";…
$ source_office     <chr> "BBC News;ShamshadNews on Twitter;Reuters News;Assoc…
$ source_date       <chr> "2021-08-26;2021-08-26;2021-08-27;2021-08-27;2021-08…
$ source_headline   <chr> "Explosion at Kabul airport;At least 11 people kille…
$ source_original   <chr> "US officials; Taliban spokesman Zabihullah Mujahid;…
$ where_prec        <dbl> 1, 1, 1, 1, 3, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2…
$ where_coordinates <chr> "Kabul international airport", "Jalalabad town", "Ka…
$ where_description <chr> "Kabul airport (Abbey gate entrance)", "Police Distr…
$ adm_1             <chr> "Kabul province", "Nangarhar province", "Kabul provi…
$ adm_2             <chr> "Kabul district", "Jalalabad district", "Kabul distr…
$ latitude          <dbl> 34.56444, 34.42884, 34.53109, 31.61180, 31.64045, 31…
$ longitude         <dbl> 69.21722, 70.45575, 69.16280, 65.70579, 65.39759, 64…
$ geom_wkt          <chr> "POINT (69.2172222 34.5644444)", "POINT (70.45575 34…
$ priogrid_gid      <dbl> 179779, 179061, 179779, 175452, 175451, 175449, 1790…
$ country           <chr> "Afghanistan", "Afghanistan", "Afghanistan", "Afghan…
$ country_id        <dbl> 700, 700, 700, 700, 700, 700, 700, 700, 700, 700, 70…
$ region            <chr> "Asia", "Asia", "Asia", "Asia", "Asia", "Asia", "Asi…
$ event_clarity     <dbl> 1, 1, 1, 1, 2, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ date_prec         <dbl> 1, 1, 1, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ date_start        <dttm> 2021-08-26, 2021-08-28, 2021-08-29, 2021-01-01, 202…
$ date_end          <dttm> 2021-08-26, 2021-08-28, 2021-08-29, 2021-01-01, 202…
$ deaths_a          <dbl> 13, 0, 0, 1, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ deaths_b          <dbl> 1, 2, 0, 0, 7, 13, 0, 7, 6, 18, 13, 12, 12, 14, 14, …
$ deaths_civilians  <dbl> 141, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ deaths_unknown    <dbl> 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ best              <dbl> 183, 2, 10, 1, 7, 13, 4, 7, 6, 18, 13, 12, 12, 14, 1…
$ high              <dbl> 184, 3, 10, 1, 16, 12, 4, 16, 16, 18, 12, 13, 13, 14…
$ low               <dbl> 171, 0, 9, 1, 7, 13, 4, 6, 7, 18, 13, 12, 12, 14, 14…
$ gwnoa             <dbl> 645, 645, 645, 700, 700, 700, 700, 700, 700, 700, 70…
$ gwnob             <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …

One thing we have to manage right away is the specific country codes used in these data. There is a whole art to managing international standards organization (ISO) codes which we have already touched on in our discussion of the countrycodes package. For the pruposes of this course, it is enough to be aware of the fact that country-level data sets use different coding systems and that this poses a small hurdle to the visualization and analysis of country-level data.

For this analysis, we are going to want to filter by country and specifically we want conflict data for Yemen. According to the UCDP codebook, this dataset uses Gleditsch-Ward (GW) country identifiers. One option could be to simply look up the GW country code for Yemen. But there is also a nice package developed called states. It has a function called sfind() that we can use to find the relevant country code.

library(states)

sfind("Yemen")[1:6]
    list ccode code3c                   country_name      start        end
169   GW   678    YEM Yemen (Arab Republic of Yemen) 1918-10-30 9999-12-31
170   GW   680    YPR    Yemen, People's Republic of 1967-11-30 1990-05-21
428  COW   678    YAR            Yemen Arab Republic 1926-09-02 1990-05-21
429  COW   679    YEM                          Yemen 1990-05-22 9999-12-31
430  COW   680    YPR        Yemen People's Republic 1967-11-30 1990-05-21

Here we see that the GW code for Yemen is 678. Although there are many listings for Yemen, we know that it is the right code because the Arab Republic of Yemen (our other option for a GW Yemen code) ceased to exist in 1990 following its unification with the People’s Democratic Republic of Yemen.

Let’s go ahead and use our newly discovered country code to wrangle some data for Yemen. We will filter for Yemen’s country ID and events from 2021 and, to keep things manageable, we will only include events that started before March 1 2021.

Looking again at the codebook, we see there are codings for how certain the coders were regarding where an event occurred and that they also coded for the quality of the reporting. We will keep events with a location precision score less than 3 and an event clarity score equal to 1.

From there we will create a new variable deaths that sums the different categories of deaths (side a, side b, civilian and unknown). Then we will select all of these variables and the location coordinates and use the st_as_sf() function from the sf package to conver the coordinates into simple features objects.

ged_yemen <- ged_data |> 
  filter(
    country_id == 678, #gw country code
    year == 2021,
    date_start < "2021-03-01", 
    where_prec < 3, # keep if certain where event occurred
    event_clarity == 1, # keep if event reporting is clear
      ) |> 
  mutate(deaths = deaths_a + deaths_b + deaths_civilians + deaths_unknown) |>
  select(event_id = id,
         country_id,
         date = date_start,
         gov_deaths = deaths_a, 
         rebel_deaths = deaths_b, 
         civilian_deaths = deaths_civilians, 
         deaths, 
         place = where_coordinates,
         latitude, 
         longitude) |>
  sf::st_as_sf(coords = c("longitude", "latitude")) 

glimpse(ged_yemen)
Rows: 83
Columns: 9
$ event_id        <dbl> 378022, 378424, 378425, 378904, 378908, 378942, 386614…
$ country_id      <dbl> 678, 678, 678, 678, 678, 678, 678, 678, 678, 678, 678,…
$ date            <dttm> 2021-01-06, 2021-01-14, 2021-01-20, 2021-01-22, 2021-…
$ gov_deaths      <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, …
$ rebel_deaths    <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ civilian_deaths <dbl> 6, 1, 1, 1, 1, 1, 1, 1, 5, 1, 7, 1, 1, 1, 3, 1, 1, 0, …
$ deaths          <dbl> 6, 1, 1, 1, 1, 1, 1, 1, 5, 1, 7, 1, 1, 1, 3, 1, 1, 2, …
$ place           <chr> "Al Ḩaymah as Suflá area", "Wādī al Ḩājib village", "A…
$ geometry        <POINT> POINT (44.06 13.70564), POINT (44.04394 13.74153), P…

Make a leaflet map

Now that we have our data, let’s make our first pop-up map. To do this we are going to be using the leaflet package. Leaflet is a really popular JavaScript library for interactive maps. To get started with leaflet, we’ll make a really simple map with one marker that says “First conflict event.”

Plot a single marker

Let’s start with a really simple hypothetical example. Let’s say we want to plot one conflict event that we have the coordinates for and label it “First conflict event.” To do this, we would first call the leaflet()function. From there will add default street map tiles with addTiles() and then our single pop-up marker with addMarkers().

library(leaflet)

leaflet() |>
  addTiles() |>  # Add default OpenStreetMap map tiles
  addMarkers(lng = 45.46916, lat = 14.14912, label = "First conflict event")

Plot some conflict events from Yemen

Now that you have the hang of it, we can move on to plotting some conflict events from our data frame. Again we will call leaflet() but this time we will add data = ged_yemen as an argument. We will also use setView() to center the map on Yemen’s capital Sana’a. We include two arguments for addMarkers. popup = ~as.character(deaths) displays the number of deaths when the user clicks on the marker and label = ~place displays the name of the town that the coordinates correspond to.

Note

Note that arguments for the popup = argument take the form of a one-sided formula, meaning that they require a tilde (~) as a prefix. Scroll to the bottom of this page for a more detailed explanation of the ~ notation in this context.

leaflet(data = ged_yemen) |> # map points in ged_yemen data frame
  addTiles() |> # add default tile
  setView(lng = 44.1910, lat = 15.3694, zoom = 6) |> # Sana'a coordinates
  addMarkers(
    popup = ~as.character(deaths), # when user clicks, show deaths
    label = ~place # when user hovers, show town
    )

Customize your leaflet map

Next, let’s do some customization to make our map look amazing. We will talk about changing the icon style, customized the information displayed in the icon and adding base maps.

Use an awesome icon

We can dress our leaflet map up a little bit with the addAwesomeMarkers() function which allows us to use the glyphicons, font awesome and ionicons libraries.

First we will use awesomeIcons() to store the icon we want to use. Here we choose “ios-close” from the ionic library. We will say that we want the icon colorto be black and the surrounding marker color to be red. Then we call addAwesomeMarkers() and specify icon = icon to call the red and black ionic marker.

# save icon
icon <- awesomeIcons(
  icon = "ios-close",
  iconColor = "black",
  markerColor = "red", 
  library = "ion" 
)

# Build map
leaflet(data = ged_yemen) |>   
  addTiles() |> 
  setView(lng = 44.1910, lat = 15.3694, zoom = 6) |> # Sana'a coordinates
  addAwesomeMarkers(
    icon = icon, 
    popup = ~as.character(deaths), 
    label = ~place
    )

Change content of the popup

Let’s say we want to add more information to our pop-up when the user clicks on it. Instead of just showing the total number of deaths, we also want to show the date of the event and the breakdown of government deaths, rebel deaths and civilian deaths.

The easiest way to do this is going to be to add a column to our data frame with a label associated with each event. We will use the base R sprintf() function combined with htmltools to create these labels. sprintf() returns a formatted string using values in a list. The first parameter in sprintf is the format and the second is the values that go into the format.

In this example, we are separating the lines of our label with html (
) and we are passing in a string (%s) for the date and numeric values (%.0f) for the number of deaths.

Then we call HMTL from htmltools and use lapply() to apply the function to every line in the data frame.

ged_yemen$popup_text <- sprintf(
      "Date: %s <br> 
       Total Deaths: %.0f <br> 
       Govt. Deaths: %.0f <br> 
       Rebel Deaths: %.0f <br> 
       Civilian Death: %.0f <br>",
      ged_yemen$date, 
      ged_yemen$deaths, 
      ged_yemen$gov_deaths, 
      ged_yemen$rebel_deaths,
      ged_yemen$civilian_deaths
    ) |> lapply(htmltools::HTML)

Now we can use the new labels to enhance the markers by specifying popup = ~popup_text in the addAwesomeMarkers() function.

leaflet(data = ged_yemen) |>  
  addTiles() |> 
  setView(lng = 44.1910, lat = 15.3694, zoom = 6) |> # Sana'a coordinates
  addAwesomeMarkers(
    icon = icon, 
    popup = ~popup_text, 
    label = ~place
    )

Now let’s spot check the labels.

ged_yemen |>
  filter(place == "Marib Dam")
Simple feature collection with 1 feature and 9 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 45.24417 ymin: 15.39639 xmax: 45.24417 ymax: 15.39639
CRS:           NA
# A tibble: 1 × 10
  event_id country_id date                gov_deaths rebel_deaths
*    <dbl>      <dbl> <dttm>                   <dbl>        <dbl>
1   385602        678 2021-02-26 00:00:00         34           27
# ℹ 5 more variables: civilian_deaths <dbl>, deaths <dbl>, place <chr>,
#   geometry <POINT>, popup_text <list>

Using basemaps

As a last step, let’s change the basemap. We can do this by specifying addProviderTiles() instead of addTiles() and specifying the basemap that we want to use. Here is a list of available basemaps. For this example, we are going to use “OpenTopoMap.”

leaflet(data = ged_yemen) |> # Jan and Feb  
  addProviderTiles("OpenTopoMap") |> # include name of provider here
  setView(lng = 44.1910, lat = 15.3694, zoom = 6) |> # Sana'a coordinates
  addAwesomeMarkers(
    icon = icon, 
    popup = ~popup_text, 
    label = ~place
    )

Now we can see a bit more about the topography, which could be super-useful for conflict analysis.