Appendix B: Learn Leaflet

Overview for learning Leaflet

Chapter section list

B.1 Introduction to Leaflet

B.1.1 Basic procedure

Procedure B.1 : Basic steps to use {leaflet}

You create a Leaflet map with these basic steps:

  1. Create a map widget by calling leaflet::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.

B.1.2 Basic example

Code Collection B.1 : Title for code collection

R Code B.1 : Basic example to demonstrate {leaflet}

Code
m <- leaflet::leaflet() |> 
  leaflet::addTiles() |>   # Add default OpenStreetMap map tiles
  leaflet::addMarkers(
      lng = 174.768, lat = -36.852, 
      popup = "The birthplace of R"
      )
m  # Print the map

R Code B.2 : Born and raised in Vienna

Code
m <- leaflet::leaflet() |> 
  leaflet::addTiles() |>   # Add default OpenStreetMap map tiles
  leaflet::addMarkers(
      lng = 16.398184, lat = 48.206776, 
      popup = "Place where I was born and raised"
      ) |> 
   leaflet::addMarkers(
      lng = 16.399601, lat = 48.205767, 
      popup = "My elementary and secondary school"
      ) |> 
    leaflet::addMarkers(
      lng = 16.374392, lat = 48.203045, 
      popup = "Secondary school with electrotechnical focus"
      ) 
m  # Print the map

B.2 The map widget

The function leaflet::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 or the R native pipe |>.

B.2.1 Initializing options

The map widget can be initialized with certain parameters. The leaflet::leafletOptions() can be passed any option described in the leaflet reference document. Using the leaflet::leafletOptions(), you can set a custom CRS and have your map displayed in a non spherical Mercator projection as described in Working with projections in Leaflet.

R Code B.3 : Initializing options example

Code
# Set value for the minZoom and maxZoom settings.
leaflet::leaflet(
    options = leaflet::leafletOptions(minZoom = 0, maxZoom = 18)
    )

B.2.2 Map methods

You can manipulate the attributes of the map widget using a series of methods. Please see the help page ?setView for details. (See the list of zoom/panoptions.)

  • leaflet::setView() sets the center of the map view and the zoom level;
  • leaflet::fitBounds() fits the view into the rectangle [lng1, lat1] – [lng2, lat2];
  • leaflet::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.

B.2.3 The data object

Both leaflet::leaflet() and the map layer functions (starting with add[capitalLetter]…) have an optional data parameter that is designed to receive spatial data in one of several forms:

  • From base R:
    • lng/lat matrix
    • data frame with lng/lat columns
  • From the {maps} package:
  • Simple features from the {sf} package

The data argument is used to derive spatial data for functions that need it; for example, if data is a {sf} Simple Features data.frame, then calling leaflet::addPolygons() on that map widget will know to add the polygons from the geometry column.

It is straightforward to derive these variables from sf 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:

  • the latitude variable is guessed by looking for columns named lat or latitude (case-insensitive)
  • the longitude variable is guessed by looking for lng, long, or longitude

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 leaflet::addCircles() below, but the columns Lat and Long in the data frame df will be automatically used.

: Reference latitude/longitude columns implicitly & explicitly

R Code B.4 : Reference latitude/longitude columns implicitly

Code
# add some circles to a map
df = base::data.frame(Lat = 1:10, Long = rnorm(10))

leaflet::leaflet(df) |> 
    leaflet::addCircles()

R Code B.5 : Reference latitude/longitude columns explicitly

Code
# add some circles to a map
df = base::data.frame(Lat = 1:10, Long = rnorm(10))

leaflet::leaflet(df) |> 
    leaflet::addCircles(lng = ~Long, lat = ~Lat)

See Section B.2.4 for more info on the ~ syntax.

R Code B.6 : Override data layer

Code
leaflet::leaflet() |> 
    leaflet::addCircles(data = df) |> 
    leaflet::leaflet()  |>  
    leaflet::addCircles(
        data = df, 
        lat = ~ Lat, 
        lng = ~ Long,
        color = "red"
        )

R Code B.7 : Using {sf} data

