Interactive maps are a powerful visualization tool, and the JavaScript library leaflet.js is a great means to achieving this objective. The {leaflet} package from RStudio makes this library accessible from R.

The package documentation is good, but as the interactive visualization is usually the last step of a complex process I felt the need to share some of my lessons learned.

For this demonstrations I am using a data frame of seven government buildings in the Czech Republic, but the actual choice of points is not that crucial – what I found nice for demonstration purposes is that:

  • the seven buildings are in two separate cities
  • can be grouped to three branches of power
  • have both names and links to Wikipedia entries

The first step is creating the the data frame and making it feel spatial via the excellent {sf} package. By using a spatial data frame as data object of our leaflet call we are absolved from the need to specify longitude and latitude. This would be hassle (but doable) for points, but next to impossible for polygons.

library(sf)             # for working with spatial data
library(dplyr)          # data frame manipulation
library(leaflet)        # because leaflet :)
library(htmltools)      # tools to support html workflow
library(rnaturalearth)  # interface to https://www.naturalearthdata.com/
library(leaflet.extras) # extending the leaflet.js


# set up a data frame of points
body <- data.frame(name = c("Kramářova vila", "Pražský hrad", "Strakova akademie", "Nejvyšší soud", "Ústavní soud", "Sněmovna", "Senát"),
                   branch = c("executive", "executive", "executive", "judiciary", "judiciary", "legislature", "legislature"),
                   link = c("https://en.wikipedia.org/wiki/Kram%C3%A1%C5%99%27s_Villa",
                            "https://en.wikipedia.org/wiki/Prague_Castle",
                            "https://en.wikipedia.org/wiki/Straka_Academy",
                            "https://en.wikipedia.org/wiki/Supreme_Court_of_the_Czech_Republic",
                            "https://en.wikipedia.org/wiki/Constitutional_Court_of_the_Czech_Republic",
                            "https://en.wikipedia.org/wiki/Chamber_of_Deputies_of_the_Czech_Republic",
                            "https://en.wikipedia.org/wiki/Senate_of_the_Czech_Republic"),
                   lat = c(14.4104392, 14.3990089, 14.4117831, 16.6021958, 16.6044039, 14.4039458, 14.4053489),
                   lon = c(50.0933681, 50.0895897, 50.0920997, 49.2051925, 49.1977642, 50.0891494, 50.0900269))


# transform the data frame from plain vanilla one to spatial
body <- body %>% 
  sf::st_as_sf(coords = c("lat", "lon"), # columns with geometry
               crs = 4326) # WGS84 is a sensible default...

The first and easiest (“hello word”) call to leaflet takes just four lines of code.

What it does is:

  • declare a leaflet object (with our spatial data frame as data object)
  • add a basemap layer
  • add a data layer – markers showing our seven buildings
  • display the prepared leaflet object

With regards to basemap and formatting it relies on (usually sensible) defaults.

It may not be overwhelming, but it is a start.

# first prepare a leaflet plot ...
lplot <- leaflet::leaflet(data = body) %>% # create leaflet object
  leaflet::addTiles() %>% # add basemap
  leaflet::addMarkers() # add data layer - markers

lplot #  ... then display it


An important aspect of working with leaflet is saving your work – this allows you to create a visualization while in R, and utilize it somewhere else in a general HTML context. Such as embedding in a website, or attaching to an email.

This is easily done via save_html function from the {htmltools} package.

# saving the prepared leaflet plot as a html file
htmltools::save_html(lplot, "leaflet.html")

The default settings of leaflet maps are okayish, but some extra work is usually required before you can consider your output presentation worthy.

The two most common tweaks are:

  • change the basemap – I find the default basemap too colorful, and as consequence distracting. Positron from Carto gives a subtle hint of color, and reminds me of Toner basemap from Stamen that is no more
  • label the markers – it is the labels that gives a real meaning to the points of interest

For labeling the markers it is necessary to map a column containing the popup text via tilde (~) operator to popup argument of the addMarkers call.

Not strictly necessary, but still a good practice, especially when dealing with data frames from uncertain sources, is sanitizing the text of the label for unexpected characters by wrapping it in htmlEscape function from {htmltools} package.

# first prepare a leaflet plot ...
lplot <- leaflet(body) %>% 
  leaflet::addProviderTiles("CartoDB.Positron") %>% 
  addMarkers(popup = ~htmltools::htmlEscape(name)) # note the tilde! ~

