This tutorial introduces geo-spatial data visualization in R. The entire R markdown document for this tutorial can be downloaded here.

This tutorial is based on R. If you have not installed R or are new to it, you will find an introduction to and more information how to use R here. For this tutorials, we need to install certain packages from an R library so that the scripts shown below are executed without errors. Before turning to the code below, please install the packages by running the code below this paragraph. If you have already installed the packages mentioned below, then you can skip ahead and ignore this section. To install the necessary packages, simply run the following code.


The installation of the packages may take some time (between 5 and 10 minutes!). The installation will also require data from your plan - it is thus recommendable to be logged into an institutional network that has a decent connectivity and download rate (e.g., a university network). The issue is caused by the fact that some of the required packages contain shape files that are render the packages to be rather over-proportionally big in comparison to other R packages. Also, due to their size, the shape files and the packages that contain them they take time to download

# install packages
# install package from github
devtools::install_github("dkahle/ggmap", ref = "tidyup")
# install klippy for copy-to-clipboard button in code chunks
# set options
options(stringsAsFactors = F)         # no automatic data transformation
options("scipen" = 100, "digits" = 4) # suppress math annotation
op <- options(gvis.plot.tag='chart')  # set gViz options
# load package
# activate klippy for copy-to-clipboard button

Depending on the maps that are used in the visualization, it may also be necessary to access other data bases. One very useful data base for maps is, of course, Google Maps. However, to access Google Maps materials, installation and setting up other pieces of software is necessary. How to get access to Google’s data is discussed below. In the following section, methods that do not require installation of software other than R.

1 Getting started with maps

The most basic way to display geospatial data is to simply download and display a map. In order to do that, we load the libraries necessary for extracting and plotting the map.

The package OpenStreetMap offers a range of maps with different features. To access the OpenStreetMap data base, it is necessary to install the package. Once the package is installed, we can simply extract the map and define the region we want to plot by defining the longitude and latitude of the upper left and lower right corner of the region we want to display. The argument minNumTiles defines the accuracy of the map, the higher the number of tiles, the higher the resolution. The type of map is defined by the type argument. The type argument defines from which server the map is extracted. Once we have extracted a map, we can plot it using the “plot” function.

