Chapter 6 Colours with leaflet

This chapter will cover:

  • colour palettes for leaflet
    • continuous
    • discrete
    • a mix (?)
  • legends in leaflet

6.1 Colour scales with leaflet

For these examples, we will use the polygons and rasters from iTRAQI.

The following chunk downloads these layers (just the same as in chapter 1 but using SA1s this time).

library(tidyverse)
library(sf)
download_layer <- function(layer_name, save_dir = "input") {
  githubURL <- glue::glue("https://raw.githubusercontent.com/RWParsons/iTRAQI_app/main/input/layers/{layer_name}")
  download.file(githubURL, file.path(save_dir, layer_name), method = "curl")
  readRDS(file.path(save_dir, layer_name))
}

raster_layer <- download_layer("rehab_raster.rds") %>%
  raster::raster(., layer = 1)

polygons_layer <- download_layer("stacked_SA1_and_SA2_polygons_year2016_simplified.rds")

6.1.1 Palettes for discrete variables

For discrete/factor variables, we can use colorFactor to create a palette for leaflet. The example below creates a very similar map to the ABS and we use the same one in the tour tab of the iTRAQI app (except that was using SA1s rather than SA2s).

palFac <- colorFactor("Greens", levels = 0:4, ordered = TRUE, reverse = TRUE)

leaflet() %>%
  addTiles() %>%
  addPolygons(
    data = polygons_layer[polygons_layer$SA_level == 2, ],
    color = "black",
    weight = 1,
    fillOpacity = 1,
    fillColor = palFac(polygons_layer[polygons_layer$SA_level == 2, ]$ra)
  )
## Warning: sf layer has inconsistent datum (+proj=longlat +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +no_defs).
## Need '+proj=longlat +datum=WGS84'

In the code above, we are passing a vector of hex codes as the fillColor argument: one for each polygon.

unique(palFac(polygons_layer$ra))
## [1] "#006D2C" "#31A354" "#74C476" "#BAE4B3" "#EDF8E9"
length(palFac(polygons_layer$ra)) == nrow(polygons_layer)
## [1] TRUE

For the iTRAQI index, we used a range of colours and did this by specifying a hex code for each one specifically.

First, we need to add the index to the polygons_layer.

iTRAQI_acute_breaks <- c(-Inf, 1, 2, 4, 6, Inf)
iTRAQI_rehab_breaks <- c(-Inf, 1, 2, 4, 6, Inf)

get_iTRAQI_index <- function(acute_mins, rehab_mins) {
  acute_cat <- cut(acute_mins / 60, breaks = iTRAQI_acute_breaks)
  rehab_cat <- cut(rehab_mins / 60, breaks = iTRAQI_rehab_breaks)

  acute_label <- as.numeric(acute_cat)
  rehab_label <- LETTERS[rehab_cat]

  index_value <- paste0(acute_label, rehab_label)
  ifelse(index_value == "NANA", NA, index_value)
}

polygons_layer <- polygons_layer %>%
  mutate(index = get_iTRAQI_index(value_acute, value_rehab))


# Create a copy of the polygons layer with only SA2s for faster displaying maps
qld_SA2s <- filter(polygons_layer, SA_level == 2)

Here are the colours we used for each of levels in the index.

index_palette_url <- RCurl::getURL("https://raw.githubusercontent.com/RWParsons/iTRAQI_app/main/input/index_palette.csv")

index_palette <- read.csv(text = index_palette_url) %>%
  select(Acute, Rehab, hex = hex2) %>%
  cbind(., iTRAQI_bins = (na.omit(unique(polygons_layer$index)) %>% sort()))

knitr::kable((index_palette))
Acute Rehab hex iTRAQI_bins
<1 A #ffe699 1A
<1 B #ffd966 1B
1-2 A #ffc000 2A
1-2 B #ffa700 2B
2-4 A #ff8457 3A
2-4 B #ff6600 3B
2-4 C #ff4900 3C
2-4 D #ff2f25 3D
4-6 B #e6005d 4B
4-6 C #d20055 4C
4-6 D #b00047 4D
4-6 E #8a003e 4E
6+ C #700055 5C
6+ D #420032 5D
6+ E #0d0d0d 5E

No we can create the palette in the same was now as we did before with the remoteness map.

paliTRAQI <- colorFactor(
  index_palette$hex,
  levels = index_palette$iTRAQI_bins,
  ordered = FALSE
)

leaflet() %>%
  addTiles() %>%
  addPolygons(
    data = qld_SA2s,
    color = "black",
    weight = 1,
    fillOpacity = 1,
    fillColor = paliTRAQI(qld_SA2s$index)
  )