Code
polygon1 <- sf::st_polygon(
    base::list(
        base::cbind(
            base::c(2, 4, 4, 1, 2), 
            base::c(2, 3, 5, 4, 2)
            )
        )
    )
polygon2 <- sf::st_polygon(list(cbind(c(5, 4, 2, 5), c(2, 3, 2, 2))))
polygon3 <- sf::st_polygon(list(cbind(c(4, 4, 5, 10, 4), c(5, 3, 2, 5, 5))))
polygon4 <- sf::st_polygon(list(cbind(c(5, 6, 6, 5, 5), c(4, 4, 3, 3, 4))))
multi_polygon <- sf::st_multipolygon(list(
    list(cbind(c(4, 4, 5, 10, 4), c(5, 3, 2, 5, 5))),
    list(cbind(c(5, 6, 6, 5, 5), c(4, 4, 3, 3, 4)))
))
sf_polygons <- sf::st_sf(geometry = sf::st_sfc(polygon1, polygon2, multi_polygon))

leaflet::leaflet(height = "300px") |> 
    leaflet::addPolygons(data = sf_polygons)

R Code B.8 : Using {maps} data

Code
mapStates = maps::map("state", fill = TRUE, plot = FALSE)
leaflet::leaflet(data = mapStates) |> 
  leaflet::addTiles()  |> 
  leaflet::addPolygons(
      fillColor = grDevices::topo.colors(10, alpha = NULL), 
      stroke = FALSE
      )

R Code B.9 : Using {rnaturalearth} data

Code
world_map_countries <- base::readRDS("data/chapter09/world_map_countries.rds")
leaflet::leaflet(data = world_map_countries) |> 
  leaflet::addTiles() |> 
  leaflet::addPolygons(fillColor = 
       grDevices::topo.colors(10, alpha = NULL), 
       stroke = FALSE
       )

B.2.4 The formula interface

The arguments of all layer functions can take normal R objects, such as a numeric vector for the lat argument, or a character vector of colors for the color argument. They can also take a one-sided formula, in which case the formula will be evaluated using the data argument as the environment. For example, ~ x means the variable x in the data object, and you can write arbitrary expressions on the right-hand side, e.g., ~ sqrt(x + 1).

Code Collection B.2 : Formula interface demonstration

R Code B.10 : Formula interface demonstration: Points varies by size and color

Code
df = base::data.frame(
  lat = stats::rnorm(100),
  lng = stats::rnorm(100),
  size = stats::runif(100, 5, 20),
  color = base::sample(grDevices::colors(), 100)
)

m = leaflet::leaflet(df) |>  
    leaflet::addTiles()

m |>  
    leaflet::addCircleMarkers(
        radius = ~size, 
        color = ~color, 
        fill = FALSE
        )
#> Assuming "lng" and "lat" are longitude and latitude, respectively

R Code B.11 : Formula interface demonstration: All points are red, random size variation

Code
m |>  
    leaflet::addCircleMarkers(
        radius = stats::runif(100, 4, 10), 
        color = c('red')
        )
#> Assuming "lng" and "lat" are longitude and latitude, respectively

B.3 Choropleths

Making choropleths with {leaflet} is easy. In this example, we’ll duplicate the step-by-step choropleth tutorial from the Leaflet.js website.

B.3.1 Data source

We’ll start by loading the data from JSON. While the Leaflet.js example loads the JSON directly into JavaScript, with the {leaflet} R package we instead want to load the data into R.

In this case, we’ll use the {sf} package to load the data into an sf data.frame, which will let us easily manipulate the geographic features, and their properties, in R.

Code Collection B.3 : Loading example US data into R

R Code B.12 : Load example US states data into R

Code
states <- sf::read_sf("https://rstudio.github.io/leaflet/json/us-states.geojson")
class(states)
names(states)
#> [1] "sf"         "tbl_df"     "tbl"        "data.frame"
#> [1] "id"       "name"     "density"  "geometry"

As you can see, we now have an {sf} data.frame with name (state name) and density (population density in people/mi^2) columns from the GeoJSON.

R Code B.13 : Load example would countries data into R