# extract map
AustraliaMap <- openmap(c(-8,110),
#   type = "osm",
#   type = "esri",
    type = "nps",
# plot map

In order to obtain different map types, we change the type argument. The following options are available for type:

opt <- c("osm", "osm-bw","maptoolkit-topo", "waze", "bing", "stamen-toner", "stamen-terrain", "stamen-watercolor", "osm-german", "osm-wanderreitkarte", "mapbox")
opt2 <- c("esri", "esri-topo", "nps", "apple-iphoto", "skobbler", "hillshade", "opencyclemap", "osm-transport", "osm-public-transport", "osm-bbike", "osm-bbike-german")
opt <- data.frame(opt, opt2) %>%
  dplyr::rename(options = colnames(.)[1],
                more = colnames(.)[2])
opt %>% %>%
  head(15) %>%
  flextable() %>%
  flextable::set_table_properties(width = .5, layout = "autofit") %>%
  flextable::theme_zebra() %>%
  flextable::fontsize(size = 12) %>%
  flextable::fontsize(size = 12, part = "header") %>%
  flextable::align_text_col(align = "center") %>%
  flextable::set_caption(caption = "Map display options.")  %>%

Unfortunately, not all options work. If they do not work, then an error message is shown telling us that the number of tiles is not supported.

We can zoom in or out by either changing the “zoom” or the “minNumTiles” arguments - in both cases, the higher the number, the more fine-grained the dispalyed map. Let’s check out some examples for maps of Queensland.

# extract map
queensland1 <- openmap(c(-8,135),
    type = "osm",
queensland2 <- openmap(c(-8,135),
    type = "esri",
# plot maps
par(mfrow = c(1, 2)) # display plots in 1 row/2 columns
plot(queensland1); plot(queensland2); par(mfrow = c(1, 1)) # restore original settings

Generating maps with rworldmap and base R

Another data base that is very useful when certain maps is the rworldmap package. The rworldmap package contains the shape files for countries but also more fine grained-shape files that display the states of selected countries. The most basic data, however, simply represents the shapes of the countries in the world.

Using the worldmap package has the advantage that one is not dependent on third parties and their servers but can operate within R without being denied access due to e.g. copy right issues or server maintenance.

# load library
# get map
worldmap <- getMap(resolution = "coarse")
# plot world map
plot(worldmap, col = "lightgrey", 
     fill = T, border = "darkgray",
     xlim = c(-180, 180), ylim = c(-90, 90),
     bg = "aliceblue",
     asp = 1, wrap=c(-180,180))

The basic map shown above can then be modified and enriched with color coding to convey geospatial data. The following shows how to customize the world map.

# load package
# plot maps
# show map with Latitude 200 as center
maps::map('world', xlim = c(100, 300))
# add axes

# show filled map with Latitude 200 as center
ww2 <- maps::map('world', wrap=c(0,360), plot=FALSE, fill=TRUE)
maps::map(ww2, xlim = c(100, 300), fill=TRUE)

Generating maps with rnaturalearth and ggplot2

We can also use the data provided by the rnaturalearth and the rnaturalearthdata and use ggplot function from the ggplot2 package as well as the sf package to create very nice visualizations of geospatial data. The advantage over using rworldmap and base R lies in the fact that the code is easier to interpret and the visualizations are more appealing.

# load data
world <- ne_countries(scale = "medium", returnclass = "sf")
# gene world map
ggplot(data = world) +
  geom_sf() +
  labs( x = "Longitude", y = "Latitude") +
  ggtitle("World map", subtitle = paste0("(", length(unique(world$admin)), " countries)"))

We can also easily zoom in on certain areas in the map using the coord_sf function and also prettify the map by adding some custom features such as a compass.

# gene world map
ggplot(data = world) +
  geom_sf() +
  labs( x = "Longitude", y = "Latitude") +
  coord_sf(xlim = c(100.00, 160.00), ylim = c(-45.00, -10.00), expand = T) +
  annotation_scale(location = "bl", width_hint = 0.5) +
  annotation_north_arrow(location = "bl", which_north = "true", 
                         pad_x = unit(0.75, "in"), pad_y = unit(0.5, "in"),
                         style = north_arrow_fancy_orienteering) +

We will now customize these basic maps and add information to them.

2 Customizing Maps

Displaying basic maps is usually less interesting because, typically, we want to add different layers to a map. In order to add layers to a map, we need to have a shape file, i.e. a file which contains information about borders or locations that can then be displayed in different colors. In other words, we need to have a shape object to add information to the map.

However, it is often the case that we want to add information that is not already available. Therefore, we load the airports data set which contains the longitude and latitude of airports across the world. We will then use this data to show the locations of airports across the globe.

# load data
airports <- base::readRDS(url("", "rb")) %>%
  dplyr::mutate(ID = as.character(ID))
airports %>% %>%
  head(15) %>%
  flextable() %>%
  flextable::set_table_properties(width = .5, layout = "autofit") %>%
  flextable::theme_zebra() %>%
  flextable::fontsize(size = 12) %>%
  flextable::fontsize(size = 12, part = "header") %>%
  flextable::align_text_col(align = "center") %>%
  flextable::set_caption(caption = "First 15 rows of the airports data.")  %>%

To display the locations of airports on a map, we first plot the map and then add a layer of points to indicate the location of airports. In addition, the “plot” functions offers various arguments for customizing the display, e.g. by changing the background color (bg), defining the color of borders (borders), defining the color of the shapes (fill and col).

# plot data on world map
plot(worldmap, xlim = c(-80, 160), ylim = c(-50, 100), 
     asp = 1, bg = "lightblue", col = "black", fill = T)
# add points
points(airports$Longitude, airports$Latitude, 
       col = "red", cex = .01)

It is, of course, also possible to highlight individual countries.

# create data frame with iso3 country codes and number of visits
countriesvisited <- data.frame(country = c("AUS", "JPN", "FIN", 
                                           "CZE", "POL", "AUT", 
                                           "USA", "GBR", "IRL", 
                                           "DEU", "DNK", "FRA", 
                                           "NDL", "BEL", "ESP",
                                           "HRV", "SVN", "NOR", 
                                           "ITA", "HUN", "ROU", 
                                           "BGR", "GRC", "TUR", 
                                           "CHE", "ARE"),
  visited = c(5, 1, 2, 1, 1, 3, 4, 4, 5, 11, 1, 1, 2, 2, 4, 4, 
              1, 1, 3, 1, 1, 2, 1, 1, 3, 2))
countriesvisited %>% %>%
  head(15) %>%
  flextable() %>%
  flextable::set_table_properties(width = .5, layout = "autofit") %>%
  flextable::theme_zebra() %>%
  flextable::fontsize(size = 12) %>%
  flextable::fontsize(size = 12, part = "header") %>%
  flextable::align_text_col(align = "center") %>%
  flextable::set_caption(caption = "First 15 rows of the countriesvisited data.")  %>%
# combine data frame with map
visitedMap <- joinCountryData2Map(countriesvisited, 
                                  joinCode = "ISO3",
                                  nameJoinColumn = "country")
## 25 codes from your data successfully matched countries in the map
## 1 codes from your data failed to match with a country code in the map
## 218 codes from the map weren't represented in your data
# def. map parameters, e.g. def. colors
mapParams <- mapCountryData(visitedMap, 
                            oceanCol = "azure2",
                            catMethod = "categorical",
                            missingCountryCol = gray(.8),
                            colourPalette = c("coral",
                                              "coral3", "orangered", 
                                              "orangered3", "orangered4"),
                            addLegend = F,
                            mapTitle = "",
                            border = NA)
# add legend and display map, c(mapParams,
                             x = 'bottom',
                             title = "No. of visits",
                             horiz = TRUE,
                             bg = "transparent",
                             bty = "n"))

It is, of course also possible to show only a part of the map by defining the x- and y-axes limits of the plot window.

# get map
newmap <- getMap(resolution = "low")
# plot map
plot(newmap, xlim = c(-20, 59), ylim = c(35, 71), 
     asp = 1, fill = T, border = "darkgray", 
     col = "wheat2", bg = "gray95")
# add points
points(airports$Longitude, airports$Latitude, col = "red", cex = .5, pch = 20)

This is of course also possible to show Australian airports.

# plot data on world map
plot(worldmap, xlim = c(110, 160), ylim = c(-45, -10), 
     asp = 1, bg = "azure2", border = "lightgrey", col = "wheat1", 
     fill = T, wrap=c(-180,180))
points(airports$Longitude, airports$Latitude, 
       col = "darkblue", cex = .5, pch = 20)

In addition to the location of airports, it is also possible to show how many flights arrive at an airport. As this information is not provided in the airport data, we load the routes data which contains information about the routes that airlines fly.

# read in routes data
routes <- base::readRDS(url("", "rb"))
routes %>% %>%
  head(15) %>%
  flextable() %>%
  flextable::set_table_properties(width = .5, layout = "autofit") %>%
  flextable::theme_zebra() %>%
  flextable::fontsize(size = 12) %>%
  flextable::fontsize(size = 12, part = "header") %>%
  flextable::align_text_col(align = "center") %>%
  flextable::set_caption(caption = "First 15 rows of the routes data.")  %>%

To show the number of arrivals at an airport (only in terms of how mayn routes end at that airport), we extract the number of routes that end in each airport.

# extract number of flights per airport
arrivals <- routes %>%
  dplyr::group_by(destinationAirportID) %>%
  dplyr::summarise(flights = n()) %>%
  dplyr::ungroup() %>%
  dplyr::rename(ID = destinationAirportID)
arrivals %>% %>%
  head(15) %>%
  flextable() %>%
  flextable::set_table_properties(width = .5, layout = "autofit") %>%
  flextable::theme_zebra() %>%
  flextable::fontsize(size = 12) %>%
  flextable::fontsize(size = 12, part = "header") %>%
  flextable::align_text_col(align = "center") %>%
  flextable::set_caption(caption = "First 15 rows of the arrivals data.")  %>%

We can now merge the airports and the arrival data set to combine the information about the location with the information about the number of routes that end at each airport.

# combine tables
airportA <- dplyr::left_join(airports, arrivals, by = "ID")
airportA %>% %>%
  head(15) %>%
  flextable() %>%
  flextable::set_table_properties(width = .5, layout = "autofit") %>%
  flextable::theme_zebra() %>%
  flextable::fontsize(size = 12) %>%
  flextable::fontsize(size = 12, part = "header") %>%
  flextable::align_text_col(align = "center") %>%
  flextable::set_caption(caption = "First 15 rows of the airportA data.")  %>%

This table allows us to plot not only the location of airports but also the number of routes that end there.

# get map
australia <- getMap(resolution = "low")
# plot data on world map
plot(australia, xlim = c(110, 160), ylim = c(-45, -10), 
     asp = 1, bg = "azure1", border = "darkgrey", 
     col = "wheat2", fill = T)
# add points
points(airportA$Longitude, airportA$Latitude,
       # define colors as transparent
       col = rgb(red = 0, green = 0, blue = 1, alpha = 0.3),
       # define size as number of flights div. by 50
       cex = airportA$flights/50, pch = 20)

The same map can also be created using the ggplot2 package which offers a very high degree of flexibility and allows for easy customization.

# create a layer of borders
ggplot(airportA, aes(x=Longitude, y= Latitude)) +   
  borders("world", colour=NA, fill="wheat1")  +
  geom_point(color="blue", alpha = .2, size = airportA$flights/20) +
  scale_x_continuous(name="Longitude", limits=c(110, 160)) +
  scale_y_continuous(name="Latitude", limits=c(-45, -10)) +
  theme(panel.background = element_rect(fill = "azure1", colour = "azure1")) +
  geom_text(aes(x=Longitude, y= Latitude, label=City),
            color = "gray20", check_overlap = T, size = 3)

In addition, it may be useful to overlay an area with shading to indicate the density of airports orflights.

# create map with density layer
ggplot(airportA, (aes(x = Longitude, y= Latitude))) +   
  borders("world", colour=NA, fill="antiquewhite")  +
  stat_density2d(aes(fill = ..level..,  alpha = I(.2)),
                 size = 1, bins = 5, data = airportA,
                 geom = "polygon") +
  geom_point(color="red", alpha = .2, size=airportA$flights/100) +
  # define color of density polygons
  scale_fill_gradient(low = "grey50", high = "grey20") +
  theme(panel.background = element_rect(fill = "lightblue",
                                colour = "lightblue"),
        panel.grid.major = element_blank(), 
        panel.grid.minor = element_blank(),
        # surpress legend
        legend.position = "none",
        axis.title.y=element_blank()) +
  geom_text(aes(x=Longitude, y= Latitude, label=Name),
            color = "gray20", fontface = "italic", check_overlap = T, size = 2,
            alpha = sqrt(airportA$flights)/10)

You can also overlay maps to show densities (e.g. of airports) as shown below.

# load library
# define box
sbbox <- make_bbox(lon = c(115, 155), lat = c(-12.5, -42), f = .1)
# get map
ausbg = get_map(location=sbbox, zoom=4,
                # possible sources
                source = "osm",
                #source = "google",
                #source = "stamen",
                # possible coloring
                #color = "bw",
                color = "color",
                # possible maptypes
# create map
ausbg = ggmap(ausbg)
# display map
ausbg +
  stat_density2d(data = airportA, aes(x = Longitude, y= Latitude, 
                     fill = ..level..,  alpha = I(.2)),
                 size = 1, bins = 5, geom = "polygon") +
  geom_point(data = airportA, mapping = aes(x=Longitude, y= Latitude), 
             color="red", alpha = .2, size=airportA$flights/20) +
  # define color of density polygons
  scale_fill_gradient(low = "grey50", high = "grey20") +
        panel.background = element_rect(fill = "aliceblue",
                                colour = "aliceblue"),
        panel.grid.major = element_blank(), 
        panel.grid.minor = element_blank(),
        # surpress legend
        legend.position = "none")

If you simply want to generate a map of a place, then you can use the ggmap package to do this.

# define box
sbbox <- make_bbox(lon = c(152.8, 153.4), lat = c(-27.1, -27.7), f = .1)
# get map
brisbane = get_map(location=sbbox, zoom=10, maptype="terrain")
# create map
brisbanemap = ggmap(brisbane)
# display map
brisbanemap +
  geom_point(data = airportA, mapping = aes(x = Longitude, y = Latitude), 
               color = "red") +
  geom_text(data = airportA, 
            mapping = aes(x = Longitude+0.1,
                          y = Latitude,
                          label = "Brisbane Airport"),
            size = 2, color = "gray20", 
            fontface = "bold", 
            check_overlap = T) 

3 Color Coding Geospatial Information

We can also use color coding of countries to convey information about different features of countries such as their population size (or results of political elections, etc.).

We use the data provided in the rnaturalearthdata and rnaturalearth packages, which contain information about the population size of countries, to color code and thus visualize differences in population size by country.

# extract world data
world <- ne_countries(scale = "medium", returnclass = "sf")
# create cut off points in Population
world$pop_estC <- base::cut(world$pop_est,
                        breaks = c(0, 500000, 1000000, 10000000, 
                                   100000000, 200000000, 500000000,
                        labels = 1:7, right = F, ordered_result = T)
# load package for colors
# define colors
palette = colorRampPalette(brewer.pal(n=7, name='Oranges'))(7)
palette = c("white", palette)
# create map
worldpopmap <- ggplot() +
  geom_sf(data = world, aes(fill = pop_estC)) +
  scale_fill_manual(values = palette) +
  # customize legend title
  labs(fill = "Population Size") +
  theme(panel.grid.major = element_blank(), 
        panel.grid.minor = element_blank(),
        # surpress legend
        legend.position = "none")
# display map

We can also scale the colors and add a legend to assist with the interpretation of the map.

# start plot
ggplot(data = world) +
    geom_sf(aes(fill = pop_est)) +
  scale_fill_viridis_c(option = "plasma", trans = "sqrt") +
  # customize legend title
  labs(fill = "Population Size")

It is relatively easy to combine color coding with points. However, it is also relatively easy to color countries if all countries have values. If this is not the case (as in the example below), we need to include an additional step so that countries that are not mentioned also receive a color coding. In this example, the data contain countries and cities that I have visited along with their latitude and longitude.

# load data
visited <-  base::readRDS(url("", "rb"))

After loading the data, we determine how many cities I have visited in a given country and add this frequency to the data set.

# determine how often a country was visited
ncountry <-$ISO3))
colnames(ncountry)[1] <- "ISO3" 
# add frequency to visited
visited <- merge(visited, ncountry, by = "ISO3")

