25  Interactive Web Maps (leaflet)

25.1 Topics Covered

  1. Adding base maps and associated controls
  2. Defining extents and zoom levels
  3. Adding operational vector layers
  4. Symbolizing vector layers
  5. Configuring pop-ups
  6. Adding layer controls
  7. Adding legends
  8. Using panes
  9. Rendering web maps

25.2 Introduction

Leaflet is a free and open-source JavaScript library and Application Programming Interface (API) for developing interactive web maps. It is very powerful and offers a free alternative to proprietary web mapping software, such as ArcGIS Online and the ArcGIS Maps SDK for JavaScript. It is used by many organizations including National Public Radio (NPR), The Washington Post, Facebook, GitHub, USA Today, and the European Commission. Here is a link to the official Leaflet webpage.

Since Leaflet is meant to be used in a web development environment, it makes use of JavaScript, the dominant client-side programming language. So, to use it you need to have some understanding of JavaScript. It also doesn’t hurt to have knowledge of Hypertext Markup Language (HTML) and Cascading Style Sheets (CSS). So, there is a bit of a learning curve. Fortunately, the leaflet package allows for the use of Leaflet without the need to learn JavaScript. Interactive web maps can be created using R code then rendered to an HTML webpage. Since this text focuses on R, we will demonstrate using Leaflet through the associated R package. If you have an interest in web development, we would recommend learning some HTML, CSS, and JavaScript; however, that is outside the scope of this text.

We will use a variety of packages in this module. If you don’t have these packages installed yet, you will need to do so before proceeding.

25.3 Data Preparation

In this demonstration, we will produce an interactive web map to visualize data summarized by county for the high plains states in the United States. The elevation, temperature, and precipitation data were extracted from raster grids provided by the PRISM Climate Group at Oregon State University. The elevation data are provided in feet, the temperature data represent 30-year annual normal mean temperature in degrees Celsius, and the precipitation data represent 30-year annual normal precipitation in millimeters. The original raster grids have a resolution of 4-by-4 kilometers and can be obtained here. We also summarized percent forest by county from the 2011 National Land Cover Database (NLCD). NLCD data can be obtained from the Multi-Resolution Land Characteristics Consortium (MRLC).

We begin by reading in the spatial data using the sf package. Generally, web maps are generated using the WGS84 Web Mercator projection. So, data that will be used in a web map should be transformed to that projection. Or, they could be referenced to the WGS84 geographic datum. We have chosen to use the WGS84 Web Mercator projection, as defined by European Petroleum Survey Group (EPSG) code 4326. st_transform() is used to make the transformation. We then change the column names to make them more interpretable.

The cities point data contains an ordinal field representing different population range categories as follows:

  • 5 = 2,500 to 9,999,
  • 6 = 10,000 to 49,999
  • 7 = 50,000 to 99,999
  • 8 = 100,000 to 499,999
  • 9 = 500,000 to 999,999

In order to display the categories separately, we split the data into new objects using the filter() function from dplyr.

fldrPth <- "gslrData/chpt25/data/"

cities_wm <- st_read(dsn=str_glue("{fldrPth}chpt25.gpkg"), 
                     "city_points", 
                     quiet=TRUE) |> 
  st_transform(crs=4326)

precip_wm <- st_read(dsn=str_glue("{fldrPth}/chpt25.gpkg"), 
                     "counties_precip", 
                     quiet=TRUE) |>
  st_transform(crs=4326)

temp_wm <- st_read(dsn=str_glue("{fldrPth}chpt25.gpkg"), 
                                "county_temp", 
                                quiet=TRUE) |>
  st_transform(crs=4326)

per_for_wm <- st_read(dsn=str_glue("{fldrPth}chpt25.gpkg"), 
                      "county_per_forest", 
                      quiet=TRUE) |>
  st_transform(cities, crs=4326)

states_wm <- st_read(dsn=str_glue("{fldrPth}chpt25.gpkg"), 
                     "hp_states_wm", 
                     quiet=TRUE) |>
  st_transform(crs=4326)

