Interactive maps with Shiny and Leaflet

Glenn Moncrieff
Fynbos Node, South African Environmental Observation Network (SAEON)
Department of Botany and Zoology, Stellenbosch University

18 August 2016

Background

Disclaimer: Much of this material is adapted/borrowed/stolen from this tutorial on leaflet for R

Lets setup our first leaflet map:

library(leaflet)

mymap1 <- leaflet() %>%
  setView(lat = -33.9249, lng= 18.4241, zoom = 8) %>%  # set map view
  addTiles() %>%                                       # Add default OpenStreetMap map background tiles
  addMarkers(lat = -34.1292, lng = 18.4462, popup="Home")

mymap1                                                  # Print the map

The basic usage of leaflet is:

  1. Create a map widget by calling leaflet().
  2. Add layers (i.e., features) to the map by using layer functions (e.g. addTiles, addMarkers, addPolygons) to modify the map widget.
  3. Repeat step 2 as desired.
  4. Print the map widget to display it.

We can change the basemap used by leaflet very easily:

mymap2 <- leaflet() %>%
    setView(lat = -33.9249, lng= 18.4241, zoom = 8) %>%   # set map view
    addProviderTiles("OpenTopoMap") %>%                   # Add topographic map tiles                             
    addMarkers(lat = -34.1292, lng = 18.4462, popup="Home")
mymap2                                                     # Print the map

or

mymap3 <- leaflet() %>%
    setView(lat = -33.9249, lng= 18.4241, zoom = 8) %>%   # set map view
    addProviderTiles("NASAGIBS.ViirsEarthAtNight2012") %>% # Add NASA nightview map tiles                         
    addMarkers(lat = -34.1292, lng = 18.4462, popup="Home")
mymap3                                                     # Print the map

Here you will find a viewer for all available base maps. All you have to do is change the argument in addProviderTiles

The leaflet map widget

mymap is a leaflet widget - essentially a list of object that make up the map

