Layer-Specific Legends in Leaflet

Wendy Wang
IBM Data Science in Practice
5 min readSep 7, 2021

This series of posts shares tips and tricks using R Shiny, based on lessons learned from practice. Please read the first post and second post for more context and information. This post, the third, will present how to create layer-specific legends in a Leaflet map.

layers of mostly fall leaves on a surface
Photo by Hasan Almasi on Unsplash

Oftentimes Leaflet maps are designed to have multiple polygon layers. These types of maps are great for demonstrating to viewers correlations between categories such as GDP, education level, and poverty rates when they switch between layer groups. This post will cover how to create such maps.

These maps can have multiple variables. In the first ever Shiny project I ever did, I ambitiously put 7 variables into a Leaflet map, which at the time I thought was very cool (and, well, still think so :p).

You may find, however, that it is not so straightforward to assign a unique legend to each layer group. Leaflet provides a decent amount of functions, but after going through the documentation up and down, I couldn’t find a way to enable a dynamic legend switch.

Fortunately for you, I came up with this trick to tackle this problem, which I will show below.

Process

I will use the same dataset as I have used in the previous posts. You can find the example Shiny application here and its code here.

The data can also be downloaded with the following lines of code:

download.file('https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_county_5m.zip',
'cb_2018_us_county_5m.zip')
unzip('cb_2018_us_county_5m.zip',exdir='cb_2018_us_county_5m')
download.file('https://www2.census.gov/programs-surveys/popest/geographies/2019/all-geocodes-v2019.xlsx',
'all-geocodes-v2019.xlsx')

TL; DR

  1. detect the switch of a layer group
  2. on the switch, remove the existing legend and add the correct legend associated with the layer group you switch to

Terms and Definitions

layerstrictly speaking, each polygon, marker, etc., on the map is a layer. As a result, when using the layerId parameter in functions like addPolygons, a vector of layer IDs, is expected instead of a single ID. If the layerId parameter is used, you will see the ID field in mouse events, which essentially captures the layerID you passed for a particular polygon, marker or other layer.

layer groupa layer group is a set of layers, for example, the polygons colored by one variable, such as the layers added by one addPolygons function

In this post, I use layer and layer group interchangeably, as we don’t have to take care of the layerId of individual layer in this workaround

Creating a Leaflet Map

First of all, we need to create a map with multiple layer groups. The shapefiles in the dataset from above come with two variables: area of land, ALAND, and area of water, AWATER, for each country. We can simply create a map with each variable as a layer group.

Loading the data and pre-processing it is relatively straightforward.

library(dplyr)
library(readxl)
library(shinydashboard)
library(rgdal)
library(leaflet)
library(htmltools)
shapes <- rgdal::readOGR("cb_2018_us_county_5m","cb_2018_us_county_5m")# the table starts from row 5
df_geo <- read_excel('all-geocodes-v2019.xlsx',skip=4) %>%
filter(`Summary Level`=='040') %>%
select(`State Code (FIPS)`, `Area Name (including legal/statistical area description)`)
colnames(df_geo) <- c('STATEFP','STATENAME')shapes@data <- shapes@data %>%
left_join(df_geo) %>%
mutate(ALAND = as.numeric(as.character(ALAND)),
AWATER = as.numeric(as.character(AWATER)),
NAME = as.character(NAME))
# remove shapes that are not in a state (e.g., Guam)
shapes <- shapes[!is.na(shapes@data$STATENAME),]
# to be used by the dropdown list as available options
names_state <- sort(df_geo$STATENAME)

In the UI section, we create a leafletOutput with id map, as well as a dropdown list widget to switch from on US state to another (this dropdown list is not required for the trick but to add some fun).

Then in the server section, we use a reactiveValues variable rvs to store the data frame of the selected state. We initialize it with New York State:

rvs <- reactiveValues(poly_state=shapes[shapes@data$STATENAME == 'New York',])

Whenever the user switches to another US state using the dropdown list widget, we update the value in rvs. This is handled by a observeEvent that reacts to the dropdown list (its id is select_state):

