Map Plots In R In A Tidyverse Way
I show how you can plot your own map in R using a few lines of code using a pipe-based workflow
I show how you can plot your own map in R using a few lines of code using a pipe-based workflow. Several powerful functions of the sf
packages are presented.
Analysis
This week I worked on a project for which I needed to create a map plot with some statistics for selected European countries; I was unfamiliar with this kind of plots, so I searched online for possible solutions. I like the tidyverse workflow, so I naturally looked for any tutorials using this style. The first hit was informative, but it didn’t have a high resolution map for Europe. Furthermore, I like to be able to use any custom map, so I searched for ways to import a custom map.
naturalearthdata.com provides many open-source maps. I decided to select the world map with country borders on a 1:10m scale (can be found here).
library(sf) # For handling geospatial data
library(ggplot2) # Plotting library
library(dplyr) # Data manipulation in tidyverse way
library(ggthemes) # Additional themese for the ggplot2 library
library(knitr) # Nice tables for this document
# This will create a natural-earth subfolder with the map data in the data folder.
if (!file.exists("data/natural-earth")) {
tmp_file <- tempfile(fileext=".zip")
download.file("https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_0_countries.zip",
tmp_file)
unzip(tmp_file, exdir = "data/natural-earth")
}
Importing these maps, however, was not straightforward to me. These
lecture
slides
provides a way to import custom maps, but the syntax of the sp
package
seems very untuitive with S4
objects for the polygons. Furthermore,
the SpatialDataFrame
objects do not support a pipe-based workflow.
However, this tutorial presents how
the modern sf
package can be used to manipulate, plot and import
spatial data in a tidyverse manner.
Importing our world map is as easy as
map_data <- st_read("data/natural-earth/", "ne_10m_admin_0_countries")
## Reading layer `ne_10m_admin_0_countries' from data source `Map-Plotting/data/natural-earth' using driver `ESRI Shapefile'
## Simple feature collection with 255 features and 94 fields
## geometry type: MULTIPOLYGON
## dimension: XY
## bbox: xmin: -180 ymin: -90 xmax: 180 ymax: 83.6341
## epsg (SRID): 4326
## proj4string: +proj=longlat +datum=WGS84 +no_defs
The map_data
uses data.frame
s for its features and saves the
geometric features as a list in the column geometry
. We can now easily
explore the data in map_data
, e.g.,
features_map_data <- map_data %>%
as_tibble() %>%
select(-geometry) %>%
head(10)
kable(features_map_data)
featurecla | scalerank | LABELRANK | SOVEREIGNT | SOV_A3 | ADM0_DIF | LEVEL | TYPE | ADMIN | ADM0_A3 | GEOU_DIF | GEOUNIT | GU_A3 | SU_DIF | SUBUNIT | SU_A3 | BRK_DIFF | NAME | NAME_LONG | BRK_A3 | BRK_NAME | BRK_GROUP | ABBREV | POSTAL | FORMAL_EN | FORMAL_FR | NAME_CIAWF | NOTE_ADM0 | NOTE_BRK | NAME_SORT | NAME_ALT | MAPCOLOR7 | MAPCOLOR8 | MAPCOLOR9 | MAPCOLOR13 | POP_EST | POP_RANK | GDP_MD_EST | POP_YEAR | LASTCENSUS | GDP_YEAR | ECONOMY | INCOME_GRP | WIKIPEDIA | FIPS_10_ | ISO_A2 | ISO_A3 | ISO_A3_EH | ISO_N3 | UN_A3 | WB_A2 | WB_A3 | WOE_ID | WOE_ID_EH | WOE_NOTE | ADM0_A3_IS | ADM0_A3_US | ADM0_A3_UN | ADM0_A3_WB | CONTINENT | REGION_UN | SUBREGION | REGION_WB | NAME_LEN | LONG_LEN | ABBREV_LEN | TINY | HOMEPART | MIN_ZOOM | MIN_LABEL | MAX_LABEL | NE_ID | WIKIDATAID | NAME_AR | NAME_BN | NAME_DE | NAME_EN | NAME_ES | NAME_FR | NAME_EL | NAME_HI | NAME_HU | NAME_ID | NAME_IT | NAME_JA | NAME_KO | NAME_NL | NAME_PL | NAME_PT | NAME_RU | NAME_SV | NAME_TR | NAME_VI | NAME_ZH |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Admin-0 country | 5 | 2 | Indonesia | IDN | 0 | 2 | Sovereign country | Indonesia | IDN | 0 | Indonesia | IDN | 0 | Indonesia | IDN | 0 | Indonesia | Indonesia | IDN | Indonesia | NA | Indo. | INDO | Republic of Indonesia | NA | Indonesia | NA | NA | Indonesia | NA | 6 | 6 | 6 | 11 | 260580739 | 17 | 3028000 | 2017 | 2010 | 2016 | 4. Emerging region: MIKT | 4. Lower middle income | -99 | ID | ID | IDN | IDN | 360 | 360 | ID | IDN | 23424846 | 23424846 | Exact WOE match as country | IDN | IDN | -99 | -99 | Asia | Asia | South-Eastern Asia | East Asia & Pacific | 9 | 9 | 5 | -99 | 1 | 0 | 1.7 | 6.7 | 1159320845 | Q252 | إندونيسيا | ইন্দোনেশিয়া | Indonesien | Indonesia | Indonesia | Indonésie | Ινδονησία | इंडोनेशिया | Indonézi | a Indonesia | Indonesia | インドネシア | 인도네시아 | Indonesië | Indonezja | Indonési | a Индонезия | Indonesie | n Endonezya | Indonesia | 印度尼西亚 |
Admin-0 country | 5 | 3 | Malaysia | MYS | 0 | 2 | Sovereign country | Malaysia | MYS | 0 | Malaysia | MYS | 0 | Malaysia | MYS | 0 | Malaysia | Malaysia | MYS | Malaysia | NA | Malay. | MY | Malaysia | NA | Malaysia | NA | NA | Malaysia | NA | 2 | 4 | 3 | 6 | 31381992 | 15 | 863000 | 2017 | 2010 | 2016 | 6. Developing region | 3. Upper middle income | -99 | MY | MY | MYS | MYS | 458 | 458 | MY | MYS | 23424901 | 23424901 | Exact WOE match as country | MYS | MYS | -99 | -99 | Asia | Asia | South-Eastern Asia | East Asia & Pacific | 8 | 8 | 6 | -99 | 1 | 0 | 3.0 | 8.0 | 1159321083 | Q833 | ماليزيا | মালয়েশিয়া | Malaysia | Malaysia | Malasia | Malaisie | Μαλαισία | मलेशिया | Malajzia | Malaysia | Malesia | マレーシア | 말레이시아 | Maleisië | Malezja | Malásia | Малайзия | Malaysia | Malezya | Malaysia | 马来西亚 |
Admin-0 country | 6 | 2 | Chile | CHL | 0 | 2 | Sovereign country | Chile | CHL | 0 | Chile | CHL | 0 | Chile | CHL | 0 | Chile | Chile | CHL | Chile | NA | Chile | CL | Republic of Chile | NA | Chile | NA | NA | Chile | NA | 5 | 1 | 5 | 9 | 17789267 | 14 | 436100 | 2017 | 2002 | 2016 | 5. Emerging region: G20 | 3. Upper middle income | -99 | CI | CL | CHL | CHL | 152 | 152 | CL | CHL | 23424782 | 23424782 | Exact WOE match as country | CHL | CHL | -99 | -99 | South America | Americas | South America | Latin America & Caribbean | 5 | 5 | 5 | -99 | 1 | 0 | 1.7 | 6.7 | 1159320493 | Q298 | تشيلي | চিলি | Chile | Chile | Chile | Chili | Χιλή | चिली | Chile | Chili | Cile | チリ | 칠레 | Chili | Chile | Chile | Чили | Chile | Şili | Chile | 智利 |
Admin-0 country | 0 | 3 | Bolivia | BOL | 0 | 2 | Sovereign country | Bolivia | BOL | 0 | Bolivia | BOL | 0 | Bolivia | BOL | 0 | Bolivia | Bolivia | BOL | Bolivia | NA | Bolivia | BO | Plurinational State of Bolivia | NA | Bolivia | NA | NA | Bolivia | NA | 1 | 5 | 2 | 3 | 11138234 | 14 | 78350 | 2017 | 2001 | 2016 | 5. Emerging region: G20 | 4. Lower middle income | -99 | BL | BO | BOL | BOL | 068 | 068 | BO | BOL | 23424762 | 23424762 | Exact WOE match as country | BOL | BOL | -99 | -99 | South America | Americas | South America | Latin America & Caribbean | 7 | 7 | 7 | -99 | 1 | 0 | 3.0 | 7.5 | 1159320439 | Q750 | بوليفيا | বলিভিয়া | Bolivien | Bolivia | Bolivia | Bolivie | Βολιβία | बोलिविया | Bolívia | Bolivia | Bolivia | ボリビア | 볼리비아 | Bolivia | Boliwia | Bolívia | Боливия | Bolivia | Bolivya | Bolivia | 玻利維亞 |
Admin-0 country | 0 | 2 | Peru | PER | 0 | 2 | Sovereign country | Peru | PER | 0 | Peru | PER | 0 | Peru | PER | 0 | Peru | Peru | PER | Peru | NA | Peru | PE | Republic of Peru | NA | Peru | NA | NA | Peru | NA | 4 | 4 | 4 | 11 | 31036656 | 15 | 410400 | 2017 | 2007 | 2016 | 5. Emerging region: G20 | 3. Upper middle income | -99 | PE | PE | PER | PER | 604 | 604 | PE | PER | 23424919 | 23424919 | Exact WOE match as country | PER | PER | -99 | -99 | South America | Americas | South America | Latin America & Caribbean | 4 | 4 | 4 | -99 | 1 | 0 | 2.0 | 7.0 | 1159321163 | Q419 | بيرو | পেরু | Peru | Peru | Perú | Pérou | Περού | पेरू | Peru | Peru | Perù | ペルー | 페루 | Peru | Peru | Peru | Перу | Peru | Peru | Peru | 秘鲁 |
Admin-0 country | 0 | 2 | Argentina | ARG | 0 | 2 | Sovereign country | Argentina | ARG | 0 | Argentina | ARG | 0 | Argentina | ARG | 0 | Argentina | Argentina | ARG | Argentina | NA | Arg. | AR | Argentine Republic | NA | Argentina | NA | NA | Argentina | NA | 3 | 1 | 3 | 13 | 44293293 | 15 | 879400 | 2017 | 2010 | 2016 | 5. Emerging region: G20 | 3. Upper middle income | -99 | AR | AR | ARG | ARG | 032 | 032 | AR | ARG | 23424747 | 23424747 | Exact WOE match as country | ARG | ARG | -99 | -99 | South America | Americas | South America | Latin America & Caribbean | 9 | 9 | 4 | -99 | 1 | 0 | 2.0 | 7.0 | 1159320331 | Q414 | الأرجنتين | আর্জেন্টিনা | Argentinien | Argentina | Argentina | Argentine | Αργεντινή | अर्जेण्टीना | Argentí | na Argentina | Argentina | アルゼンチン | 아르헨티나 | Argentinië | Argentyna | Argenti | na Аргентина | Argentin | a Arjantin | Argentina | 阿根廷 |
Admin-0 country | 3 | 3 | United Kingdom | GB1 | 1 | 2 | Dependency | Dhekelia Sovereign Base Area | ESB | 0 | Dhekelia Sovereign Base Area | ESB | 0 | Dhekelia Sovereign Base Area | ESB | 0 | Dhekelia | Dhekelia | ESB | Dhekelia | NA | Dhek. | DH | NA | NA | NA | U.K. Base | NA | Dhekelia Sovereign Base Area | NA | 6 | 6 | 6 | 3 | 7850 | 5 | 314 | 2013 | -99 | 2013 | 2. Developed region: nonG7 | 2. High income: nonOECD | -99 | -99 | -99 | -99 | -99 | -99 | -099 | -99 | -99 | -99 | -99 | No WOE equivalent. | GBR | ESB | -99 | -99 | Asia | Asia | Western Asia | Europe & Central Asia | 8 | 8 | 5 | 3 | -99 | 0 | 6.5 | 11.0 | 1159320709 | Q9206745 | ديكيليا كانتونمنت | দেখেলিয়া ক্যান্টনমেন্ | ট Dekelia | Dhekelia Cantonment | Dekelia | Dhekelia | Ντεκέλια Κάντονμεντ | ढेकेलिया छावनी | Dekéli | a Dhekelia Cantonment | Base di Dheke | lia デケリア | 데켈리아 지 | 역 Dhekelia Cantonme | nt Dhekelia | Dekeli | a Декелия | Dhekeli | a Dhekelia Kantonu | Căn cứ quân sự Dhekelia | NA |
Admin-0 country | 6 | 5 | Cyprus | CYP | 0 | 2 | Sovereign country | Cyprus | CYP | 0 | Cyprus | CYP | 0 | Cyprus | CYP | 0 | Cyprus | Cyprus | CYP | Cyprus | NA | Cyp. | CY | Republic of Cyprus | NA | Cyprus | NA | NA | Cyprus | NA | 1 | 2 | 3 | 7 | 1221549 | 12 | 29260 | 2017 | 2001 | 2016 | 6. Developing region | 2. High income: nonOECD | -99 | CY | CY | CYP | CYP | 196 | 196 | CY | CYP | -90 | 23424994 | WOE lists as subunit of united Cyprus | CYP | CYP | -99 | -99 | Asia | Asia | Western Asia | Europe & Central Asia | 6 | 6 | 4 | -99 | 1 | 0 | 4.5 | 9.5 | 1159320533 | Q229 | قبرص | সাইপ্রাস | Republik Zypern | Cyprus | Chipre | Chypre | Κύπρος | साइप्रस | Ciprus | Siprus | Cipro | キプロス | 키프로스 | Cyprus | Cypr | Chipre | Кипр | Cypern | Kıbrıs Cumhuriyeti | Cộng hòa Síp | 賽普勒斯 |
Admin-0 country | 0 | 2 | India | IND | 0 | 2 | Sovereign country | India | IND | 0 | India | IND | 0 | India | IND | 0 | India | India | IND | India | NA | India | IND | Republic of India | NA | India | NA | NA | India | NA | 1 | 3 | 2 | 2 | 1281935911 | 18 | 8721000 | 2017 | 2011 | 2016 | 3. Emerging region: BRIC | 4. Lower middle income | -99 | IN | IN | IND | IND | 356 | 356 | IN | IND | 23424848 | 23424848 | Exact WOE match as country | IND | IND | -99 | -99 | Asia | Asia | Southern Asia | South Asia | 5 | 5 | 5 | -99 | 1 | 0 | 1.7 | 6.7 | 1159320847 | Q668 | الهند | ভারত | Indien | India | India | Inde | Ινδία | भारत | India | India | India | インド | 인도 | India | Indie | Índia | Индия | Indien | Hindistan | Ấn Độ | 印度 |
Admin-0 country | 0 | 2 | China | CH1 | 1 | 2 | Country | China | CHN | 0 | China | CHN | 0 | China | CHN | 0 | China | China | CHN | China | NA | China | CN | People's Republic of China | NA | China | NA | NA | China | NA | 4 | 4 | 4 | 3 | 1379302771 | 18 | 21140000 | 2017 | 2010 | 2016 | 3. Emerging region: BRIC | 3. Upper middle income | -99 | CH | CN | CHN | CHN | 156 | 156 | CN | CHN | 23424781 | 23424781 | Exact WOE match as country | CHN | CHN | -99 | -99 | Asia | Asia | Eastern Asia | East Asia & Pacific | 5 | 5 | 5 | -99 | 1 | 0 | 1.7 | 5.7 | 1159320471 | Q148 | جمهورية الصين الشعبية | গণপ্রজাতন্ত্রী চীন | Volksrepublik China | People's Republic of China | República Popular China | République populaire de Chine | Λαϊκή Δημοκρατία της Κίνας | चीनी जनवादी गणराज् | य Kína | Republik Rakyat Tiongko | k Cina | 中華人民共和国 | 중화인민공화국 | Volksrepubliek China | Chińska Republika Ludowa | China | Китайская Народная Республика | Kina | Çin Halk Cumhuriyeti | Cộng hòa Nhân dân Trung Hoa | 中华人民共和国 |
For this tutorial we want to focus on a European countries, hence we
need to filter the data to only contain the european countries’ info.
Fortunately, the map_data
contains a feature CONTINTENT
, so we can
easily filter out the unwanted countries.
europe_map_data <- map_data %>%
select(NAME, CONTINENT, SUBREGION, POP_EST) %>%
filter(CONTINENT == "Europe")
Lets try to plot a map of European countries. New versions of ggplot2
contain a function geom_sf
which supports plotting sf
objects
directly, so lets try it…
ggplot(europe_map_data) + geom_sf() +
theme_minimal()
That does not seem to work… the reason is that, even though we removed
the data of non European countries, we never changed the bbox
setting
of our data. The bbox
object sets the longitude and latitude range for
our plot, which is still for the whole europe. To change this we can use
the st_crop
function as
europe_map_data <- europe_map_data %>%
st_crop(xmin=-25, xmax=55, ymin=35, ymax=71)
## although coordinates are longitude/latitude, st_intersection assumes that they are planar
## Warning: attribute variables are assumed to be spatially constant
## throughout all geometries
ggplot(europe_map_data) + geom_sf() +
theme_minimal()
If you’re familiar with the ggplot2
workflow, it is now easy to
construct the aesthetic mappings like you’re used to. Our map_data
contains a feature SUBREGION
and Europe is divided into Northern,
Eastern, Southern and Western Europe. We can easily visualise this in
our European map as
ggplot(europe_map_data) + geom_sf(aes(fill=SUBREGION)) +
theme_minimal()
The sf
has many in-built functions; one of these functions is
st_area
which can be used to compute the area of polygons. The
population density of each country can be easily plotted by
europe_map_data <- europe_map_data %>%
mutate(area = as.numeric(st_area(.))) %>%
mutate(pop_density = POP_EST / area)
ggplot(europe_map_data) + geom_sf(aes(fill=pop_density)) +
theme_minimal() +
scale_fill_continuous_tableau(palette = "Green")
Using aggregating functions of the tidyverse
package is also
straight-forward. Lets create a similar population density plot but
instead for each subregion of Europe.
subregion_data <- europe_map_data %>%
group_by(SUBREGION) %>%
summarise(area = sum(area),
pop_est = sum(POP_EST)) %>%
ungroup() %>%
mutate(pop_density = pop_est / area)
ggplot(subregion_data) + geom_sf(aes(fill=pop_density)) +
theme_minimal() +
scale_fill_continuous_tableau(palette = "Green")
As a last exercise lets find the centroid for each country.
# First get all centroids of each European country
get_coordinates = function(data) {
return_data <- data %>%
st_geometry() %>%
st_centroid() %>%
st_coordinates() %>%
as_data_frame()
}
europe_centres <- europe_map_data %>%
group_by(NAME) %>%
do(get_coordinates(.))
europe_map_data <- europe_map_data %>%
left_join(europe_centres, by="NAME")
Actually, I only want to see the centroid of the Netherlands…
netherlands_map_data = europe_map_data %>%
filter(NAME == "Netherlands") %>%
st_crop(xmin=1, xmax=10, ymin=50, ymax=55)
## although coordinates are longitude/latitude, st_intersection assumes that they are planar
## Warning: attribute variables are assumed to be spatially constant
## throughout all geometries
ggplot(netherlands_map_data) + geom_sf() +
geom_point(aes(x=X, y=Y, colour="red")) +
theme_minimal()
Setup
The analysis of this tutorial is performed using R version 3.5.1. To use
the st_crop
function from the sf
package version 0.6.3 is needed.
geom_sf
also requires a recent version of ggplot2
.