class(mymap1)
## [1] "leaflet"    "htmlwidget"
str(mymap1)                                                   # Print widget
## List of 8
##  $ x            :List of 3
##   ..$ setView:List of 3
##   .. ..$ : num [1:2] -33.9 18.4
##   .. ..$ : num 8
##   .. ..$ : list()
##   .. .. ..- attr(*, "class")= chr "list"
##   .. ..- attr(*, "class")= chr "list"
##   ..$ calls  :List of 2
##   .. ..$ :List of 2
##   .. .. ..$ method: chr "addTiles"
##   .. .. ..$ args  :List of 4
##   .. .. .. ..$ : chr "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
##   .. .. .. ..$ : NULL
##   .. .. .. ..$ : NULL
##   .. .. .. ..$ :List of 18
##   .. .. .. .. ..$ minZoom             : num 0
##   .. .. .. .. ..$ maxZoom             : num 18
##   .. .. .. .. ..$ maxNativeZoom       : NULL
##   .. .. .. .. ..$ tileSize            : num 256
##   .. .. .. .. ..$ subdomains          : chr "abc"
##   .. .. .. .. ..$ errorTileUrl        : chr ""
##   .. .. .. .. ..$ tms                 : logi FALSE
##   .. .. .. .. ..$ continuousWorld     : logi FALSE
##   .. .. .. .. ..$ noWrap              : logi FALSE
##   .. .. .. .. ..$ zoomOffset          : num 0
##   .. .. .. .. ..$ zoomReverse         : logi FALSE
##   .. .. .. .. ..$ opacity             : num 1
##   .. .. .. .. ..$ zIndex              : NULL
##   .. .. .. .. ..$ unloadInvisibleTiles: NULL
##   .. .. .. .. ..$ updateWhenIdle      : NULL
##   .. .. .. .. ..$ detectRetina        : logi FALSE
##   .. .. .. .. ..$ reuseTiles          : logi FALSE
##   .. .. .. .. ..$ attribution         : chr "&copy; <a href=\"http://openstreetmap.org\">OpenStreetMap</a> contributors, <a href=\"http://creativecommons.org/licenses/by-sa"| __truncated__
##   .. .. .. .. ..- attr(*, "class")= chr "list"
##   .. .. .. ..- attr(*, "class")= chr "list"
##   .. ..$ :List of 2
##   .. .. ..$ method: chr "addMarkers"
##   .. .. ..$ args  :List of 9
##   .. .. .. ..$ : num -34.1
##   .. .. .. ..$ : num 18.4
##   .. .. .. ..$ : NULL
##   .. .. .. ..$ : NULL
##   .. .. .. ..$ : NULL
##   .. .. .. ..$ :List of 9
##   .. .. .. .. ..$ clickable   : logi TRUE
##   .. .. .. .. ..$ draggable   : logi FALSE
##   .. .. .. .. ..$ keyboard    : logi TRUE
##   .. .. .. .. ..$ title       : chr ""
##   .. .. .. .. ..$ alt         : chr ""
##   .. .. .. .. ..$ zIndexOffset: num 0
##   .. .. .. .. ..$ opacity     : num 1
##   .. .. .. .. ..$ riseOnHover : logi FALSE
##   .. .. .. .. ..$ riseOffset  : num 250
##   .. .. .. .. ..- attr(*, "class")= chr "list"
##   .. .. .. ..$ : chr "Home"
##   .. .. .. ..$ : NULL
##   .. .. .. ..$ : NULL
##   .. .. .. ..- attr(*, "class")= chr "list"
##   ..$ limits :List of 2
##   .. ..$ lat: num [1:2] -34.1 -34.1
##   .. ..$ lng: num [1:2] 18.4 18.4
##  $ width        : NULL
##  $ height       : NULL
##  $ sizingPolicy :List of 6
##   ..$ defaultWidth : chr "100%"
##   ..$ defaultHeight: num 400
##   ..$ padding      : num 0
##   ..$ viewer       :List of 6
##   .. ..$ defaultWidth : NULL
##   .. ..$ defaultHeight: NULL
##   .. ..$ padding      : NULL
##   .. ..$ fill         : logi TRUE
##   .. ..$ suppress     : logi FALSE
##   .. ..$ paneHeight   : NULL
##   ..$ browser      :List of 4
##   .. ..$ defaultWidth : NULL
##   .. ..$ defaultHeight: NULL
##   .. ..$ padding      : NULL
##   .. ..$ fill         : logi TRUE
##   ..$ knitr        :List of 3
##   .. ..$ defaultWidth : NULL
##   .. ..$ defaultHeight: NULL
##   .. ..$ figure       : logi TRUE
##  $ dependencies : NULL
##  $ elementId    : NULL
##  $ preRenderHook: NULL
##  $ jsHooks      : list()
##  - attr(*, "class")= chr [1:2] "leaflet" "htmlwidget"
##  - attr(*, "package")= chr "leaflet"

Adding familiar spatial objects

leaflet functions to add map layers accept a data argument

library(rgdal) # for handling spatial objects
library(raster)# for rasters
library(knitr) # for tidy table

#load some spatial data
vegclear <- readOGR(dsn="/Users/glennmoncrieff/Documents/presentations/leaflet_CRUG",layer="vegclear_small")
## OGR data source with driver: ESRI Shapefile 
## Source: "/Users/glennmoncrieff/Documents/presentations/leaflet_CRUG", layer: "vegclear_small"
## with 948 features
## It has 123 fields
#lets work with he polygons first
class(vegclear)
## [1] "SpatialPolygonsDataFrame"
## attr(,"package")
## [1] "sp"
crs(vegclear)
## CRS arguments: +proj=longlat +ellps=WGS84 +no_defs
plot(vegclear,col=1:20,border=NA)

