Fork me on GitHub

Overview

library(oidnChaRts)
geo_lines_map(data_geo_lines_map, 
              library = "leaflet")

Example data

For these examples we will use the data_geo_lines_map data.frame from the oidnChaRts library:

library(oidnChaRts)
head(data_geo_lines_map)
## # A tibble: 6 × 9
##   sender.location sender.latitude sender.longitude  receiver.location
##             <chr>           <dbl>            <dbl>              <chr>
## 1  DEU, Mockethal        50.97178         13.96013 USA, New York (NY)
## 2  DEU, Mockethal        50.97178         13.96013 USA, New York (NY)
## 3  DEU, Mockethal        50.97178         13.96013 USA, New York (NY)
## 4  DEU, Mockethal        50.97178         13.96013 USA, New York (NY)
## 5  DEU, Mockethal        50.97178         13.96013 USA, New York (NY)
## 6  DEU, Mockethal        50.97178         13.96013 USA, New York (NY)
## # ... with 5 more variables: receiver.latitude <dbl>,
## #   receiver.longitude <dbl>, date <date>, journey <chr>,
## #   number.of.letters <int>

This dataset was generated by randomly sampling a set of letters sent between European migrants to the US during the 20th Century, and adding a random offset to the date column. In general, your data should be designed to be as similar as possible to this:

  • sender.location: String describing the send location (city, house, etc)
  • sender.latitude: Decimal latitude of the send location
  • sender.longitude: Decimal longitude of the send location
  • receiver.location: String describing the send location (city, house, etc)
  • receiver.latitude: Decimal latitude of the receive locaiton
  • receiver.longitude: Decimal longitude of the receive locaiton
  • date: Date sent
  • journey: Concatenation of the send/receive locations, allows for quick de-duplication of data.
  • number.of.letters: Useful column for adding information to popups on geolines, or the line width.

Tutorial Overview

We will create interactive maps with the following data overlaid:

  • Journeys between locations will be referred to as “geolines” and are the shortest distances between the points on Earth (see great circle distance for more information).
  • End points of the journeys (send and receive locations) will be referred to as “termini”

Leaflet

Maps built with leaflet need a map to be “instantiated” and for map tiles to be set, the default map looks like this:

library(leaflet)
leaflet() %>%
  addTiles()

Leaflet isn’t able to compute great circles on its own, we therefore use the great geosphere library to compute intermediatry points along the great arcs between send and receive locations. Please note we’re using dplyr and %>% to handily extract the send/receive locations, you may need to refer to other tutorials to understand this.

library(geosphere)
library(dplyr)
geo_lines <- gcIntermediate(
  data_geo_lines_map %>%
  select(sender.longitude, sender.latitude),
  data_geo_lines_map %>%
  select(receiver.longitude, receiver.latitude),
  sp = TRUE, # SpatialLines are what Leaflet wants
  addStartEnd = TRUE, # By default this is FALSE, and would be inaccurate
  n = 50 # number of intermediate points
  )
## Individual geolines are SpatialLines, if you're interested in how they look uncomment the line below
## attributes(geo_lines[1])

The geo_lines object can now be provided to addPolylines and visualised on our map:

leaflet() %>%
  addTiles() %>%
  addPolylines(data = geo_lines)

The above visualisation is very unattractive because multiple lines have been placed upon one another, however it is fairly easy to fix this by modifying the data with dplyr.

unique_journies <- data_geo_lines_map %>%
  group_by(sender.latitude, sender.longitude, receiver.latitude, receiver.longitude) %>%
  mutate(total.letters = sum(number.of.letters)) %>%
  ungroup() %>%
  select(-number.of.letters, -date) %>%
  unique()
library(knitr)
kable(head(iris), format = "html")
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
5.1 3.5 1.4 0.2 setosa
4.9 3.0 1.4 0.2 setosa
4.7 3.2 1.3 0.2 setosa
4.6 3.1 1.5 0.2 setosa
5.0 3.6 1.4 0.2 setosa
5.4 3.9 1.7 0.4 setosa