The next part is tricky as we do not only need to determine the color for the countries I have visited but also the color for those that I have not visited. In order to do that, we load the data set that underlies the world map that will be displayed.

# load package
# load world data for plotting 
# def. color (bias for contrast)
pal <- colorRampPalette(brewer.pal(6, 'Greens'), 
                        bias = 10)(length(visited$Freq))
pal <- pal[with(visited, findInterval(Freq, sort(unique(Freq))))]
# define color for countries not in visited
countrycolor <- rep("white", length(wrld_simpl@data$NAME))
# define colors for countries in visited
countrycolor[match(visited$Country, wrld_simpl@data$NAME)] <- pal

After assigning colors to all countries, we can proceed by plotting the color coded map along with points for the locations of the cities I have visited.

# plot map
plot(wrld_simpl, ylim=c(-40, 85), xlim = c(-180, 180),
     mar=c(0,0,0,0), bg="gray40", border = NA, 
# add points
points(visited$Longitude, visited$Latitude, col="red", pch=20, cex = .5)

A slightly more elegent way to achive the same goal is to plot the information using the ggplot2 package.

# use the contry name instead of 3-letter ISO as id
wrld_simpl@data$id <- wrld_simpl@data$NAME
wrld <- fortify(wrld_simpl, region="id") %>%
  dplyr::filter(id != "Antarctica")
# change column names of visited
visited <- visited %>%
  dplyr::rename(id = Country)
# combine visited and wrld
worlddata <- dplyr::left_join(wrld, visited, by = "id")
# change NA to 0
worlddata <- worlddata %>%
  dplyr::mutate(Freq = ifelse(, 0, Freq),
                Freq = as.factor(Freq))
# start plotting
ggmapplot <- ggplot() + 
  geom_map(data=worlddata, map=worlddata, 
           aes(map_id=id, x=long, y=lat, 
           fill=Freq), color=NA, size=0.25) + 
  geom_point(data = visited,
             aes(x = Longitude, y = Latitude), 
             col = "red", size = .75) +
  scale_fill_manual(values=c("white", "gray80", "gray75", 
                             "gray70",  "gray65", "gray60", "gray55"), 
                    name="No. cities visited") + 
#  coord_map("gilbert") +  # spherical
  coord_map() +           # for normal Mercator projection
  labs(x="", y="") + 
  theme(plot.background = element_rect(fill = "grey40", colour = NA),
                   panel.border = element_blank(),
                   panel.background = 
          element_rect(fill = "transparent", colour = NA),
                   panel.grid = element_blank(),
                   axis.text = element_blank(),
                   axis.ticks = element_blank(),
                   legend.position = "right")

4 Interactive Maps

You can also use the leaflet package to create interactive maps. The leaflet function from the leaflet package creates a leaflet-map using html-widgets. The widget can be rendered on HTML pages generated from R Markdown, Shiny, or other applications. The advantage in using this function lies in the fact that it offers very detailed maps which enable zooming in to very specific locations.

# load package
# load library
m <- leaflet() %>% setView(lng = 153.05, lat = -27.45, zoom = 12)
# display map
m %>% addTiles()

Another option is to display information about different countries. In this case, we can use the information provided in the maptools package which comes with a SpatialPolygonsDataFrame of the world and the population by country (in 2005). To make the visualization a bit more appealing, we will calculate the population density, add this variable to the data which underlies the visualization, and then display the information interactively. In this case, this means that you can use mouse-over or hoover effects so that you see the population density in each country if you put the curser on that country (given the information is available for that country).

We start by loading the required package from the library, adding population density to the data, and removing data points without meaningful information (e.g. we set values like Inf to NA).

# load data
# calculate population density and add it to the data 
wrld_simpl@data$PopulationDensity <- round(wrld_simpl@data$POP2005/wrld_simpl@data$AREA,2)
wrld_simpl@data$PopulationDensity <- ifelse(wrld_simpl@data$PopulationDensity == "Inf", NA, wrld_simpl@data$PopulationDensity)
wrld_simpl@data$PopulationDensity <- ifelse(wrld_simpl@data$PopulationDensity == "NaN", NA, wrld_simpl@data$PopulationDensity)

We can now display the data.

# define colors
qpal <- colorQuantile(rev(viridis::viridis(10)),
                      wrld_simpl$PopulationDensity, n=10)
# generate visualization
l <- leaflet(wrld_simpl, options =
               leafletOptions(attributionControl = FALSE, minzoom=1.5)) %>%
      NAME, ' ',
      formatC(PopulationDensity, big.mark = ',', format='d')),
    labelOptions= labelOptions(direction = 'auto'),
    weight=1, color='#333333', opacity=1,
    fillColor = ~qpal(PopulationDensity), fillOpacity = 1,
    highlightOptions = highlightOptions(
      color='#000000', weight = 2,
      bringToFront = TRUE, sendToBack = TRUE)
    ) %>%
    "topright", pal = qpal, values = ~PopulationDensity,
    title = htmltools::HTML("Population density <br> (2005)"),
    opacity = 1 )
# display visualization