Note: A portion of the explanations & examples pulled from: R Studio
Leaflet is one of the most popular open-source JavaScript libraries for interactive maps. Its used by websites ranging from The New York Times and The Washington Post to GitHub and Flickr, as well as GIS specialists like OpenStreetMap, Mapbox, and CartoDB.
Overall, this R package makes it easy to integrate and control Leaflet maps in R.
To install this R package, run this command at your R prompt: if (!require(leaflet)) {
install.packages("leaflet")
library(leaflet)
}
if (!require(leaflet)) {
install.packages("leaflet")
library(leaflet)
}
** If you are having issues downloading the leaflet package, it may be worth trying toinstall the development version from Github, run:
devtools::install_github("rstudio/leaflet")
. Once installed, you can use this package at the R console, within R Markdown documents, and within Shiny applications.
Then, do the same for all of the other packages that will be required from this demo:
if (!require(Rcpp)) {
install.packages("Rcpp")
library(Rcpp)
}
if (!require(devtools)) {
install.packages("devtools")
library(devtools)
}
if (!require(maps)) {
install.packages("maps")
library(maps)
}
if (!require(curl)) {
install.packages("curl")
library(curl)
}
if (!require(jsonlite)) {
install.packages("jsonlite")
library(jsonlite)
}
if (!require(readr)) {
install.packages("readr")
library(readr)
}
if (!require(data.table)) {
install.packages("data.table")
library(data.table)
}
if (!require(sp)) {
install.packages("sp")
library(sp)
}
if (!require(OpenStreetMap)) {
install.packages("OpenStreetMap")
library(OpenStreetMap)
}
if (!require(dygraphs)) {
install.packages("dygraphs")
library(dygraphs)
}
if (!require(dplyr)) {
install.packages("dplyr")
library(dplyr)
}
if (!require(rgdal)) {
install.packages("rgdal")
library(rgdal)
}
if (!require(dygraphs)) {
install.packages("dygraphs")
library(dygraphs)
}
You create a Leaflet map with these four basic steps:
addTiles
, addMarkers
, addPolygons
) to modify the map widget.Here’s a basic example:
library(leaflet)
m <- leaflet() %>%
addTiles() %>% # Add default OpenStreetMap map tiles
addMarkers(lng=174.768, lat=-36.852, popup="The birthplace of R")
m # Print the map
In case you’re not familiar with the magrittr pipe operator (%>%), here is the equivalent without using pipes:
m <- leaflet()
m <- addTiles(m)
m <- addMarkers(m, lng=174.768, lat=-36.852, popup="The birthplace of R")
m
The function leaflet()
returns a Leaflet map widget, which stores a list of objects that can be modified or updated later. Most functions in this package have an argument map as their first argument, which makes it easy to use the pipe operator %>%
in the magrittr package, as you have seen from the example in the Introduction.
The map widget can be initialized with certain parameters. This is achieved by populating the options argument as shown below.
leaflet(options = leafletOptions(minZoom = 0, maxZoom = 18))
The leafletOptions()
can be passed any option described in the leaflet reference document. Using the leafletOptions()
, you can set a custom CRS and have your map displayed in a non spherical mercator projection as described in projections.
You can manipulate the attributes of the map widget using a series of methods. Please see the help page ?setView
for details.
setView()
# sets the center of the map view and the zoom level;fitBounds()
# fits the view into the rectangle [lng1, lat1]
[lng2, lat2]
;clearBounds()
# clears the bound, so that the view will be automatically determined by the range of latitude/longitude data in the map layers if provided.Both leaflet() and the map layer functions have an optional data parameter that is designed to receive spatial data in one of several forms:
The data argument is used to derive spatial data for functions that need it; for example, if data is a SpatialPolygonsDataFrame object, then calling addPolygons on that map widget will know to add the polygons from that SpatialPolygonsDataFrame.
It is straightforward to derive these variables from sp objects since they always represent spatial data in the same way. On the other hand, for a normal matrix or data frame, any numeric column could potentially contain spatial data. So we resort to guessing based on column names:
You can always explicitly identify latitude/longitude columns by providing lng and lat arguments to the layer function.
For example, we do not specify the values for the arguments lat
and lng
in addCircles()
below, but the columns Lat and Long in the data frame df will be automatically used:
# add some circles to a map
df = data.frame(Lat = 1:10, Long = rnorm(10))
leaflet(df) %>% addCircles()
You can also explicitly specify the Lat and Long columns (see below for more info on the ~ syntax):
leaflet(df) %>% addCircles(lng = ~Long, lat = ~Lat)
library(maps)
mapStates = map("state", fill = TRUE, plot = FALSE)
leaflet(data = mapStates) %>% addTiles() %>%
addPolygons(fillColor = topo.colors(10, alpha = NULL), stroke = FALSE)
Leaflet supports basemaps using map tiles, popularized by Google Maps and now used by nearly all interactive web maps.
The easiest way to add tiles is by calling addTiles()
with no arguments; by default, OpenStreetMap tiles are used.
m <- leaflet() %>% setView(lng = -71.0589, lat = 42.3601, zoom = 12)
m %>% addTiles()
Alternatively, many popular free third-party basemaps can be added using the addProviderTiles() function, which is implemented using the leaflet-providers plugin. See here for the complete set.
As a convenience, leaflet also provides a named list of all the third-party tile providers that are supported by the plugin. This enables you to use auto-completion feature of your favorite R IDE (like RStudio) and not have to remember or look up supported tile providers; just type providers$ and choose from one of the options. You can also use names(providers) to view all of the options.
m %>% addProviderTiles(providers$Stamen.Toner)
Icon markers are added using the addMarkers or the addAwesomeMarkers functions. Their default appearance is a dropped pin. As with most layer functions, the popup argument can be used to add a message to be displayed on click, and the label option can be used to display a text label either on hover or statically.
data(quakes)
Show first 20 rows from the quakes
dataset:
leaflet(data = quakes[1:20,]) %>% addTiles() %>%
addMarkers(~long, ~lat, popup = ~as.character(mag), label = ~as.character(mag))
You can provide custom markers in one of several ways, depending on the scenario. For each of these ways, the icon can be provided as either a URL or as a file path.
For the simple case of applying a single icon to a set of markers, use makeIcon()
.
greenLeafIcon <- makeIcon(
iconUrl = "https://leafletjs.com/examples/custom-icons/leaf-green.png",
iconWidth = 38, iconHeight = 95,
iconAnchorX = 22, iconAnchorY = 94,
shadowUrl = "https://leafletjs.com/examples/custom-icons/leaf-shadow.png",
shadowWidth = 50, shadowHeight = 64,
shadowAnchorX = 4, shadowAnchorY = 62
)
leaflet(data = quakes[1:4,]) %>% addTiles() %>%
addMarkers(~long, ~lat, icon = greenLeafIcon)
** If the custom icons are not displaying in your viewer, click the “Show in new window” button to open it in your web browser. They should correctly display then.
If you have several icons to apply that vary only by a couple of parameters (i.e. they share the same size and anchor points but have different URLs), use the icons()
function. icons()
performs similarly to data.frame()
, in that any arguments that are shorter than the number of markers will be recycled to fit.
quakes1 <- quakes[1:10,]
Example of complete customization of markers (naval battle between Dr. Josh Gray & Hadley Wickham for the rights to the land of spatial R.)
leafIcons <- icons(
iconUrl = ifelse(quakes1$mag < 4.6,
"https://cnr.ncsu.edu/geospatial/wp-content/uploads/sites/12/2017/06/Josh_Gray-400x400.jpg",
"https://pbs.twimg.com/profile_images/905186381995147264/7zKAG5sY.jpg"
),
iconWidth = 38, iconHeight = 38,
iconAnchorX = 22, iconAnchorY = 94
# shadowUrl = "http://leafletjs.com/examples/custom-icons/leaf-shadow.png",
# shadowWidth = 50, shadowHeight = 64,
# shadowAnchorX = 4, shadowAnchorY = 62
)
leaflet(data = quakes1) %>% addTiles() %>%
addMarkers(~long, ~lat, icon = leafIcons)
Finally, if you have a set of icons that vary in multiple parameters, it may be more convenient to use the iconList()
function. It lets you create a list of (named or unnamed) makeIcon()
icons, and select from that list by position or name.
if (!require(geojsonio)) {
install.packages("geojsonio")
library(geojsonio)
}
nycounties <- geojsonio::geojson_read("https://raw.githubusercontent.com/codeforamerica/click_that_hood/master/public/data/new-york-counties.geojson", what = "sp")
pal <- colorFactor("viridis", NULL)
leaflet::leaflet(nycounties) %>%
addTiles() %>%
addPolygons(stroke = FALSE, smoothFactor = 0.3, fillOpacity = 1,
fillColor = ~pal(nycounties$geoid),
label = ~paste0(nycounties$name, ": ", formatC(nycounties$name,
big.mark = ",")))
if (!require(leaflet.extras)) {
install.packages("leaflet.extras")
library(leaflet.extras)
}
airports <- readr::read_file(
system.file("examples/data/gpx/md-airports.gpx.zip", package = "leaflet.extras")
)
leaflet() %>%
addBootstrapDependency() %>%
setView(-76.6413, 39.0458, 8) %>%
addProviderTiles(
providers$CartoDB.Positron,
options = providerTileOptions(detectRetina = TRUE)
) %>%
addWebGLGPXHeatmap(airports, size = 20000, group = "airports", opacity = 0.9) %>%
addGPX(
airports,
markerType = "circleMarker",
stroke = FALSE, fillColor = "black", fillOpacity = 1,
markerOptions = markerOptions(radius = 1.5),
group = "airports"
)
* Note. The “unofficial” leaflet packages such as leaflet.esri
, leaflet.extras
, leafletCN
, and leafletR
, can occasionally be combative with the main leaflet
package and its associated functions. So, it can be helpful, and definitely headache-preventing, to detach an unofficial leaflet package whenever it is done being called to prevent potential fights betwen the leaflet package family:
detach(package:leaflet.extras, unload = TRUE)
Two-dimensional RasterLayer objects (from the raster package) can be turned into images and added to Leaflet maps using the addRasterImage
function.
Because the addRasterImage
function embeds the image in the map widget, it will increase the size of the generated HTML proportionally. If you have a large raster layer, you can provide a larger number of bytes and see how it goes, or use raster::resample
or raster::aggregate
to decrease the number of cells.
In order to render the RasterLayer as an image, each cell value must be converted to an RGB(A) color. You can specify the color scale using the colors argument, which accepts a variety of color specifications:
The name of a Color Brewer 2 palette. If no colors argument is provided, then “Spectral” is the default. A vector that represents the ordered list of colors to map to the data. Any color specification that is accepted by grDevices::col2rgb
can be used, including “#RRGGBB” and “#RRGGBBAA” forms. Example: colors = c("#E0F3DB", "#A8DDB5", "#43A2CA")
.
I’ve created a colorful raster overlaid on a base map for you to get a general sense of how the coloring operations function:
r <- raster(xmn = -2.8, xmx = -2.79, ymn = 54.04, ymx = 54.05, nrows = 30, ncols = 30)
values(r) <- matrix(1:900, nrow(r), ncol(r), byrow = TRUE)
crs(r) <- CRS("+init=epsg:4326")
if (requireNamespace("rgdal")) {
leaflet() %>% addTiles() %>%
addRasterImage(r, colors = "Spectral", opacity = 0.8)
}
First, get point data–cyclists’ locational data collected in Netherlands on cycle highway:
Nl_cyc <- read.table("https://raw.githubusercontent.com/gcmillar/3D-Buildings-NL/master/Nl_cyc_random.csv", header = TRUE, row.names=NULL, sep=",")
Then, make data spatial (spatialpointsdataframe):
setnames(Nl_cyc, "Longitude", "lon")
setnames(Nl_cyc, "Latitude", "lat")
coordinates(Nl_cyc) <- ~ lon + lat
proj4string(Nl_cyc) <- CRS('+proj=longlat +datum=WGS84')
Custom map tiles can be called on using external urls. The example below uses a 3rd-party tile provider called Thunderforest. It is a very similar provider to OpenStreetMap, it just offers a different range of tile styles. Usually when this approach is taken, you need to insert your API key within the link that is called on for grabbing the tile’s style (at very end of url): "apikey=f402a17480854b188376a96ff65cb87f"
** ↑ That is my API key for Thunderforest. ↑ **
Feel free to use it for today as I will be sure to change it when done with the demo. It is very simple to obtain your own from Thunderforest and other map tile providers such as Mapbox. It usually only requires you to sign up (for free) and register your email address. If you plan on making a good deal of maps in the next 3-4 years. I would highly suggest doing this and really learning how you can access different tile styles from multiple providers. Better yet, if you could learn to customize your own tiles and use them in the maps you design and create, the cartographic world is yours.
library(leaflet)
OpenCycleMap = "https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=f402a17480854b188376a96ff65cb87f"
Transport = "https://tile.thunderforest.com/transport/{z}/{x}/{y}.png?apikey=f402a17480854b188376a96ff65cb87f"
Landscape = "https://tile.thunderforest.com/landscape/{z}/{x}/{y}.png?apikey=f402a17480854b188376a96ff65cb87f"
Transport.Dark = "https://tile.thunderforest.com/transport-dark/{z}/{x}/{y}.png?apikey=f402a17480854b188376a96ff65cb87f"
Setting color palette for cyclists’ points that will be mapped and colored by elevation at each location (red = high; blue = low).
conduct.pal <- colorNumeric (c("dodgerblue4", "slategray2", "red3"),
Nl_cyc$Elevation)
And finally, call custom map tiles and insert them into your leaflet map as toggable layers.
m <- leaflet() %>%
addTiles(urlTemplate = Landscape, group = "Landscape") %>%
addTiles(urlTemplate = OpenCycleMap, group = "Open Cycle Map") %>%
addProviderTiles("Esri.WorldTopoMap", group = "Topographical") %>%
addProviderTiles("OpenStreetMap.Mapnik", group = "Road map") %>%
addProviderTiles("Esri.WorldImagery", group = "Satellite") %>%
addTiles(urlTemplate = Transport, group = "Transport") %>%
addTiles(urlTemplate = Transport.Dark, group = "Transport Dark") %>%
addCircles (data = Nl_cyc, group='Participant 1', stroke = T, radius = 80,
fillOpacity = 0.2, fillColor = conduct.pal(Nl_cyc$Elevation),
opacity = 0.2, color = conduct.pal(Nl_cyc$Elevation)) %>%
# Layers control
addLayersControl(position = 'bottomright',
baseGroups = c("Landscape", "Open Cycle Map", "Topographical",
"Road map", "Satellite","Transport",
"Transport Dark"),
overlayGroups = c("Participant 1"),
options = layersControlOptions(collapsed = FALSE)) %>%
addLegend(values = Nl_cyc$Elevation, pal = conduct.pal,
opacity = 1, title = "Elevation", position = "bottomleft")
m
Shiny is a web framework for R. To learn more about Shiny, visit shiny.rstudio.com.
The Leaflet package includes powerful and convenient features for integrating with Shiny applications.
Most Shiny output widgets are incorporated into an app by including an output (e.g. plotOutput
) for the widget in the UI definition, and using a render function (e.g. renderPlot
) in the server function. Leaflet maps are no different; in the UI you call leafletOutput
, and on the server side you assign a renderLeaflet
call to the output. Inside the renderLeaflet
expression, you return a Leaflet map object.
Leaflet maps and objects send input values (which we’ll refer to as “events” in this document) to Shiny as the user interacts with them.
Object event names generally use this pattern: input$MAPID_OBJCATEGORY_EVENTNAME
So for a leafletOutput("mymap")
had a circle on it, clicking on that circle would update the Shiny input at input$mymap_shape_click
. (Note that the layer ID is not part of the name, though it is part of the value.)
If no shape has ever been clicked on this map, then input$mymap_shape_click
is null.
Valid values for OBJCATEGORY above are marker, shape, geojson, and topojson. (Tiles and controls don’t raise mouse events.) Valid values for EVENTNAME are click, mouseover, and mouseout.
All of these events are set to either NULL if the event has never happened, or a list()
that includes:
GeoJSON events also include additional properties:
The map itself also has a few input values/events:
input$MAPID_click
is an event that is sent when the map background or basemap is clicked. The value is a list with lat and lng.
input$MAPID_bounds
provides the latitude/longitude bounds of the currently visible map area; the value is a list()
that has named elements north, east, south, and west.
input$MAPID_zoom
is an integer that indicates the zoom level.
input$MAPID_center
provides the latitude/longtitude of the center of the currently visible map area; the value is a list() that has named elements lat and lng.
library(shiny)
library(leaflet)
library(dygraphs)
library(dplyr)
library(rgdal)
Let’s build our data directory in advance so we don’t have to download the data every time.
tmp <- tempdir()
url <- "http://personal.tcu.edu/kylewalker/data/mexico.zip"
file <- basename(url)
download.file(url, file)
unzip(file, exdir = tmp)
mexico <- {
on.exit({unlink(tmp);unlink(file)}) #delete our files since no longer need
readOGR(dsn = tmp, layer = "mexico", encoding = "UTF-8")
}
## OGR data source with driver: ESRI Shapefile
## Source: "/private/var/folders/tz/xhbz8_6x675fjtnn2rwyrwh40000gn/T/RtmpXUOqix", layer: "mexico"
## with 32 features
## It has 9 fields
## Integer64 fields read as strings: id
Now let’s get our time series data from Diego Valle.
crime_mexico <- jsonlite::fromJSON(
"https://rawgit.com/diegovalle/crimenmexico.diegovalle.net/master/assets/json/states.json"
)
Instead of the GDP data, let’s use mean homicide_rate
for our choropleth.
mexico$homicide <- crime_mexico$hd %>%
group_by( state_code ) %>%
summarise( homicide = mean(rate) ) %>%
ungroup() %>%
dplyr::select( homicide ) %>%
unlist
pal <- colorBin(
palette = RColorBrewer::brewer.pal(n = 9, "YlGn")[-(1:2)],
domain = c(0, 50), bins =7)
popup <- paste0("<strong>Estado: </strong>",
mexico$name, "<br><strong>Homicide Rate: </strong>",
round(mexico$homicide, 2))
leaf_mexico <- leaflet(data = mexico) %>%
addTiles() %>%
addPolygons(fillColor = ~pal(homicide),
fillOpacity = 0.8, color = "#BDBDC3", weight = 1,
layerId = ~id, popup = popup)
ui <- fluidPage(
leafletOutput("map1"), dygraphOutput("dygraph1",height = 200),
textOutput("message", container = h3)
)
server <- function(input, output, session) {
v <- reactiveValues(msg = "")
output$map1 <- renderLeaflet({
leaf_mexico
})
output$dygraph1 <- renderDygraph({
# start dygraph with all the states
crime_wide <- reshape(
crime_mexico$hd[,c("date","rate","state_code"),drop=F],
v.names="rate",
idvar = "date",
timevar="state_code",
direction="wide"
)
colnames(crime_wide) <- c("date",as.character(mexico$state))
rownames(crime_wide) <- as.Date(crime_wide$date)
dygraph( crime_wide[,-1]) %>%
dyLegend( show = "never" )
})
observeEvent(input$dygraph1_date_window, {
if(!is.null(input$dygraph1_date_window)){
# get the new mean based on the range selected by dygraph
mexico$filtered_rate <- crime_mexico$hd %>%
filter(
as.Date(date) >= as.Date(input$dygraph1_date_window[[1]]),
as.Date(date) <= as.Date(input$dygraph1_date_window[[2]])
) %>%
group_by( state_code ) %>%
summarise( homicide = mean(rate) ) %>%
ungroup() %>%
dplyr::select( homicide ) %>%
unlist
# leaflet comes with this nice feature leafletProxy
# to avoid rebuilding the whole map
# let's use it
leafletProxy( "map1", data = mexico ) %>%
removeShape( layerId = ~id ) %>%
addPolygons( fillColor = ~pal( filtered_rate ),
fillOpacity = 0.8,
color = "#BDBDC3",
weight = 1,
layerId = ~id,
popup = paste0("<strong>Estado: </strong>",
mexico$name,
"<br><strong>Homicide Rate: </strong>",
round(mexico$filtered_rate,2)
)
)
}
})
observeEvent(input$map1_shape_click, {
v$msg <- paste("Clicked shape", input$map1_shape_click$id)
# on our click let's update the dygraph to only show
# the time series for the clicked
state_crime_data <- subset(crime_mexico$hd,state_code == input$map1_shape_click$id)
rownames(state_crime_data) <- as.Date(state_crime_data$date)
output$dygraph1 <- renderDygraph({
dygraph(
xts::as.xts(state_crime_data[,"rate",drop=F]),
ylab = paste0(
"homicide rate ",
as.character(mexico$state[input$map1_shape_click$id])
)
)
})
})
}
When using Shiny, you should always reserve the following as the final print / call statement. It has been commented out since it requires constant connection to server:
# shinyApp(ui, server)