## Warning: sf layer has inconsistent datum (+proj=longlat +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +no_defs).
## Need '+proj=longlat +datum=WGS84'

6.1.2 Palettes for continuous variables

For the rasters, we used continuous colour scales.

leaflet() %>%
  addTiles() %>%
  addRasterImage(
    x = raster_layer
  )

Since the raster is a grid of values, we don’t need to pass a huge vector of hex codes, but instead we need to pass a function which can take that value and return a hex code.

But still, there are options. We could use a binned colour scheme to make an isochrone type map as below.

bins <- c(0, 30, 60, 120, 180, 240, 300, 360, 900, 1200)
palBin <- colorBin("YlOrRd", domain = 0:1200, bins = bins, na.color = "transparent")

leaflet() %>%
  addTiles() %>%
  addRasterImage(
    x = raster_layer,
    colors = palBin
  )
## Warning in colors(.): Some values were outside the color scale and will be
## treated as NA

And our other option is to use a continuous colour scale.

palNum <- colorNumeric("YlOrRd", domain = 0:1200, na.color = "transparent")

leaflet() %>%
  addTiles() %>%
  addRasterImage(
    x = raster_layer,
    colors = palNum
  )
## Warning in colors(.): Some values were outside the color scale and will be
## treated as NA

6.1.3 A mix of continuous and discrete colour scales

This may sound like an odd thing to want to do: in the iTRAQI app, you may notice that we use the same legend and colour scale for all of our time-to-care maps, and that the scale on the legend is not linear from 0 to 1200 minutes. Fortunately, we can make make a (hacky) mix of both colorbin and colorNumeric to give specified breaks to a continuous scale. (We can also display the legend as if it were the colorBin legend.)

The first thing to consider is that the palette passed to colors argument doesn’t need to be made with a single palette from leaflet. Instead, we can merge many within a parent function. Here’s a smaller example.

palNumLow <- colorNumeric("Greens", domain = 0:300)
palNumHigh <- colorNumeric("Reds", domain = 300:1200)

pal_combined <- function(x) {
  case_when(
    x <= 300 ~ palNumLow(x),
    x > 300 ~ palNumHigh(x),
    TRUE ~ "transparent"
  )
}

leaflet() %>%
  addTiles() %>%
  addRasterImage(
    x = raster_layer,
    colors = pal_combined
  )
## Warning in palNumLow(x): Some values were outside the color scale and will be
## treated as NA
## Warning in palNumHigh(x): Some values were outside the color scale and will be
## treated as NA

What a truly hideous colour scale! Here, we used one colour scale (“Greens”) for values between 0 and 300 minutes, and another scale (“Reds”) for values from 300 to 1200! The good thing is that we can now mix and match scales any which way we desire, and combine several numeric scales across unevenly spaced breaks to create a single, continuous palette which doesn’t get washed out due to the extremes in the domain. Note that in our drive times here, we have times from the Torres Strait which were around 1200 minutes! Using any of the sequential colour palettes would mean that the differences across most of mainland QLD would appear to be smaller than they are as we would require the domain of the palette to accomodate for these extremely large times.

In the code below, we start out by using the bins that we had defined for the palBin(). We can then use the sequence of colours from the palBin, at each bin, as the end-range colours within a range of colorNumeric’s. By doing this, palNum1 is now a continuous colour scale from the first to the second level of palBin, palNum2 is a continuous scale from the second to third level of palBin…. For the last one, palNum9, we go from the deepest colour within the palBin to black.

palBin <- colorBin("YlOrRd", domain = min(bins):max(bins), bins = bins, na.color = "transparent")

palNum1 <- colorNumeric(c(palBin(bins[1]), palBin(bins[2])), domain = 0:30, na.color = "transparent")
palNum2 <- colorNumeric(c(palBin(bins[2]), palBin(bins[3])), domain = 30:60, na.color = "transparent")
palNum3 <- colorNumeric(c(palBin(bins[3]), palBin(bins[4])), domain = 60:120, na.color = "transparent")
palNum4 <- colorNumeric(c(palBin(bins[4]), palBin(bins[5])), domain = 120:180, na.color = "transparent")
palNum5 <- colorNumeric(c(palBin(bins[5]), palBin(bins[6])), domain = 180:240, na.color = "transparent")
palNum6 <- colorNumeric(c(palBin(bins[6]), palBin(bins[7])), domain = 240:300, na.color = "transparent")
palNum7 <- colorNumeric(c(palBin(bins[7]), palBin(bins[8])), domain = 300:360, na.color = "transparent")
palNum8 <- colorNumeric(c(palBin(bins[8]), palBin(bins[9])), domain = 360:900, na.color = "transparent")
palNum9 <- colorNumeric(c(palBin(bins[9]), "#000000"), domain = 900:1200, na.color = "transparent")