Code
countries <- sf::read_sf("https://rstudio.github.io/leaflet/json/countries.geojson")
class(countries)
names(countries)
#> [1] "sf"         "tbl_df"     "tbl"        "data.frame"
#> [1] "gdp_md_est" "pop_est"    "geometry"

R Code B.14 : Load would country WHR data

Code
world_whr_2024 <- base::readRDS("data/chapter09/world_whr_2024.rds")

# add `"tbl_df" "tbl"` to class `"sf" "data.frame"`
world_whr_2024 <- my_as_tibble_sf(world_whr_2024) 

class(world_whr_2024)
names(world_whr_2024)
#> [1] "sf"         "tbl_df"     "tbl"        "data.frame"
#>  [1] "iso_a3"       "name"         "sovereignt"   "continent"    "area"        
#>  [6] "pop_est"      "pop_est_dens" "economy"      "income_grp"   "gdp_cap_est" 
#> [11] "life_exp"     "well_being"   "footprint"    "HPI"          "inequality"  
#> [16] "gender"       "press"        "WHR"          "geometry"

B.3.2 Basic map

Next, let’s make a basic map with just the outline of the states/countries. For our basemap, we’ll use the same “mapbox.light” MapBox style that the example does; if you don’t have a MapBox account, you can just use leaflet::addTiles() in place of the leaflet::addProviderTiles() call, or choose a free provider.

Code Collection B.4 : Basic choropleth map

R Code B.15 : Basic US states map

Code
map_states <- leaflet::leaflet(states) |> 
  leaflet::setView(-96, 37.8, 4) |> 
  leaflet::addProviderTiles(
      provider = "MapBox", 
      options = leaflet::providerTileOptions(
            id = "mapbox.light",
            accessToken = Sys.getenv('MAPBOX_PUBLIC_TOKEN')
            )
      )

map_states |> leaflet::addPolygons()
#> Warning in sf::st_is_longlat(x): bounding box has potentially an invalid value
#> range for longlat data

Explanation of leaflet::setView()

  1. leaflet::setView() has five parameters:
  • map
  • lng (longitude: east to west; -180° to 180°)
  • lat (latitude: north to south; -90° to 90°)
  • zoom
  • options = list()
  1. map is the dataset (states in this case)
  2. The lng/lat values is point that center on the interesting area. You can find the coordinates for instance with the latlong.net-service.
  3. This maps uses zoom level 4. The calculation for this level is the following:

At each zoom level, each tile is divided in four, and its size (length of the edge, given by the tileSize option) doubles, quadrupling the area. In other words, the width and height of the world is 256 * 2^zoomlevel (in pixels). So the formula in our example is 256 * 2^4 = 4096 pixels.

Most tile services offer tiles up to zoom level 18, depending on their coverage. This is enough to see a few city blocks per tile. (For more information see the Leaflet.JS tutorial on zoom levels.)

R Code B.16 : Basic world map

Code
map_world <- leaflet::leaflet(world_whr_2024) |> 
  leaflet::setView(0, 10, 1) |> 
  leaflet::addProviderTiles(
      provider = "MapBox", 
      options = leaflet::providerTileOptions(
            id = "mapbox.light",
            accessToken = Sys.getenv('MAPBOX_PUBLIC_TOKEN')
            )
      )

map_world |> leaflet::addPolygons()

The world center point is lng = 0 and lat = 0. But because of the height-width ratio of the graphics the north part is a little bit hidden. After some experimentation it turned out that leaflet::setView(0, 10, 1) is the best match.

R Code B.17 : World map focused to Europe

Code
map_europe <- leaflet::leaflet(world_map_countries) |> 
  leaflet::setMaxBounds(-24.5, 71.5, 34.3, 41.5) |> 
  leaflet::addProviderTiles(
      provider = "MapBox", 
      options = leaflet::providerTileOptions(
            id = "mapbox.light",
            accessToken = Sys.getenv('MAPBOX_PUBLIC_TOKEN'),
            minZoom = 3,
            maxZoom = 8
            )
      )

map_europe |> leaflet::addPolygons()
Figure B.1: World map with bounds set to the coordinates of Europe

Here I am using coordinates for maximal boundaries with a specific minimum and maximum zoom level. The idea is to focus on Europe.