There are 464 fewer lines when duplicates are removed,

End-points

To differentiate between send and receive points, a useful metaphor is as follows:

  • Filled circles for receive points
  • Closed circles for send points
  • Closed circles with a separate colour

Using dplyr, we can compute the groups of points described above:

library(tidyr) # using for separate
send_receive_points <- data_geo_lines_map %>%
  mutate(send = paste(sender.latitude, sender.longitude)) %>%
  mutate(receive = paste(receiver.latitude, receiver.longitude)) %>%
  select(send, receive) %>%
  unique()

both_s_and_r <- send_receive_points %>%
  filter(send %in% receive) %>%
  select(-receive) %>%
  unique()

only_send <- send_receive_points %>%
  filter(!send %in% receive) %>%
  select(-receive) %>%
  filter(!send %in% both_s_and_r$send) %>%
  unique() %>%
  separate(send, c('Latitude', 'Longitude'), sep=" ", convert = TRUE)

only_receive <- send_receive_points %>%
  filter(!receive %in% send) %>%
  select(-send) %>%
  filter(!receive %in% both_s_and_r$send) %>%
  unique() %>%
  separate(receive, c('Latitude', 'Longitude'), sep=" ", convert = TRUE)

both_s_and_r <- both_s_and_r %>%
  separate(send, c('Latitude', 'Longitude'), sep=" ", convert = TRUE)

These may then be provided to the leaflet map as different markers:

leaflet() %>%
  addTiles() %>%
  addPolylines(data = geo_lines, color = "#decbe4", opacity = 0.2) %>%
  addCircleMarkers(
    data = only_send,
    fill = FALSE,
    stroke = TRUE,
    # fillColor = "red",
    color = "#1b9e77",
    radius = 4,
    weight = 2
  ) %>%
  addCircleMarkers(
    data = only_receive,
    fill = TRUE,
    stroke = FALSE,
    fillColor = "#d95f02",
    color = "#d95f02",
    fillOpacity = 0.7,
    opacity = 0.7,
    radius = 5
  ) %>%
  addCircleMarkers(
    data = both_s_and_r,
    fill = TRUE,
    stroke = FALSE,
    fillColor = "#7570b3",
    color = "#7570b3",
    fillOpacity = 0.7,
    opacity = 0.7,
    radius = 5
  )

Plotly

Plotly provides the ability to create a variety of maps, and is often a good choice of library because it provides a number of “on-screen” tools. However, due to a Null Island issue Plotly cannot be used to create effective geolines maps as of January 2017. For the interested party, the issue has been raised here - https://github.com/ropensci/plotly/issues/731.

However, it is still useful to cover the steps required to build such a chart. We first initialise a plotly-geo object with plot_geo, and as is generally the case with plotly objects, use layout to modify the map projection:

library(plotly)
plot_geo() %>%
  layout(geo = list(projection = list(type = "mercator")))

Geolines are added to plotly maps via the add_segments function, however note the additional lines to Null Island just off west coast of Africa

plot_geo() %>%
  layout(geo = list(projection = list(type = "mercator"))) %>%
  add_segments(data = data_geo_lines_map,
               x = ~sender.longitude, xend = ~receiver.longitude,
               y = ~sender.latitude, yend = ~receiver.latitude)

highcharts

Highcharts provides support for mapping! The following minimal example shows how to create a world map using

# library(highcharter)
# library(dplyr)
# library(httr)
# library(jsonlite)
# 
# world <- content(GET("http://code.highcharts.com/mapdata/custom/world-palestine-highres.geo.json"))
# # is text
# world <- fromJSON(world, simplifyVector = FALSE)
# 
# highchart(type = "map") %>%
#   hc_chart(backgroundColor = "#161C20") %>% 
#   hc_add_series(mapData = world, showInLegend = FALSE, nullColor = "#424242",
#                 borderWidth = 0)
# hcmap("countries/us/us-ca-all") %>%
#   hc_title(text = "California")