We can then combine all of these within a parent function which can then be called with any value. Since palNum9 approaches values of 1200 and blackness, all values above 1200 are black.

palNumMix <- function(x) {
  case_when(
    x < 30 ~ palNum1(x),
    x < 60 ~ palNum2(x),
    x < 120 ~ palNum3(x),
    x < 180 ~ palNum4(x),
    x < 240 ~ palNum5(x),
    x < 300 ~ palNum6(x),
    x < 360 ~ palNum7(x),
    x < 900 ~ palNum8(x),
    x < 1200 ~ palNum9(x),
    x >= 1200 ~ "#000000",
    TRUE ~ "transparent"
  )
}

The end result is a much more appropriate colour scale than either the binned or (the original) continuous colour scales.

For iTRAQI, we used this colour scale for both the rasters and the polygons (except for the index).

leaflet() %>%
  addTiles() %>%
  addRasterImage(
    x = raster_layer,
    colors = palNumMix
  )
leaflet() %>%
  addTiles() %>%
  addPolygons(
    data = qld_SA2s,
    color = "black",
    weight = 1,
    fillOpacity = 1,
    fillColor = palNumMix(qld_SA2s$value_rehab)
  )

6.2 Legends with leaflet

There are a couple ways that you can make legends in leaflet. The first is to use leaflet’s addLegend(). This works well for the binned or continuous colour scale, but it won’t work for the mixed colour scale.

leaflet() %>%
  addTiles() %>%
  addRasterImage(
    x = raster_layer,
    colors = palBin
  ) %>%
  addLegend(
    position = "bottomright",
    pal = palBin,
    values = 0:1200
  )
leaflet() %>%
  addTiles() %>%
  addPolygons(
    data = qld_SA2s,
    color = "black",
    weight = 1,
    fillOpacity = 1,
    fillColor = palNum(qld_SA2s$value_rehab)
  ) %>%
  addLegend(
    position = "bottomright",
    pal = palNum,
    values = 0:1200
  )

The biggest problem here is when we want to add the continuous scale to the rehab map within iTRAQI. Here, we used a continuous legend from 0 to 20 hours, and the custom palette we made (palNumMixed). Since we can’t pass this palette to addLegend, we have to get help from a new package, {leaflegend}. This package allows you to add more customisable legends to your leaflet map, and it also lets us pass our hacky colour scale we made to generate the legend.

Firstly, to make the legend display in hours, we make a(nother) parent function palNumMixHours which wraps palNumMixed but converts from minutes to hours. This way, we can add colours to the actual map with palNumMix but create the legend using palNumMixHours so that the values on the legend are in hours.

library(leaflegend)

palNumMixHours <- function(x) palNumMix(x * 60)

leaflet() %>%
  addTiles() %>%
  addRasterImage(
    x = raster_layer,
    colors = palNumMix
  ) %>%
  addLegendNumeric(
    pal = palNumMixHours,
    position = "topright",
    height = 250,
    width = 24,
    bins = 10,
    value = c(-0.01, 0:20, 20.1),
    htmltools::tagList(tags$div("Time to care (hours)"), tags$br())
  )

The legends on the main map of iTRAQI also use leaflegend but this was sort of optional. They’re very similar to legend we made using addLegend for the raster above but since we used leaflegend for the rehab map, we thought it’d be best to keep the styling consistent. Also, being able to format the size and title of the legend easily with leaflegend was a nice feature. Here’s what we used for the non-index layers of the main map.

leaflet() %>%
  addTiles() %>%
  addPolygons(
    data = qld_SA2s,
    color = "black",
    weight = 1,
    fillOpacity = 1,
    fillColor = palNum(qld_SA2s$value_rehab)
  ) %>%
  addLegendBin(
    opacity = 1,
    position = "topright",
    pal = palBin,
    values = 0:900,
    title = htmltools::tagList(tags$div("Time to care (minutes)"), tags$br())
  )
## Warning: sf layer has inconsistent datum (+proj=longlat +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +no_defs).
## Need '+proj=longlat +datum=WGS84'

If you’re interested in incorporating an interactive plot alongside your map as we did, chapter 7 will help you match your colour scales between your ggplot and maps. If not, you can skip to 8.