names(per_for_wm) <- c("county", "per_for", "geometry")
names(precip_wm) <- c("county", "precip", "geometry")
names(temp_wm) <- c("county", "temp", "geometry")

st_geometry(per_for_wm) <- "geometry"
st_geometry(precip_wm) <- "geometry"
st_geometry(temp_wm) <- "geometry"

cities_5 <- cities_wm |> filter(POP_CLASS==5)
cities_6 <- cities_wm |> filter(POP_CLASS==6)
cities_7 <- cities_wm |> filter(POP_CLASS==7)
cities_8 <- cities_wm |> filter(POP_CLASS==8)
cities_9 <- cities_wm |> filter(POP_CLASS==9)

25.4 Interactive Map Design

25.4.1 Base maps and Extents

Generally, you start designing a web map by defining a base map and a loading extent and zoom level. In the first example, we are simply creating a map with the default base map, extent, and zoom level by initializing the map with the leaflet() function then adding the base map using addTiles(). Raster tile layers represent pictures or images of raw data as opposed to the original geospatial data files. They are stored as tiles that change based on the zoom level. Most base maps are stored as raster tile layers.

In the example above, we did not explicitly state the desired extent and zoom level, and by default the map loaded to a global extent. The setView() function is used to define the extent using latitude and longitude coordinates. The zoom level is then defined. Larger numbers indicate more zoomed in. A zoom level of 1 indicates a global extent. Take some time to change the coordinates and zoom level to see how this impacts the map. Note that positive values indicate north latitude or east longitude while negative values indicate south latitude or west longitude.

leaflet() |>
  addTiles() |>
  setView(lat= 37.1, lng=-95.7, zoom=4)

It is also possible to limit the user’s ability to zoom and pan using leafletOptions(). In the next code block, we define a loading extent and zoom level. Using minZoom and maxZoom, we limit the allowed zoom levels. Setting dragging to FALSE will not allow the user to pan the map.

We will not use these options to produce our maps; however, we wanted to demo them, as this can be useful in many situations.

leaflet(options=leafletOptions(center=c(54.5, 15.2), 
                               zoom =3, 
                               minZoom=2, 
                               maxZoom=5, 
                               dragging=FALSE)) |>
  addTiles()

There are many base maps available other than the default OpenStreetMap base map. To print a list of available base maps, you can use the code shown below. We have had issues in the past getting all of these layers to work depending on the computer and/or browser being used. Unfortunately, we have not found a workaround for this issue. So, if you cannot get a base map to work, we recommend simply use a different one. Since the vector of base map names is long, we did not print it on the pages; however, you can run the code to obtain the vector.

names(providers)

The next code block demonstrates loading a different base map by providing the layer’s name within the addProviderTiles() function. Copyright and attribution information are automatically added to the interactive map. Take some time to experiment with different base maps by altering the example code.

leaflet() |>
  addProviderTiles("Esri.NatGeoWorldMap") |>
  setView(lat= 54.5, lng=15.2, zoom=3)

What if you would like the user to be able to switch between some pre-defined base maps? This can be accomplished by loading in multiple layers using addTiles() or addProviderTiles(). We then need to use addLayersControl() to create a widget to switch between the base maps. In order to reference the layers in the addLayersControl() function, we need to assign a unique name to each one using the group parameter. These names are then used in the widget. A collapse argument can be added to the addLayersControl() function to indicate whether or not it should collapse.

leaflet() |>
  addTiles(group = "OSM") |>
  addProviderTiles("Esri.NatGeoWorldMap", group="ESRI") |>
  addProviderTiles("CartoDB.DarkMatter", group= "CartoDB") |>
  addLayersControl(baseGroups = c("OSM", "ESRI", "CartoDB")) |>
  setView(lat= 42, lng=-105, zoom=5)

25.4.2 Operational Layers

25.4.2.1 Point Data