B.3.3 Adding some color

Now, let’s color the states according to their population density. You have various options for mapping data to colors; for this example we’ll match the Leaflet.js tutorial by mapping a specific set of bins into {RColorBrewer} colors.

Procedure B.2 : Mapping data to colors

  1. First, we’ll define the bins. This is a numeric vector that defines the boundaries between intervals ((0,10], (10,20], and so on).
  2. Then, we’ll call leaflet::colorBin() to generate a palette function that maps the {RColorBrewer} “YlOrRd" colors to our bins.
  3. Finally, we’ll modify leaflet::addPolygons() to use the palette function and the density values to generate a vector of colors for fillColor, and also add some other static style properties.

Code Collection B.5 : Mapping data to colors

R Code B.18 : Mapping data to colors for population densities fo US states

Code
bins_states <- c(0, 10, 20, 50, 100, 200, 500, 1000, Inf)
pal_states <- leaflet::colorBin(
    palette = "YlOrRd", 
    domain = states$density, 
    bins = bins_states
    )

map_states |>  leaflet::addPolygons(
  fillColor = ~pal_states(density),
  weight = 2,
  opacity = 1,
  color = "white",
  dashArray = "3",
  fillOpacity = 0.7)
#> Warning in sf::st_is_longlat(x): bounding box has potentially an invalid value
#> range for longlat data

R Code B.19 : Mapping WHR world data for countries of the world to colors

Code
bins_world <- c(1, 2, 3, 4, 5, 6, 7, 8)
pal_world <- leaflet::colorBin(
    palette = "YlOrRd", 
    domain = world_whr_2024$WHR, 
    bins = bins_world
    )

map_world |>  leaflet::addPolygons(
  fillColor = ~pal_world(WHR),
  weight = 2,
  opacity = 1,
  color = "white",
  dashArray = "3",
  fillOpacity = 0.7)

B.3.4 Adding interaction

The next thing we’ll want is to make the polygons highlight as the mouse passes over them. The leaflet::addPolygon() function has a highlight argument that makes this simple.

Code Collection B.6 : Adding interaction to the leaflet map

R Code B.20 : Show boundaries of states with mouse hovering

Code
map_states |> leaflet::addPolygons(
  fillColor = ~pal_states(density),
  weight = 2,
  opacity = 1,
  color = "white",
  dashArray = "3",
  fillOpacity = 0.7,
  highlightOptions = leaflet::highlightOptions(
    weight = 5,
    color = "#666",
    dashArray = "",
    fillOpacity = 0.7,
    bringToFront = TRUE))
#> Warning in sf::st_is_longlat(x): bounding box has potentially an invalid value
#> range for longlat data

R Code B.21 : Show boundaries of countries with mouse hovering

Code
map_world |> leaflet::addPolygons(
  fillColor = ~pal_world(WHR),
  weight = 2,
  opacity = 1,
  color = "white",
  dashArray = "3",
  fillOpacity = 0.7,
  highlightOptions = leaflet::highlightOptions(
    weight = 2,
    color = "black",
    dashArray = "",
    fillOpacity = 0.7,
    bringToFront = TRUE))

(The Leaflet.js tutorial also adds an event handler that zooms into a state when it’s clicked. This isn’t currently possible with the {leaflet} R package, except with either custom JavaScript or using {shiny}, both of which are outside the scope of this example.)

B.3.5 Custom info

Now let’s expose the state names and values to the user.

The Leaflet.js tutorial shows the hovered-over state’s information in a custom control. Again, that’s possible by adding custom JavaScript or using {shiny}, but for this example we’ll use the built-in labels feature instead.

We’ll generate the labels by handcrafting some HTML, and passing it to base::lapply(htmltools::HTML) so that {leaflet} knows to treat each label as HTML instead of as plain text. We’ll also set some label options to improve the style of the label element itself.

Code Collection B.7 : Showing custom info when hovering with the mouse

R Code B.22 : Showing states name and population density when hovering with the mouse over the states