vegdata <- vegclear@data
head(vegdata) %>% kable  # print a tidy table
Prjct_N NbalId Area Totl_Md Ttl_Cst Nmbr_f_ Clrng_R Frst_Dt Lst_Dt_ Initl_D Initl_T Initl_M Initl_C Intl_En Int_C_1 Frst_Dm Frst_Tt Frst_Md Frst_Cs Frst_E_ Frst_Cn Scnd_Dm Scnd_Tt Scnd_Md Scnd_Cs Scnd_En Scnd_Cn Thrd_Dm Thrd_Tt Thrd_Md Thrd_Cs Thrd_E_ Thrd_Cn Frth_Dm Frth_Tt Frth_Md Frth_Cs Frth_En Frth_Cn Ffth_Dm Ffth_Tt Ffth_Md Ffth_Cs Ffth_E_ Ffth_Cn Sxth_Dm Sxth_Tt Sxth_Md Sxth_Cs Sxth_E_ Sxth_Cn Svnth_D Svnth_T Svnth_M Svnth_C Svnth_E Svn_C_1 Egth_Dm Egth_Tt Egth_Md Egth_Cs Egth_E_ Egth_Cn Nnth_Dm Nnth_Tt Nnth_Md Nnth_Cs Nnth_E_ Nnth_Cn Tnth_Dm Tnth_Tt Tnth_Md Tnth_Cs Tnth_E_ Tnth_Cn Elvnt_D Elvnt_T Elvnt_M Elvnt_C Elvnt_E Elv_C_1 Twlft_D Twlft_T Twlft_M Twlft_C Twlft_E Twl_C_1 Thrtnth Thrtn_1 Thrtn_2 Thrtn_3 Thrtn_4 Thrtn_5 Fortnth Frtnt_1 Frtnt_2 Frtnt_3 Frtnt_4 Frtnt_5 IntlS_D IntlS_T IntlS_M IntlS_C IntlS_E InS_C_1 Nth_Dmn Nth_Tt_ Nth_Mdl Nth_Cst Nth_E_D Nth_Cnt Mantnnc Mntnn_1 Mntnn_2 Mntnn_3 Mntnn_4 Mntnn_5 Rehablt Rhblt_1 Rhblt_2 Rhblt_3 Rhblt_4 Rhblt_5
0 SANP WfW TMNP N G22B400002 14.84 90.96 18429.51 1 No Restriction 20130212 20130225 Acacia podalyriifolia 70 90.96 18429.51 20130225 G22B400100304 NA 0.0 0.00 0.00 NA NA NA 0.0 0.00 0.00 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA
1 SANP WfW TMNP N G22B400003 2.52 0.00 0.00 0 No Restriction NA NA NA 0 0.00 0.00 NA NA NA 0.0 0.00 0.00 NA NA NA 0.0 0.00 0.00 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA
2 SANP WfW TMNP N G22B400004 3.08 0.00 0.00 0 No Restriction NA NA NA 0 0.00 0.00 NA NA NA 0.0 0.00 0.00 NA NA NA 0.0 0.00 0.00 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA
3 SANP WfW TMNP N G22B400005 13.78 150.52 26657.00 2 No Restriction 20130212 20140218 NA 0 0.00 0.00 NA NA Pinus spp 17.0 90.08 16232.18 20130222 G22B400100290 Acacia saligna 39.5 60.44 10424.82 20140218 G22B400100410 NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA
4 SANP WfW TMNP N G22B400006 13.85 678.08 139025.68 2 No Restriction 20130131 20140313 NA 0 0.00 0.00 NA NA Acacia longifolia 96.6 391.80 78214.26 20130228 G22B400100291 Acacia saligna 202.0 286.28 60811.42 20140313 G22B400100416 NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA
5 SANP WfW TMNP N G22B400007 12.71 300.40 70364.79 1 No Restriction 20130528 20130624 Acacia saligna 157 300.40 70364.79 20130624 G22B400100324 NA 0.0 0.00 0.00 NA NA NA 0.0 0.00 0.00 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA NA 0 0 0 NA NA

Adding our shape to leaflet map

#create color pallete
pal <- colorNumeric(
  palette = "Blues",
  domain = vegclear$Ttl_Cst
)