We now have a map with three base map layers that the user can choose between. We have also defined an initial extent and zoom level. However, we haven’t added any operational layers yet.

To begin, we add the smallest cities to the map using addMarkers(). Within the function, we must specify the data to be used. We also define the content of the pop-up using the popup argument. This will simply print “Some Text” in each pop-up.

Throughout the remaining examples, you will see the htmlEscape() function from the htmltools package used often. This function is used to make sure that pop-up content is not incorrectly interpreted as HTML tags or code. Also, it should be used to avoid any security risks. In short, you should use this function when creating pop-ups.

leaflet() |>
  addTiles(group = "OSM") |>
  addProviderTiles("Esri.NatGeoWorldMap", group="ESRI") |>
  addProviderTiles("CartoDB.DarkMatter", group= "CartoDB") |>
  addMarkers(data=cities_5, popup = ~htmlEscape(paste0("Some Text"))) |>
  addLayersControl(baseGroups = c("OSM", "ESRI", "CartoDB")) |>
  setView(lat= 42, lng=-105, zoom=5)

Other symbology options are available for point data. In this example, we use addCircleMarkers() as opposed to addMarkers(). We also now displaying all the city population groups using multiple addCircleMarkers() calls. Using the radius and color arguments, we specify sizes and colors for each symbol. We set fillOpacity to 1 so that no transparency is applied and the stroke to FALSE so that no outline is included. We also provide a group name for each point layer.

The pop-ups are more complicated than the above example. We have the pop-up provide the city name and the city population. Bold tags are HTML tags used to bold text while the break tag indicates a line break. Note that these components are not wrapped in htmlEscape() since we do want them to be interpreted as HTML tags as opposed to text. We use the base R paste0() function to combine all arguments into a single string. We also indicate that we want comma separators included in the population numbers.

You may have noticed the use of ~ in the code. This allows for the data object to be passed on. So, we can just use the column names in the pop-up syntax and do not need to define the data object.