Code
labels_states <- base::sprintf(
  "<strong>%s</strong><br/>%g people / mi<sup>2</sup>",
  states$name, states$density
  ) |>  
    base::lapply(htmltools::HTML)

map_states <- map_states |>  leaflet::addPolygons(
  fillColor = ~pal_states(density),
  weight = 2,
  opacity = 1,
  color = "white",
  dashArray = "3",
  fillOpacity = 0.7,
  highlightOptions = leaflet::highlightOptions(
    weight = 5,
    color = "#666",
    dashArray = "",
    fillOpacity = 0.7,
    bringToFront = TRUE),
  label = labels_states,
  labelOptions = leaflet::labelOptions(
    style = list("font-weight" = "normal", padding = "3px 8px"),
    textsize = "15px",
    direction = "auto"))
#> Warning in sf::st_is_longlat(x): bounding box has potentially an invalid value
#> range for longlat data
Code
map_states

R Code B.23 : Showing country name and WHR score when hovering with the mouse over the country

Code
labels_world <- base::sprintf(
  "<strong>%s</strong><br/>%g  Well-being score",
  world_whr_2024$name, world_whr_2024$WHR
  ) |>  
    base::lapply(htmltools::HTML)

map_world <- map_world |>  leaflet::addPolygons(
  fillColor = ~pal_world(WHR),
  weight = 2,
  opacity = 1,
  color = "white",
  dashArray = "3",
  fillOpacity = 0.7,
  highlightOptions = leaflet::highlightOptions(
    weight = 2,
    color = "black",
    dashArray = "",
    fillOpacity = 0.7,
    bringToFront = TRUE),
  label = labels_world,
  labelOptions = leaflet::labelOptions(
    style = list("font-weight" = "normal", padding = "3px 8px"),
    textsize = "15px",
    direction = "auto"))
map_world

B.3.6 Legend

As our final step, let’s add a legend. Because we chose to color our map using leaflet::colorBin(), the leaflet::addLegend() function makes it particularly easy to add a legend with the correct colors and intervals.

Code Collection B.8 : Adding legend to a map

R Code B.24 : Adding legend to population density of US states

Code
map_states |> leaflet::addLegend(
    pal = pal_states, 
    values = ~density, 
    opacity = 0.7, 
    title = "Pop.density",
  position = "bottomright")

R Code B.25 : Adding legend to map of world countries WHR scores

Code
map_world |> leaflet::addLegend(
    pal = pal_world, 
    values = ~WHR, 
    opacity = 0.7, 
    title = "WHR",
  position = "bottomleft")

B.4 Show/Hide layers

The {leaflet} package includes functions to show and hide map layers. You can allow users to decide what layers to show and hide, or programmatically control the visibility of layers using server-side code in Shiny.

In both cases, the fundamental unit of showing/hiding is the group.

B.4.1 Understanding groups

A group is a label given to a set of layers. You assign layers to groups by using the group parameter when adding the layers to the map.

R Code B.26 : Understanding groups: faked example

Listing / Output B.1: Understanding group: A faked example
leaflet::leaflet()  |> 
 leaflet::addTiles()  |> 
  leaflet::addMarkers(data = coffee_shops, group = "Food & Drink") |> 
  leaflet::addMarkers(data = restaurants, group = "Food & Drink")  |> 
  leaflet::addMarkers(data = restrooms, group = "Restrooms")

Many layers can belong to same group. But each layer can only belong to zero or one groups (you can’t assign a layer to two groups).

Group vs. Layer IDs

Groups and Layer IDs may appear similar, in that both are used to assign a name to a layer. However, they differ in that layer IDs are used to provide a unique identifier to individual markers and shapes, etc., while groups are used to give shared labels to many items.

You generally provide one group value for the entire leaflet::addMarkers() call, and you can reuse that same group value in future leaflet::add*() calls to add to that group’s membership (as in the example above).

layerId arguments are always vectorized: when calling e.g., leaflet::addMarkers() you need to provide one layer ID per marker, and they must all be unique. If you add a circle with a layer ID of "foo" and later add a different shape with the same layer ID, the original circle will be removed.

B.4.2 Interactive Layer Display

You can use {leaflet}’s layers control feature to allow users to toggle the visibility of groups.