lplot #  ... then display it


The points form two distinct clusters – the government and Parliament are in Prague, while the two top courts sit in Brno. This makes navigating the map sort of tricky.

One way to remedy this is to group the markers in clusters. An useful feature of clusters is that they zoom in once clicked.

Clusters are formed via the clusterOptions argument of a markers call; the default clusterOptions = markerClusterOptions() is good in vast majority of cases.

When we created a way to zoom the map to a very fine detail (via double clicking a cluster) it is good idea to add a feature to restore the zoom level to its original setting, showing the full map. This is done the easiest via addResetMapButton() from the {leaflet.extras} package.

Finally we are replacing the default teardrop shaped markers with dots via addCircleMarkers() instead of plain addMarkers() function. The circles / dots have more mapable features than plain markers, and therefore can carry more information.

In this case I am using color to show information about the branches of the government. The variable is an obviously categorical one, and I am first creating a palette mapping the branch to color via a named vector using colorFactor() function from {leaflet}.

The palette is then used twice:

  • first in the color argument of the addCircleMarkers function to give color to the markers
  • secondly in the pal argument of the addLegend function to give explanation of the colors used in map legend

In a different context we might also consider:

  • mapping the radius of the dots to a numeric variable and have the size of markers vary
  • creating the palette via a colorNumeric() call and map the intensity of color of the dots to a numeric variable
# prepare a palette - manual colors according to branch column
palPwr <- leaflet::colorFactor(palette = c("executive" = "red", 
                                           "judiciary" = "goldenrod", 
                                           "legislature" = "steelblue"), 
                               domain = body$branch)

# first prepare a leaflet plot ...
lplot <- leaflet(body) %>% 
  addProviderTiles("CartoDB.Positron") %>% 
  addCircleMarkers(radius = 10, # size of the dots
                   fillOpacity = .7, # alpha of the dots
                   stroke = FALSE, # no outline
                   popup = ~htmlEscape(name),
                   color = palPwr(body$branch),
                   clusterOptions = markerClusterOptions()) %>% 
  leaflet::addLegend(position = "bottomright",
            values = ~branch, # data frame column for legend
            opacity = .7, # alpha of the legend
            pal = palPwr, # palette declared earlier
            title = "Branch") %>%  # legend title
  leaflet.extras::addResetMapButton()

lplot #  ... then display it


Two other commonly encountered use cases, especially in a commercial context, are:

  • including a custom HTML code in the popup, such as a HTML link to a web site
  • using a custom image as an icon for the marker, such as a corporate logo

The custom HTML is easy, all it takes is constructing a column in the body data frame with the HTML code of the label.

For the purpose of my example I am using three basic HTML tags:

The custom icon takes two steps:

  • declaring the icon via the makeIcon() function from the {leaflet} package
  • specifying the icon declared earlier in the icon argument of an addMarkers call
# create a column with HTML code for popup text
body <- body %>% 
  dplyr::mutate(label = paste0("<b>", htmlEscape(body$name), "</b> <br>",
                      "Be sure to check ",
                      '<a href="', # note the use of single quotes - to make a single " a legit piece of code
                       body$link,
                       '"target="_blank">the wikipedia page</a>.')) # again note the combination of single & double quotes

# declare an icon - link to an image somwehere on the net
ikonka <- leaflet::makeIcon(iconUrl = "https://www.jla-data.net/img/netopejr.png", # url to icon
                   iconWidth = 50, iconHeight = 50) # sizing as required

# first prepare a leaflet plot ...
lplot <- leaflet(body) %>% 
  addProviderTiles("CartoDB.Positron") %>%
  addMarkers(popup = ~label, 
             clusterOptions = markerClusterOptions(), 
             icon = ikonka) %>% 
  addResetMapButton()

lplot #  ... then display it


With a lot of data displayed at once your leaflet map may become somewhat cluttered. In such cases it is a good idea to partition your data into groups, and allow user to switch them on and off – especially useful when one of the groups is much smaller than the rest, and at a risk of being overwhelmed.

The first step is creating filtered dataset of the individual groups – I am creating one for Prague points, and one for Brno ones.

The second step is drawing the leaflet map. Note how:

  • the data object of the root leaflet object contains the original data frame, but is never actually used (except for determining the initial zoom level)
  • the addCircleMarkers call is executed twice, with different values for data object and with specified value of the group object
  • the addLayersControl from {leaflet} creates a checkbox to select any (or all) of the groups specified in overlayGroups object; it is necessary that the items in vector provided match the names of groups created in previous addCircleMarkers calls