leaflet() |>
  addTiles(group = "OSM") |>
  addProviderTiles("Esri.NatGeoWorldMap", group="ESRI") |>
  addProviderTiles("CartoDB.DarkMatter", group= "CartoDB") |>
  addCircleMarkers(data=cities_5, 
                   group="2,500 to 9,999", 
                   radius=3, 
                   color="#ffffd4", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_6, 
                   group="10,000 to 49,999", 
                   radius=5, 
                   color="#fed98e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_7, 
                   group="50,000 to 90,999", 
                   radius=7, 
                   color="#fe9929", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_8, 
                   group="100,000 to 499,999", 
                   radius=9, 
                   color="#d95f0e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_9, 
                   group="500,000 to 999,999", 
                   radius=11, 
                   color="#993404", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addLayersControl(baseGroups = c("OSM", "ESRI", "CartoDB")) |>
  setView(lat= 42, lng=-105, zoom=5)

Similar to the base maps, we want the user to be able to turn the point layers on and off. This is accomplished by adding an overlayGroups argument to addLayerConrols().

leaflet() |>
  addTiles(group = "OSM") |>
  addProviderTiles("Esri.NatGeoWorldMap", group="ESRI") |>
  addProviderTiles("CartoDB.DarkMatter", group= "CartoDB") |>
  addCircleMarkers(data=cities_5, 
                   group="2,500 to 9,999", 
                   radius=3, 
                   color="#ffffd4", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_6, 
                   group="10,000 to 49,999", 
                   radius=5, 
                   color="#fed98e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_7, 
                   group="50,000 to 90,999", 
                   radius=7, 
                   color="#fe9929", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_8, 
                   group="100,000 to 499,999", 
                   radius=9, 
                   color="#d95f0e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_9, 
                   group="500,000 to 999,999", 
                   radius=11, 
                   color="#993404", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addLayersControl(overlayGroups= c("2,500 to 9,999", 
                                    "10,000 to 49,999", 
                                    "50,000 to 90,999", 
                                    "100,000 to 499,999", 
                                    "500,000 to 999,999"), 
                   baseGroups = c("OSM", "ESRI", "CartoDB"),
                   options    = layersControlOptions(autoZIndex = FALSE)) |>
  setView(lat= 42, lng=-105, zoom=5)

25.4.2.2 Polygon Data

We now add some polygon layers to the map, which is accomplished using the addPolygons() function. In this first example, we add the county-level percent forest data. We do not define the symbology, so the default is used, which isn’t great for this use case.

per_for_pal <- colorNumeric(palette="Greens", domain=per_for_wm$per_for)
precip_pal <- colorNumeric(palette="Purples", domain=precip_wm$precip)
temp_pal <- colorNumeric(palette="YlOrRd", domain=temp_wm$temp)

leaflet() |>
  addTiles(group = "OSM") |>
  addProviderTiles("Esri.NatGeoWorldMap", group="ESRI") |>
  addProviderTiles("CartoDB.DarkMatter", group= "CartoDB") |>
  addPolygons(data=per_for_wm) %>%
  addCircleMarkers(data=cities_5, 
                   group="2,500 to 9,999", 
                   radius=3, 
                   color="#ffffd4", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_6, 
                   group="10,000 to 49,999", 
                   radius=5, 
                   color="#fed98e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_7, 
                   group="50,000 to 90,999", 
                   radius=7, 
                   color="#fe9929", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_8, 
                   group="100,000 to 499,999", 
                   radius=9, 
                   color="#d95f0e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_9, 
                   group="500,000 to 999,999", 
                   radius=11, 
                   color="#993404", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addLayersControl(overlayGroups= c("2,500 to 9,999", 
                                    "10,000 to 49,999", 
                                    "50,000 to 90,999", 
                                    "100,000 to 499,999", 
                                    "500,000 to 999,999"), 
                   baseGroups = c("OSM", "ESRI", "CartoDB")) |>
  setView(lat= 42, lng=-105, zoom=5)

We now add all of the polygon layers and define the desired symbology. First, we need to define the color palettes used to symbolize the data. The leaflet package offers several methods for defining color palettes:

Here, we demonstrate using colorNumeric(). First, we specify the desired palette. We use palettes provided by the RColorBrewer package. We must indicate a domain, which defines the range of values to map to, which is accomplished here by providing the data to which the color ramp will be applied.

The following points explain arguments defined in the addPolygons() functions:

  1. We state the data object to be mapped
  2. We provide the predefined color palette and map it to the desired variable
  3. We set the opacity to 1, set a border color, and define a border weight
  4. Highlight options are defined to determine how the feature will change when it is hovered over
  5. We create a pop-up that will display the county name and the variable of interest when the feature is clicked
  6. We provide a unique group name for the polygon layers.

Note that the base R round() function is used to round off numbers for cleaner display in the pop-up. The precipitation is divided by 100 so that the number is provided in millimeters (the original values were multiplied by 100 so that the data could be stored as integers). We also add units to all the measures.

Before we move on, it would be nice to give the user the ability to turn the polygon layers on and off. This is accomplished by adding the polygon layer group names to the overlayGroups list in the addLayersControl() function.

We would like to include the state boundaries above the other layers. This is accomplished using addPolylines(), which displays polygons with an outline but no fill.

per_for_pal <- colorNumeric(palette="Greens", domain=per_for_wm$per_for)
precip_pal <- colorNumeric(palette="Purples", domain=precip_wm$precip)
temp_pal <- colorNumeric(palette="YlOrRd", domain=temp_wm$temp)

leaflet() |>
  addTiles(group = "OSM") |>
  addProviderTiles("Esri.NatGeoWorldMap", group="ESRI") |>
  addProviderTiles("CartoDB.DarkMatter", group= "CartoDB") |>
  addPolygons(data=per_for_wm, 
              fillColor = per_for_pal(per_for_wm$per_for), 
              fillOpacity=1, 
              col="#302E2D", 
              weight=1,
              highlight = highlightOptions(weight = 3, 
                                           color = "white", 
                                           bringToFront = FALSE), 
              popup= paste0("<b>", 
                            htmlEscape(per_for_wm$county),
                            "</b>",
                            "<br/>",
                            htmlEscape(round(per_for_wm$per_for, 1)), "%"), 
              group="Percent Forest") |>
  addPolygons(data=precip_wm, 
              fillColor = precip_pal(precip_wm$precip), 
              fillOpacity=1, 
              col="#302E2D", 
              weight=1, 
              highlight = highlightOptions(weight = 3, 
                                           color = "white", 
                                           bringToFront = FALSE), 
              popup= paste0("<b>", htmlEscape(precip_wm$county),
                            "</b>",
                            "<br/>",
                            htmlEscape(round(precip_wm$precip/100, 1)), "mm"),
              group="Precipitation") |>
  addPolygons(data=temp_wm, 
              fillColor = temp_pal(temp_wm$temp), 
              fillOpacity=1, 
              col="#302E2D", 
              weight=1, 
              highlight = highlightOptions(weight = 3, 
                                           color = "white", 
                                           bringToFront = FALSE), 
              popup= paste0("<b>", 
                            htmlEscape(temp_wm$county), 
                            "</b>",
                            "<br/>",
                            htmlEscape(round(temp_wm$temp, 1)), "&#x2103"), 
              group="Temperature") |>
  addCircleMarkers(data=cities_5, 
                   group="2,500 to 9,999", 
                   radius=3, 
                   color="#ffffd4", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_6, 
                   group="10,000 to 49,999", 
                   radius=5, 
                   color="#fed98e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_7, 
                   group="50,000 to 90,999", 
                   radius=7, 
                   color="#fe9929", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_8, 
                   group="100,000 to 499,999", 
                   radius=9, 
                   color="#d95f0e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_9, 
                   group="500,000 to 999,999", 
                   radius=11, 
                   color="#993404", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addPolylines(data=states_wm, color="#000000", weight=2.5) |>
  addLayersControl(overlayGroups= c("2,500 to 9,999", 
                                    "10,000 to 49,999", 
                                    "50,000 to 90,999", 
                                    "100,000 to 499,999", 
                                    "500,000 to 999,999", 
                                    "Percent Forest", 
                                    "Precipitation", 
                                    "Temperature"), 
                   baseGroups = c("OSM", "ESRI", "CartoDB")) |>
  setView(lat= 42, lng=-105, zoom=5)

25.4.3 Legends

In order to make the data more interpretable, we now add legends for all three polygon layers using addLegend(). Within this function, we define the position, color palette, values, legend title, and the group layer to which the legend is associated.

per_for_pal <- colorNumeric(palette="Greens", domain=per_for_wm$per_for)
precip_pal <- colorNumeric(palette="Purples", domain=precip_wm$precip)
temp_pal <- colorNumeric(palette="YlOrRd", domain=temp_wm$temp)

leaflet() |>
  addTiles(group = "OSM") |>
  addProviderTiles("Esri.NatGeoWorldMap", group="ESRI") |>
  addProviderTiles("CartoDB.DarkMatter", group= "CartoDB") |>
  addPolygons(data=per_for_wm, 
              fillColor = per_for_pal(per_for_wm$per_for), 
              fillOpacity=1, 
              col="#302E2D", 
              weight=1,
              highlight = highlightOptions(weight = 3, 
                                           color = "white", 
                                           bringToFront = FALSE), 
              popup= paste0("<b>", 
                            htmlEscape(per_for_wm$county),
                            "</b>",
                            "<br/>",
                            htmlEscape(round(per_for_wm$per_for, 1)), "%"), 
              group="Percent Forest") |>
  addPolygons(data=precip_wm, 
              fillColor = precip_pal(precip_wm$precip), 
              fillOpacity=1, 
              col="#302E2D", 
              weight=1, 
              highlight = highlightOptions(weight = 3, 
                                           color = "white", 
                                           bringToFront = FALSE), 
              popup= paste0("<b>", htmlEscape(precip_wm$county),
                            "</b>",
                            "<br/>",
                            htmlEscape(round(precip_wm$precip/100, 1)), "mm"),
              group="Precipitation") |>
  addPolygons(data=temp_wm, 
              fillColor = temp_pal(temp_wm$temp), 
              fillOpacity=1, 
              col="#302E2D", 
              weight=1, 
              highlight = highlightOptions(weight = 3, 
                                           color = "white", 
                                           bringToFront = FALSE), 
              popup= paste0("<b>", 
                            htmlEscape(temp_wm$county), 
                            "</b>",
                            "<br/>",
                            htmlEscape(round(temp_wm$temp, 1)), "&#x2103"), 
              group="Temperature") |>
  addCircleMarkers(data=cities_5, 
                   group="2,500 to 9,999", 
                   radius=3, 
                   color="#ffffd4", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_6, 
                   group="10,000 to 49,999", 
                   radius=5, 
                   color="#fed98e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_7, 
                   group="50,000 to 90,999", 
                   radius=7, 
                   color="#fe9929", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_8, 
                   group="100,000 to 499,999", 
                   radius=9, 
                   color="#d95f0e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addCircleMarkers(data=cities_9, 
                   group="500,000 to 999,999", 
                   radius=11, 
                   color="#993404", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=",")))) |>
  addLayersControl(overlayGroups= c("2,500 to 9,999", 
                                    "10,000 to 49,999", 
                                    "50,000 to 90,999", 
                                    "100,000 to 499,999", 
                                    "500,000 to 999,999", 
                                    "Percent Forest", 
                                    "Precipitation", 
                                    "Temperature"), 
                   baseGroups = c("OSM", "ESRI", "CartoDB")) |>
  addPolylines(data=states_wm, color="#000000", weight=2.5) |>
  addLegend(position="bottomleft", 
            pal=per_for_pal, 
            values=per_for_wm$per_for, 
            title="Percent Forest", 
            group="Percent Forest") |>
  addLegend(position= "bottomleft", 
            pal=temp_pal, 
            values=temp_wm$temp, 
            title="Mean Annual Temp. (C)", 
            group="Temperature") |>
  addLegend(position="bottomleft", 
            pal=precip_pal, 
            values=precip_wm$precip, 
            title="Total Annual Precip. (mm)", 
            group="Precipitation") |>
  setView(lat= 42, lng=-105, zoom=5)

25.4.4 Finalize Map

Before calling this a finished product, we make two additional change. The legends overlap with the zoom control buttons. Unfortunately, at the time of this writing, fixing this overlap is not easily accomplished using the leaflet package. So, we have to insert some JavaScript into the leaflet() call.

Second, if layers are turned on-and-off using the layer controls, the order of the layers is not maintained. Specifically, when a layer is turned back on, it draws above all of the other layers. One means to control the drawing order is to create panes using addMapPane() and assign layers to these panes using options = pathOptions(pane = "PANE NAME") within the add*() function for the layer. The z-index is used to control the drawing order, and higher values indicate drawing above other panes.

Table 25.1 lists the z-index assigned to the default Leaflet panes. As an example, marker icons draw above operational layers, which draw above base maps. It is possible to add additional panes. Since we are interested in controlling the drawing order of operational layers specifically, we use z-indices between 400 and 500. We specifically create three panes, one to store the city point features, one for the county layers, and one for the states. If we want to maintain the exact drawing order, we could create a pane for each operational layer. As configured, the panes will only make sure that the county layers always draw below the point layers, and the state layer draws above all other layers.

Table 25.1. Leaflet panes.
Pane Z-Index Use
mapPane auto Holds all other content
tilePane 200 Basemap and tile layers
overlayPane 400 Operational layers
shadowPane 500 Shadows for markers
markerPane 600 Makers and icons
tooltipPane 650 Tool tips
popupPane 700 Popups
per_for_pal <- colorNumeric(palette="Greens", domain=per_for_wm$per_for)
precip_pal <- colorNumeric(palette="Purples", domain=precip_wm$precip)
temp_pal <- colorNumeric(palette="YlOrRd", domain=temp_wm$temp)

lMap <- leaflet(options = leafletOptions(zoomControl = FALSE)) |>
    htmlwidgets::onRender("function(el, x) {
        L.control.zoom({ position: 'topright' }).addTo(this)
    }") |>
  addMapPane("cities", zIndex = 420) |>
  addMapPane("counties", zIndex = 410) |>
   addMapPane("states", zIndex = 430) |>
  addTiles(group = "OSM") |>
  addProviderTiles("Esri.NatGeoWorldMap", group="ESRI") |>
  addProviderTiles("CartoDB.DarkMatter", group= "CartoDB") |>
  addPolygons(data=per_for_wm, 
              fillColor = per_for_pal(per_for_wm$per_for), 
              fillOpacity=1, 
              col="#302E2D", 
              weight=1,
              highlight = highlightOptions(weight = 3, 
                                           color = "white", 
                                           bringToFront = FALSE), 
              popup= paste0("<b>", 
                            htmlEscape(per_for_wm$county),
                            "</b>",
                            "<br/>",
                            htmlEscape(round(per_for_wm$per_for, 1)), "%"), 
              group="Percent Forest",
              options = pathOptions(pane = "counties")) |>
  addPolygons(data=precip_wm, 
              fillColor = precip_pal(precip_wm$precip), 
              fillOpacity=1, 
              col="#302E2D", 
              weight=1, 
              highlight = highlightOptions(weight = 3, 
                                           color = "white", 
                                           bringToFront = FALSE), 
              popup= paste0("<b>", htmlEscape(precip_wm$county),
                            "</b>",
                            "<br/>",
                            htmlEscape(round(precip_wm$precip/100, 1)), "mm"),
              group="Precipitation",
              options = pathOptions(pane = "counties")) |>
  addPolygons(data=temp_wm, 
              fillColor = temp_pal(temp_wm$temp), 
              fillOpacity=1, 
              col="#302E2D", 
              weight=1, 
              highlight = highlightOptions(weight = 3, 
                                           color = "white", 
                                           bringToFront = FALSE), 
              popup= paste0("<b>", 
                            htmlEscape(temp_wm$county), 
                            "</b>",
                            "<br/>",
                            htmlEscape(round(temp_wm$temp, 1)), "&#x2103"), 
              group="Temperature",
              options = pathOptions(pane = "counties")) |>
  addCircleMarkers(data=cities_5, 
                   group="2,500 to 9,999", 
                   radius=3, 
                   color="#ffffd4", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=","))),
                   options = pathOptions(pane = "cities")) |>
  addCircleMarkers(data=cities_6, 
                   group="10,000 to 49,999", 
                   radius=5, 
                   color="#fed98e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=","))),
                   options = pathOptions(pane = "cities")) |>
  addCircleMarkers(data=cities_7, 
                   group="50,000 to 90,999", 
                   radius=7, 
                   color="#fe9929", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=","))),
                   options = pathOptions(pane = "cities")) |>
  addCircleMarkers(data=cities_8, 
                   group="100,000 to 499,999", 
                   radius=9, 
                   color="#d95f0e", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>", 
                                   htmlEscape(format(POP2010, big.mark=","))),
                   options = pathOptions(pane = "cities")) |>
  addCircleMarkers(data=cities_9, 
                   group="500,000 to 999,999", 
                   radius=11, 
                   color="#993404", 
                   stroke=FALSE, 
                   fillOpacity=1, 
                   popup = ~paste0("<b>", 
                                   htmlEscape(NAME), 
                                   "</b>",
                                   "<br/>",
                                   htmlEscape(format(POP2010, big.mark=","))),
                   options = pathOptions(pane = "cities")) |>
  addPolylines(data=states_wm, 
               color="#000000", 
               weight=2.5,
               options = pathOptions(pane = "states")) |>
  addLayersControl(overlayGroups= c("2,500 to 9,999", 
                                    "10,000 to 49,999", 
                                    "50,000 to 90,999", 
                                    "100,000 to 499,999", 
                                    "500,000 to 999,999", 
                                    "Percent Forest", 
                                    "Precipitation", 
                                    "Temperature"), 
                   baseGroups = c("OSM", "ESRI", "CartoDB")) |>
  addLegend(position="bottomleft", 
            pal=per_for_pal, 
            values=per_for_wm$per_for, 
            title="Percent Forest", 
            group="Percent Forest") |>
  addLegend(position= "bottomleft", 
          pal=temp_pal, 
          values=temp_wm$temp, 
          title="Mean Annual Temp. (C)", 
          group="Temperature") |>
  addLegend(position="bottomleft", 
          pal=precip_pal, 
          values=precip_wm$precip, 
          title="Total Annual Precip. (mm)", 
          group="Precipitation") |>
  setView(lat= 42, lng=-105, zoom=5)