Code Collection B.9 : Title for code collection

R Code B.27 : Numbered R Code Title (Original)

Code
outline <- 
  datasets::quakes[grDevices::chull(
      datasets::quakes$long, 
      datasets::quakes$lat
      ),
  ]

map <- leaflet::leaflet(datasets::quakes) |> 

  # Base groups
  leaflet::addTiles(group = "OSM (default)")  |> 
  leaflet::addProviderTiles(
    leaflet::providers$CartoDB.Positron, 
    group = "Positron (minimal)"
    )  |> 
  leaflet::addProviderTiles(
    leaflet::providers$Esri.WorldImagery, 
    group = "World Imagery (satellite)"
    )  |> 
  
  # Overlay groups
  leaflet::addCircles(
    ~ long,
    ~ lat,
    ~ 10 ^ mag / 5,
    stroke = FALSE,
    group = "Quakes",
    fillColor = "tomato"
  ) |> 
  leaflet::addPolygons(
    data = outline,
    lng = ~ long,
    lat = ~ lat,
    fill = FALSE,
    weight = 2,
    color = "#FFFFCC",
    group = "Outline"
  ) |> 
  
  # Layers control
  leaflet::addLayersControl(
    baseGroups = base::c(
      "OSM (default)",
      "Positron (minimal)",
      "World Imagery (satellite)"
    ),
    overlayGroups = base::c(
      "Quakes", 
      "Outline"
      ),
    options = leaflet::layersControlOptions(collapsed = FALSE)
  )

map

R Code B.28 : Numbered R Code Title (Tidyverse)

Code
1 + 1
#> [1] 2

leaflet::addLayersControl() distinguishes between base groups, which can only be viewed one group at a time, and overlay groups, which can be individually checked or unchecked.

Although base groups are generally tile layers, and overlay groups are usually markers and shapes, there is no restriction on what types of layers can be placed in each category.

Only one layers control can be present on a map at a time. If you call leaflet::addLayersControl() multiple times, the last call will win

B.4.3 Programmatic Layer Display

You can use leaflet::showGroup() and leaflet::hideGroup() to show and hide groups from code. This mostly makes sense in a Shiny context with leaflet::leafletProxy(), where perhaps you might toggle group visibility based on input controls in a sidebar.

You can also use leaflet::showGroup()/leaflet::hideGroup() in conjunction with leaflet::addLayersControl() to set which groups are checked by default.

Code Collection B.10 : Title for code collection

R Code B.29 : Numbered R Code Title (Original)

Code
map |> leaflet::hideGroup("Outline")

R Code B.30 : Numbered R Code Title (Tidyverse)

Code
1 + 1
#> [1] 2

Finally, you can remove the layers in a group using leaflet::clearGroup(). Note that this doesn’t just remove the layers from the group, it also removes them from the map. (It does not, however, remove the group itself from the map; it still exists, but is empty.)

B.4.4 With Marker Clusters

If markers are added to different groups, and when using marker clustering as described in the marker page, {leaflet} will generate different sets of clusters for different groups. This allows showing/hiding of marker clusters belonging to a group independently of other marker clusters in other groups.

Code Collection B.11 : Title for code collection

R Code B.31 : Numbered R Code Title (Original)

Code
quakes <- datasets::quakes |> 
  dplyr::mutate(
    mag.level = base::cut(
      mag, base::c(3,4,5,6),
      labels = base::c('>3 & <=4', '>4 & <=5', '>5 & <=6')))

quakes.df <- base::split(quakes, quakes$mag.level)

l <- leaflet::leaflet() |>  leaflet::addTiles()

base::names(quakes.df)  |> 
  purrr::walk(function(df) {
    l <<-  l  |> 
      leaflet::addMarkers(
        data = quakes.df[[df]],
        lng =  ~ long,
        lat =  ~ lat,
        label =  ~ base::as.character(mag),
        popup =  ~ base::as.character(mag),
        group = df,
        clusterOptions = 
          leaflet::markerClusterOptions(
            removeOutsideVisibleBounds = FALSE
            ),
            labelOptions = leaflet::labelOptions(
              noHide = FALSE,
              direction = 'auto'
              )
        )
  }
)