#add polygons to leaflet map
mymap5 <- leaflet() %>% 
  
    setView(lat = -33.9249, lng= 18.4241, zoom = 10) %>%   # set map view
  
    addProviderTiles("Esri.WorldImagery") %>% 
  
    addPolygons(data=vegclear,
    stroke = FALSE, fillOpacity = 0.8, smoothFactor = 0.5,
    color = ~pal(Ttl_Cst), 
    popup = paste0("Cost of clearing: ", as.character(vegclear$Ttl_Cst))
    ) %>%
    
    addLegend("bottomright", pal = pal, values = vegclear$Ttl_Cst,
    title = "Total cost of alien clearing",
    labFormat = labelFormat(prefix = "R"),
    opacity = 1
    )
    
mymap5

Rasters and thinking about projections

Leaflet uses the web mercator projection, and any spatial objects displayed by the leaflet widget appear in this projection.

#load raster
temp <-  raster("/Users/glennmoncrieff/Documents/presentations/leaflet_CRUG/Tmax_jan_mean")

crs(temp)
## CRS arguments:
##  +proj=utm +zone=34 +south +datum=WGS84 +units=m +no_defs
## +ellps=WGS84 +towgs84=0,0,0
#raster color palette
pal <- colorNumeric(
  palette = rev(heat.colors(20)),
  domain = values(temp),
  na.color = "transparent"
)

mymap6 <- leaflet() %>% 
  setView(lat = -33.9249, lng= 18.4241, zoom = 10) %>%   # set map view
  addTiles() %>%
  addRasterImage(temp, colors = pal, opacity = 0.8) %>%
  addLegend("bottomright", pal = pal, values = values(temp),title = "Jan maximum temp")

mymap6

So leaflet reprojects for us anyway. But this comes at a performance cost.

#reproject for leaflet
temp_leaf <- projectRasterForLeaflet(temp)

crs(temp_leaf)
## CRS arguments:
##  +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0
## +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs
#without projecting for leaflet:

system.time(
  mymap6 <- leaflet() %>% 
  setView(lat = -33.9249, lng= 18.4241, zoom = 10) %>%   # set map view
  addTiles() %>%
  addRasterImage(temp, colors = pal, opacity = 0.8) %>%
  addLegend("bottomright", pal = pal, values = values(temp),title = "Jan maximum temp"))
##    user  system elapsed 
##   2.498   0.514   3.118
#after reprojection

system.time(
  mymap7 <- leaflet() %>% 
  setView(lat = -33.9249, lng= 18.4241, zoom = 10) %>%   # set map view
  addTiles() %>%
  addRasterImage(temp_leaf, colors = pal, opacity = 0.8, project = FALSE) %>%
  addLegend("bottomright", pal = pal, values = values(temp_leaf),title = "Jan maximum temp"))
##    user  system elapsed 
##   0.557   0.043   0.601

Integration with Shiny

library(shiny)

ui <- fluidPage(
  leafletOutput("mymap")
)

server <- function(input, output, session) {
  
  map <- leaflet() %>%
          setView(lat = -33.9249, lng= 18.4241, zoom = 8) %>%  # set map view
          addTiles() %>%                                       # Add default OpenStreetMap map background tiles
          addMarkers(lat = -34.1292, lng = 18.4462, popup="Home")
  
  output$mymap <-  renderLeaflet(map) 
  }
  
shinyApp(ui=ui, server=server)

updating the map with leafletProxy

library(shiny)

ui <- fluidPage(
  leafletOutput("mymap")
)

server <- function(input, output, session) {
  
  map <- leaflet() %>%
          setView(lat = -33.9249, lng= 18.4241, zoom = 8) %>%  # set map view
          addTiles() %>%                                       # Add default OpenStreetMap map background tiles
          addMarkers(lat = -34.1292, lng = 18.4462)
  
  output$mymap <-  renderLeaflet(map) 
  
  observeEvent(input$mymap_marker_click, {
        leafletProxy("mymap") %>% clearMarkers()
    })
  }
  
shinyApp(ui=ui, server=server)

Lets take a look at a real (in progress) example

Vegetation mapping application at https://github.com/GMoncrieff/ShinyVeg