lMap

25.5 Map Export

In order to produce a webpage from the final map or include it within another webpage, it needs to be rendered to an HTML file. First, the map must be saved to an object. This has already been done, and the interactive map is referenced to the lMap object.

The saveWidget() function from the htmlwidgets package allows for Leaflet maps created in R to be rendered as HTML files. We must provide the object and a name for the output file. If a full file path is not defined, then the result is saved to the working directory.

When we create an interactive map, we prefer to set the selfcontained argument to FALSE, which will cause the assets for the page to be written to a folder as opposed to being embedded in the HTML file, resulting in much cleaner and interpretable HTML.

If you choose to render the generated web map, you can navigate to the folder in which is was saved and click on the HTML file, which will load in your default web browser.

outFld <- "gslrData/chpt25/output/"
saveWidget(lMap, file=str_glue("{outFld}high_plains.html"), selfcontained = FALSE)

25.6 Concluding Remarks

If you would like to experiment further with leaflet, here is a link to a cheat sheet. The package documentation can be found here, and a GitHub page devoted to the package is available here.

Additionally, if you are interested in web development, take some time to learn some web programming. A little knowledge of HTML, CSS, and JavaScript can go a long way.

25.7 Questions

  1. Explain how to set the initial zoom level and center of a leaflet map.
  2. Explain the concept of a raster tile layer.
  3. Explain the concept of panes and z-indices as used in leaflet.
  4. Explain the purpose of the selfcontained parameter within saveWidget().
  5. Explain the purpose of the htmlEscape() function.
  6. Explain the purpose of the group parameter when adding operational layers.
  7. Explain the difference between addMarkers() and addCircleMarkers() for point features.
  8. Explain the differences between colorNumeric(), colorBin(), colorQuantile(), and colorFactor().

25.8 Exercises

Produce your own interactive web map using leaflet and R. Render the product as an HTML webpage with all required assets in an associated folder. You can create a map relating to any topic of your choosing. However, it must meet the following criteria. Note that we did not providing any data for this exercise, so you will need to find your own source files.

  1. The web map should include at least three spatial layers (not counting the base maps). We recommend using vector data as opposed to raster data.
  2. All layers must be well symbolized.
  3. Pop-ups should be well configured. You can choose to not include pop-ups for up to two layers. However, the pop-ups for these layers should be disabled.
  4. An appropriate initial zoom and extent should be defined.
  5. The user should be able to choose between at least two base maps.
  6. The user should be able to turn all the operational layers on and off.
  7. Make sure the drawing order is maintained when layers are turned on and off.
  8. Consider the overall design and usability of the product including use of color; use of symbology; configuration of pop-ups; use of layer lists; defined initial zoom and extent; positioning of elements; and overall neatness of the webpage, map, and code.