l  |> 
  leaflet::addLayersControl(
    overlayGroups = base::names(quakes.df),
    options = leaflet::layersControlOptions(collapsed = FALSE)
  )

R Code B.32 : Numbered R Code Title (Tidyverse)

Code
1 + 1
#> [1] 2

B.5 Case Study: WHR Example

In this section I want to apply my new knowledge about layers. My practice example are the WHR data of several years. My aim is to show the distribution of the happiness scores for each year with maps of different providers.

I will start with the comparison of two datasets: The first year of WHR data (2011) and the last available year (2024).

B.5.1 Prepare data

My first step is to prepare the data.

R Code B.33 : Prepare data

Code
whr_final <- base::readRDS("data/chapter09/whr_final.rds")
world_map_countries <-  base::readRDS("data/chapter09/world_map_countries.rds")

whr_map_final <- 
  dplyr::left_join(
    world_map_countries,
    whr_final,
    dplyr::join_by(iso3)
  ) |> 
  dplyr::filter(wb_group_code == "WLD") |>
  dplyr::select(1:4, 8) |> 
  dplyr::relocate(
    geometry, 
    .after = dplyr::last_col()
    ) |> 
  dplyr::rename(score = ladder_score) |> 
  dplyr::arrange(name) |> 
  tidyr::pivot_wider(
    names_from = year,
    values_from = score
  )
  

skimr::skim(whr_map_final)
Data summary
Name whr_map_final
Number of rows 161
Number of columns 16
_______________________
Column type frequency:
character 2
numeric 13
sfc 1
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
iso3 0 1 3 3 0 161 0
name 0 1 4 32 0 161 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
2011 9 0.94 5.40 1.11 3.01 4.58 5.23 6.25 7.86 ▂▇▇▅▃
2012 10 0.94 5.42 1.12 2.94 4.57 5.34 6.31 7.69 ▂▇▇▅▅
2014 8 0.95 5.37 1.16 2.84 4.52 5.21 6.27 7.59 ▃▇▇▇▆
2015 10 0.94 5.39 1.15 2.90 4.40 5.31 6.30 7.53 ▃▆▇▇▅
2016 11 0.93 5.36 1.14 2.69 4.50 5.28 6.10 7.54 ▂▆▇▇▅
2017 10 0.94 5.38 1.12 2.90 4.45 5.36 6.17 7.63 ▃▇▇▇▅
2018 10 0.94 5.42 1.11 3.08 4.54 5.37 6.19 7.77 ▃▇▇▆▃
2019 13 0.92 5.48 1.10 2.57 4.73 5.51 6.23 7.81 ▂▃▇▆▃
2020 16 0.90 5.53 1.08 2.52 4.85 5.48 6.22 7.84 ▁▅▇▇▃
2021 19 0.88 5.55 1.09 2.40 4.89 5.57 6.29 7.82 ▁▃▇▇▃
2022 27 0.83 5.53 1.15 1.86 4.66 5.67 6.32 7.80 ▁▂▆▇▃
2023 21 0.87 5.52 1.18 1.72 4.63 5.78 6.37 7.74 ▁▂▆▇▅
2024 17 0.89 5.57 1.16 1.36 4.67 5.86 6.43 7.74 ▁▂▅▇▅

Variable type: sfc

skim_variable n_missing complete_rate missing complete n n_unique valid
geometry 0 1 0 161 161 1 161

B.5.2 Compare years

The first difficulty I have to meet is to get radio buttons for the data of different years. I solved this issue by setting a fixed provider and using the baseGroup arguments for the data of the different years.

Watch out!

I am not sure if this is the correct approach.

R Code B.34 : Example: Compare WHR data 2011 with 2024

Code
bins_world <- c(1, 2, 3, 4, 5, 6, 7, 8)

pal_2011 <- leaflet::colorBin(
    palette = "YlOrRd", 
    domain = whr_map_final$`2011`, 
    bins = bins_world
    )

pal_2024 <- leaflet::colorBin(
    palette = "YlOrRd", 
    domain = whr_map_final$`2024`, 
    bins = bins_world
    )