While most of the default options in {leaflet} are sensible I found the addLayersControl to be an exception, as it is by default collapsed. This is easily remedied by a options = layersControlOptions(collapsed = FALSE) call.

Setting the overlayGroups object of addLayersControl call creates a control with check box functionality (all groups can be turned on or off separately).

In some use cases it might be desirable to create a radio button functionality (one, and only one, group can be selected at any time); this can be done by assigning the vector of groups to a baseGroups object of the addLayersControl instead of overlayGroups.

# data frame or Prague points
praha <- body %>% 
  dplyr::filter(branch != "judiciary") # a shortcut - all except judiciary are in Prague

# data frame of Brno points
brno <- body %>% 
  filter(branch == "judiciary") # only judiciary happens to be in Brno (and only in Brno)

# first prepare a leaflet plot ...
lplot <- leaflet(data = body) %>% # data = original body - to get the zoom right
  addProviderTiles("CartoDB.Positron") %>% 
  addCircleMarkers(data = praha, # first group
                   radius = 10,
                   fillOpacity = .7,
                   stroke = FALSE,
                   popup = ~htmlEscape(name),
                   color = palPwr(praha$branch), # using already created palette
                   clusterOptions = markerClusterOptions(),
                   group = "Prague") %>% 
  addCircleMarkers(data = brno, # second group
                   radius = 10,
                   fillOpacity = .7,
                   stroke = FALSE,
                   popup = ~htmlEscape(name),
                   color = palPwr(brno$branch), # using already created palette
                   clusterOptions = markerClusterOptions(),
                   group = "Brno") %>% 
  addLegend(position = "bottomright",
            values = ~branch,
            opacity = .7,
            pal = palPwr, # palette declared previously
            title = "Branch") %>% 
  leaflet::addLayersControl(overlayGroups = c("Prague", "Brno"),
                   options = layersControlOptions(collapsed = FALSE)) %>% 
  addResetMapButton()

lplot #  ... then display it


While points are the most commonly used spatial items in leaflet maps they are not the only ones possible. Other options include polygons and lines.

Unlike points, with just two coordinates, it is rarely practical to specify polygon boundaries directly in R code, and polygons are usually imported. Either in one of the popular formats such as ESRI shapefile or GeoJSON, or via one of the spatial R packages.

In this case I am using state borders of the Czech Republic provided via {rnaturalearth} package, serving as an interface to Natural Earth data.

I am using the polygon just to illuminate the borders, and I am not mapping any data to it. Should I desire so the most common use case would be to map a the color of fill object to a data item via a palette to create a choropleth map.

As I wanted something a bit different for my last example I am using an unusual background map – the NASA 2012 view of Earth at night – for no better reason than to show off.

As I want the continent of Europe to be displayed nice and clear I specify via the setView call from {leaflet} package the center of my map to Munich, Bavaria, and zoom the map out a bit by setting the zoom attribute of setView call to 4.

And finally I removed the lengthy attribution at the bottom of the map by setting options object of my root leaflet call to leafletOptions(attributionControl = FALSE).

This is a naughty thing to do, and should only be used with care and for for a good reason. But there are other ways to provide attribution and the control uses valuable screen real estate.

# Czechia as a country from the Natural Earth
czechia <- rnaturalearth::ne_countries(country = "Czechia", scale = 'large')

# first prepare a leaflet plot ...
lplot <- leaflet(body,
                 options = leafletOptions(attributionControl = FALSE)) %>% # naughty me!
  addProviderTiles("NASAGIBS.ViirsEarthAtNight2012") %>%  # something different...
  addCircleMarkers(radius = 2,
                   popup = ~htmlEscape(name),
                   color = "cornflowerblue") %>% 
  leaflet::addPolygons(data = czechia,
              fill = NA,
              weight = 3,
              color = "orangered",
              opacity = .8) %>% 
  leaflet::setView(lng = 11.57549, # map centered on Munich, Bavaria
          lat = 48.13743,
          zoom = 4)  # & zoomed out a bit...

lplot #  ... then display it


I hope I have demonstrated that using leaflet.js library via the {leaflet} package is both a powerful visualization technique, and a fun thing to do.

Now I wish you the best in your efforts.