observeEvent(input$select_state, {
rvs$poly_state <- shapes[shapes@data$STATENAME == input$select_state,]
})

With everything now in place, we create the Leaflet output:

output$map <- renderLeaflet({

rvs$map <- rvs$poly_state %>%
leaflet() %>%
addTiles('http://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png') %>%
addPolygons(data = rvs$poly_state,
group = 'Area of Land',
weight=1, opacity = 1.0,color = 'white',
fillOpacity = 0.9, smoothFactor = 0.5,
fillColor = ~colorBin('OrRd',ALAND)(ALAND)) %>%
addPolygons(data = rvs$poly_state,
group = 'Area of Water',
weight=1, opacity = 1.0,color = 'grey',
fillOpacity = 0.9, smoothFactor = 0.5,
fillColor = ~colorBin('YlGnBu',AWATER)(AWATER)) %>%
addLayersControl(
position = "bottomright",
baseGroups = c('Area of Land','Area of Water'),
options = layersControlOptions(collapsed = TRUE)) %>%
addLegend(
"topright",
pal = colorBin('OrRd', rvs$poly_state$ALAND),
values = rvs$poly_state$ALAND
) %>%
hideGroup(c('Area of Land','Area of Water')) %>%
showGroup('Area of Land')

})

In this map, we add two layer groups, one called “Area of Land” and the other called “Area of Water”, followed by a layer controller. The legend shows the one for area of land, as we choose to display this layer group.

So far I hope you find it very familiar to what you would do.

Associate Refresh of Legend with Switch of Layer Group

Although this dynamic display of a legend I am showing here is not pre-built in an easy-to-call function, it can be easily implemented. You need to use:

  • input${map_id}_groups: After adding the controller for layer groups, this event is logged by Shiny. The value of it is simply the name of the current selected layer group.
  • observeEvent: With this we can monitor the {map_id}_groups event, so that whenever this selected layer group is changed, the code we need will be executed. But.. what code? Can we manipulate an already-rendered Leaflet map? It doesn’t sound fun to render everything again just to change the legend, although you can definitely do that.
  • leafletProxy: I will give most of the credit to this built-in function that is perfectly designed for handling modifications of an already-rendered Leaflet map.
## update legend when the selected layer group changes
observeEvent(input$map_groups, {
my_map <- leafletProxy("map") %>% clearControls()

if (input$map_groups == 'Area of Land'){
my_map <- my_map %>%
addLegend(
"topright",
pal = colorBin('OrRd', rvs$poly_state$ALAND),
values = rvs$poly_state$ALAND)
}else{
my_map <- my_map %>%
addLegend(
"topright",
pal = colorBin('YlGnBu', rvs$poly_state$AWATER),
values = rvs$poly_state$AWATER)
}
})

Remember our map id is map. Here we ask leafletProxy to create a proxy of this already-rendered map and remove the existing legend with clearControls(), then add the needed legend based on the condition. If the chosen layer group is Area of Land, we will add the orange-to-red color scale on ALAND. Otherwise a different color scale will be added for Area of Water.

an animated map of New York State broken down by county. The first image shows by opaqueness of color how much land area there is in a county, where much of upstate New York have greater areas of land, as they are larger or have less water area. The second image shows by opaqueness of color how much water area there is in a county. The western part of Long Island especially has a lot of water area.

You could do a lot more with this {map_id}_groups event and leafletProxy. For example, you could update not only the legend, but also information related to this layer that sits outside of the Leaflet widget, such as a bar plot that showcases Area of Land or Area of Water for each county like the one we created in the previous blog.

With this trick on hand, I hope you will now be able to confidently build a Leaflet map in Shiny with multiple layer groups!

More Information

Documentation of leafletProxy with examples: https://rstudio.github.io/leaflet/shiny.html#modifying-existing-maps-with-leafletproxy

--

--

Wendy Wang
IBM Data Science in Practice

Machine Learning | Deep Learning | Evolutionary Psychology | Neuroscience