map_whr <- 
  leaflet::leaflet(whr_map_final) |> 
  leaflet::setView(0, 10, 2) |> 
  leaflet::addProviderTiles("CartoDB.Positron") |> 
  
  # Base groups

  leaflet::addPolygons(
    fillColor = ~pal_2011(`2011`),
    weight = 2,
    opacity = 1,
    color = "white",
    dashArray = "3",
    fillOpacity = 0.7,
    group = "2011"
  ) |> 
  
  leaflet::addPolygons(
  fillColor = ~pal_2024(`2024`),
  weight = 2,
  opacity = 1,
  color = "white",
  dashArray = "3",
  fillOpacity = 0.7,
  group = "2024"
) |> 
  
  # Layers control
  leaflet::addLayersControl(
    baseGroups = base::c(
      "2024",
      "2011" 
      ),
    options = leaflet::layersControlOptions(collapsed = FALSE)
  )
  
map_whr

B.5.3 Add custom info

R Code B.35 : Example: Add custom info for WHR data 2011 with 2024

Code
bins_world <- c(1, 2, 3, 4, 5, 6, 7, 8)

pal_2011 <- leaflet::colorBin(
    palette = "YlOrRd", 
    domain = whr_map_final$`2011`, 
    bins = bins_world
    )

pal_2024 <- leaflet::colorBin(
    palette = "YlOrRd", 
    domain = whr_map_final$`2024`, 
    bins = bins_world
    )


labels_2011 <- base::sprintf(
  "<strong>%s: 2011</strong><br/>%g  Well-being score",
  whr_map_final$name, whr_map_final$`2011`
  ) |>  
    base::lapply(htmltools::HTML)

labels_2024 <- base::sprintf(
  "<strong>%s: 2024</strong><br/>%g  Well-being score",
  whr_map_final$name, whr_map_final$`2024`
  ) |>  
    base::lapply(htmltools::HTML)


map_whr_info <- 
  leaflet::leaflet(whr_map_final) |> 
  leaflet::setView(0, 10, 2) |> 
  leaflet::addProviderTiles("CartoDB.Positron") |> 
  
  # Base groups

  leaflet::addPolygons(
    fillColor = ~pal_2011(`2011`),
    weight = 2,
    opacity = 1,
    color = "white",
    dashArray = "3",
    fillOpacity = 0.7,
    group = "2011",
    highlightOptions = leaflet::highlightOptions(
      weight = 2,
      color = "black",
      dashArray = "",
      fillOpacity = 0.7,
      bringToFront = TRUE),
    label = labels_2011,
    labelOptions = leaflet::labelOptions(
      style = list("font-weight" = "normal", padding = "3px 8px"),
      textsize = "15px",
      direction = "auto"
      )
  ) |> 
  
  leaflet::addPolygons(
    fillColor = ~pal_2024(`2024`),
    weight = 2,
    opacity = 1,
    color = "white",
    dashArray = "3",
    fillOpacity = 0.7,
    group = "2024",
    highlightOptions = leaflet::highlightOptions(
      weight = 2,
      color = "black",
      dashArray = "",
      fillOpacity = 0.7,
      bringToFront = TRUE),
    label = labels_2024,
    labelOptions = leaflet::labelOptions(
      style = list("font-weight" = "normal", padding = "3px 8px"),
      textsize = "15px",
      direction = "auto"
      )
  ) |> 
  
  # Layers control
  leaflet::addLayersControl(
    baseGroups = base::c(
      "2024",
      "2011" 
      ),
    options = leaflet::layersControlOptions(collapsed = FALSE)
  )

map_whr_info

I need to simplify the redundant code by using different functions. This would especially important if I am going to use all 13 years (2011, 2012, 2014-2024). The only difference is the year variable.

Another issue would be the long legend and the clumsy control by moving the cursor to the legend to choose the desired year and then moving back to map to see the details of the desired country. This destroy in some way a direct comparison of the different colors & values of the specified country.

A complete different approach that comes to my mind is using interactive control via {shiny}. See Section 4.5.3 especially my comments to a serverless {shiny} app in Note 4.1.