Leaflet in R
This post shows how to build beautiful interactive maps in R using leaflet.
library(leaflet)
library(sf)
library(terra)
library(raster)
library(RColorBrewer)
library(htmlwidgets)
Read data
Here I am reading three different datasets, a polygon (mx_states) and a point (caps) layer, as well as a raster (DEM).
# States polygons
# Data downloaded from http://www.conabio.gob.mx/informacion/gis/?vns=gis_root/dipol/estata/dest22gw
mx_states <- st_read("dest22gw.shp")
# DEM
# Data downloaded from: http://www.conabio.gob.mx/informacion/gis/?vns=gis_root/dipol/estata/dest22gw
dem <- rast("filled_demgw.tif")
# Capitals
# Data downloaded from: https://www.efrainmaps.es/descargas-gratuitas/m%C3%A9xico/
caps <- st_read("México_Ciudades.shp")
Create palettes
Create palettes for the data. Here we are goin to use RcolorBrewer functionalities and some leaflet functions. Also, notice that I am creating two palettes for the DEM. This is a small hack to put the legend in a reverse order (low values in the lower side and higher in the upper one).
## States palette
coul <- brewer.pal(4, "PuOr")
pal_st <- colorRampPalette(coul)(33)
## Dem palette
coul <- grDevices::colorRampPalette(c("#026449", "#12722c","#d7d17e",
"#95400d", "#980802", "#746c69", "#f1f1f1","#fdfdfd"),
interpolate = "spline",
bias = 1)(256)
pal_dem <- leaflet::colorNumeric(
c("#026449", "#12722c","#d7d17e",
"#95400d", "#980802", "#746c69", "#f1f1f1","#fdfdfd"),
values(dem),
na.color = "transparent",
alpha = FALSE,
reverse = FALSE
)
# Palette hack to invert legend
pal_dem2 <- leaflet::colorNumeric(
c("#026449", "#12722c","#d7d17e",
"#95400d", "#980802", "#746c69", "#f1f1f1","#fdfdfd"),
values(dem),
na.color = "transparent",
alpha = FALSE,
reverse = TRUE
)
## Capitals palette, same as states
Leaflet map
Then create the leaflet map. First let’s add the polygons.
mapa <- leaflet::leaflet()
## Add Polygons
mapa <- mapa %>%
leaflet::addPolygons(data = mx_states,
stroke = TRUE,
smoothFactor = 0.5,
opacity = 1,
fillOpacity = 0.9,
fillColor = ~ pal_st,
weight = ~0.2,
color = ~"black",
group = "States",
popup = ~mx_states$NOMGEO)
Add the raster. Here notice the use of pal_dem2
in addLegend
and sort the values in decreasing order using labFormat
.
## Get tange of dem
minmax <- range(raster::values(dem)[!is.na(raster::values(dem))])
## Add raster
mapa <- mapa %>%
leaflet::addRasterImage(raster::raster(dem),
colors = pal_dem,
opacity = 0.9,
group = "DEM",
layerId = "DEM") %>%
leaflet::addLegend(position = "bottomleft",
pal = pal_dem2,
values = seq(minmax[1], minmax[2], 100), #4 categorical maps terra::levels(dem)[[1]]$ID,
title = "Elevación m s.n.m",
labFormat = labelFormat(transform = function(x) sort(x, decreasing = TRUE)))
# for categorical maps
# labFormat = leaflet::labelFormat(
# transform = function(x) {
# df_eq %>%
# dplyr::filter(ID == x) %>%
# dplyr::pull(!!sym(key))
# }))
Add the points. Here I set a different color to the circle inside the marker.
## Points
### Create customized markers
### Can create in several lists, that's why two lapply are used
### In this case we really only need one level
resul <- lapply(1:length(pal_st), function(j){
leaflet::makeAwesomeIcon(
icon = "circle",
library = "fa",
iconColor = pal_st[j],
markerColor = "white",
)
})
# Cast as awesome icon list
resul <- structure(resul, class = "leaflet_awesome_icon_set")
## Add points
mapa <- mapa %>%
leaflet::addAwesomeMarkers(data = caps,
icon = resul,
popup = ~caps$CIUDAD,
group = "Capitals")
Add three Esri basemaps
## Base maps
mapas_base <- c("Esri.WorldTopoMap", "Esri.WorldImagery", "Esri.WorldGrayCanvas")
# Add basemaps
for(provider in mapas_base) {
mapa <- mapa %>%
leaflet::addProviderTiles(provider,
group = provider)
}
Add controls and mini map. OverlayGroups
should match the name given for each layer in the previous sections.
# Add controls and mini map
mapa <- mapa %>%
leaflet::addLayersControl(overlayGroups = c("States", "DEM", "Capitals"),
baseGroups = mapas_base,
position = "topright",
options = leaflet::layersControlOptions(collapsed = FALSE,
hideSingleBase = TRUE)) %>%
leaflet::addMiniMap(tiles = mapas_base[[1]],
toggleDisplay = TRUE,
position = "bottomleft")
Add more customizations: change base map, zoom to extent of layers, add globe button to reset zoom level to the starting point, add opacity slider.
# More customizations
mapa <- mapa %>%
# update base map
htmlwidgets::onRender("
function(el, x) {
var myMap = this;
myMap.on('baselayerchange',
function (e) {
myMap.minimap.changeLayer(L.tileLayer.provider(e.name));
})
}") %>%
# add full extent button
leaflet::addEasyButton(leaflet::easyButton(
icon = "fa-globe",
title = "Zoom to Level 1",
onClick = leaflet::JS("function(btn, map){ map.fitBounds([
[", 14.55712, ",", -117.12579, "], ",
"[", 32.71876, ",", -86.74011, "]
]); }"))) %>%
# opacity slider
leaflet::addControl(html = "<input id=\"OpacitySlide\" type=\"range\" min=\"0\" max=\"1\" step=\"0.1\" value=\"0.5\">") %>%
# change opacity of the layers
htmlwidgets::onRender(
"function(el,x,data){
var map = this;
var evthandler = function(e){
var layers = map.layerManager.getVisibleGroups();
console.log('VisibleGroups: ', layers);
console.log('Target value: ', +e.target.value);
layers.forEach(function(group) {
var layer = map.layerManager._byGroup[group];
console.log('currently processing: ', group);
Object.keys(layer).forEach(function(el){
if(layer[el] instanceof L.Polygon){;
console.log('Change opacity of: ', group, el);
layer[el].setStyle({fillOpacity:+e.target.value});
}
});
})
};
$('#OpacitySlide').mousedown(function () { map.dragging.disable(); });
$('#OpacitySlide').mouseup(function () { map.dragging.enable(); });
$('#OpacitySlide').on('input', evthandler)}
")
Save file as html widget.
htmlwidgets::saveWidget(mapa,
"Map1.html")
The final result (click on the following image to access the map):