Color Palette Choice and Customization in R and ggplot2

Workshops for Ukraine

Cédric Scherer // Jan 26, 2023

Working with
Colors

Working with
Colors in R

Color Definitions

pal_a <- c("green", "red", "blue") 

Color Definitions

pal_b <- c("olivedrab", "salmon3", "slateblue")

Color Definitions

pal_c <- c("#28A87D", "#A83D28", "#2853a8")

Color Definitions

pal_d <- c(rgb(40, 168, 125, maxColorValue = 255), rgb(168, 61, 40, maxColorValue = 255), rgb(40, 83, 168, maxColorValue = 255))

Demonstrate Colors

colorspace::demoplot(pal_d)

colorspace::demoplot(pal_d, type = "scatter")

Create Sequential Color Palettes

colorRampPalette(pal_d)(35)
 [1] "#28A87D" "#2FA178" "#379B72" "#3E956D" "#468E69" "#4D8864" "#55825F" "#5C7B59" "#647555" "#6B6F50" "#73694A" "#7A6245" "#825C41" "#89563C" "#914F37"
[16] "#984932" "#A0432C" "#A83D28" "#A03E2F" "#983F37" "#91403E" "#894246" "#82434D" "#7A4455" "#73465C" "#6B4764" "#64486B" "#5C4973" "#554B7A" "#4D4C82"
[31] "#464D89" "#3E4F91" "#375098" "#2F51A0" "#2853A8"

Create Sequential Color Palettes

unikn::shades_of(col_1 = "#28A87D", col_n = "red", n = 35)
 [1] "#28A87D" "#2EA379" "#349E75" "#3A9971" "#41946E" "#478F6A" "#4D8A66" "#548563" "#5A805F" "#607B5B" "#677658" "#6D7154" "#736C50" "#7A674D" "#806249"
[16] "#865D45" "#8D5842" "#93543E" "#994F3A" "#A04A37" "#A64533" "#AC402F" "#B33B2C" "#B93628" "#BF3124" "#C62C21" "#CC271D" "#D22219" "#D91D16" "#DF1812"
[31] "#E5130E" "#EC0E0B" "#F20907" "#F80403" "#FF0000"

Create Sequential Color Palettes

pal_seq_multi <- colorRampPalette(pal_d)(35)

Create Sequential Color Palettes

pal_seq_multi <- colorRampPalette(pal_d)(35)

Create Sequential Color Palettes

colors <- c("#28A87D", "grey98")
pal_seq <- colorRampPalette(colors)(35)

Create Diverging Color Palettes

colors <- c("#7754BF", "grey98", "#208462")
pal_div <- colorRampPalette(colors)(35)

Create Diverging Color Palettes

colors <- c("#AA89F6", "grey5", "#49BD92")
pal_div_dark <- colorRampPalette(colors)(35)

Adjust Colors

colors <- colorspace::darken(c("#28A87D", "grey98"), .35)
pal_seq_dark <- colorRampPalette(colors)(35)

Adjust Colors

colors <- c(colorspace::darken("#28A87D", .35), "grey98")
pal_seq_dark <- colorRampPalette(colors)(35)

Adjust Colors

colors <- c(colorspace::darken("#28A87D", .35, space = "HLS"), "grey98")
pal_seq_dark_hls <- colorRampPalette(colors)(35)

Adjust Colors

The color adjustment can be calculated in three different color spaces.

  • If space = "HCL", the colors are transformed to HCL, (polarLUV), the luminance component L is adjusted, and then the colors are transformed back to a hexadecimal RGB string.

  • If space = "HLS", the colors are transformed to HLS, the lightness component L is adjusted, and then the color is transformed back to a hexadecimal RGB string.

  • If space = "combined", the colors are first adjusted in both the HCL and HLS spaces. Then, the adjusted HLS colors are converted into HCL, and then the chroma components of the adjusted HLS colors are copied to the adjusted HCL colors. Thus, in effect, the combined model adjusts luminance in HCL space but chroma in HLS space.

Adjust Colors

colors <- c("grey5", colorspace::lighten("#28A87D", .85))
pal_seq_rev <- colorRampPalette(colors)(35)

Adjust Colors

colors <- c("red", "grey98", "blue")
pal_div_raw <- colorRampPalette(colors)(35)

Adjust Colors

colors_des <- colorspace::desaturate(colors, .7)
pal_div_des <- colorRampPalette(colors_des)(35)

Adjust Colors

colors_des <- prismatic::clr_desaturate(colors, .7)
pal_div_des <- colorRampPalette(colors_des)(35)

Adjust Colors

colors <- c(prismatic::clr_darken("#28A87D", .35), "grey98")
pal_seq_dark <- colorRampPalette(colors)(35)

Mix Colors

colors <- c(prismatic::clr_mix("blue", "green"), "grey98")
pal_seq_mix <- colorRampPalette(colors)(35)

Pre-Defined Color Palettes

{colorspace}

colorspace::hcl_palettes(plot = TRUE)

{colorspace}

colorspace::hcl_palettes(palette = "PuBuGn", n = 10, plot = TRUE)

{colorspace}

pal <- colorspace::sequential_hcl(palette = "PuBuGn", n = 10)
plot(prismatic::color(pal))

{colorspace}

pal <- colorspace::sequential_hcl(palette = "PuBuGn", n = 10)
scales::show_col(pal)

{RColorBrewer}

RColorBrewer::display.brewer.all()

{RColorBrewer}

RColorBrewer::display.brewer.all(colorblindFriendly = TRUE)

{RColorBrewer}

pal <- RColorBrewer::brewer.pal(name = "RdYlBu", n = 10)
plot(prismatic::color(pal))

{scico}

scico::scico_palette_show()

{scico}

pal <- scico::scico(palette = "tokyo", n = 10)
plot(prismatic::color(pal))

{rcartocolor}

rcartocolor::display_carto_all()

{rcartocolor}

rcartocolor::display_carto_all(colorblind_friendly = TRUE)

{rcartocolor}

pal <- rcartocolor::carto_pal(name = "ag_Sunset", n = 7)
plot(prismatic::color(pal))

{rcartocolor}

pal_extd <- colorRampPalette(pal)(20)
plot(prismatic::color(pal_extd))

More Color Packages

  • {dichromat} – for dichromats *
  • {fishualize} – based on teleost fishes
  • {ggsci} – based on scientific journal and Sci-Fi movies
  • {ggthemes} – based on popular news rooms and software
  • {ghibli} – based on Studio Ghibli movies
  • {MetBrewer} – inspired by the Metropolitan Museum of Art
  • {nord} – collection of northern inspired palettes
  • {pals} – comprehensive collection of recommended palettes *
  • {Redmonder} – inspired by Microsoft products *
  • {wesanderson} – based on movies by Wes Anderson *

Working with
Colors in ggplot2

Setup

library(ggplot2)
library(dplyr)
library(prismatic)

theme_set(theme_minimal(base_family = "Asap Condensed", base_size = 15, base_line_size = .5))

theme_update(
  panel.grid.minor = element_blank(),
  plot.title = element_text(
    face = "bold", size = rel(1.5), margin = margin(b = 15)
  ),
  plot.title.position = "plot",
  plot.caption = element_text(
    color = "grey45", size = rel(.7), hjust = 0, margin = margin(t = 15)
  ),
  plot.caption.position = "plot"
)

The Data Set

df_world <- readr::read_csv("data/urban-gdp-pop.csv")
sf_world <- readr::read_rds("data/urban-gdp-pop-sf.rds")
dplyr::glimpse(df_world)
Rows: 159
Columns: 11
$ sovereignt     <chr> "Cuba", "Denmark", "Saudi Arabia", "Yemen", "Italy", "Comoros", "Gabon", "Norway", "Kyrgyzstan", "Madagascar", "Libya", "Bahrain", "Ven…
$ iso_a3         <chr> "CUB", "DNK", "SAU", "YEM", "ITA", "COM", "GAB", "NOR", "KGZ", "MDG", "LBY", "BHR", "VEN", "TGO", "KOR", "TKM", "IRL", "KEN", "SVN", "S…
$ type           <chr> "Sovereignty", "Country", "Sovereign country", "Sovereign country", "Sovereign country", NA, "Sovereign country", NA, "Sovereign countr…
$ continent      <chr> "North America", "Europe", "Asia", "Asia", "Europe", NA, "Africa", NA, "Asia", "Africa", "Africa", NA, "South America", "Africa", "Asia…
$ region_un      <chr> "Americas", "Europe", "Asia", "Asia", "Europe", NA, "Africa", NA, "Asia", "Africa", "Africa", NA, "Americas", "Africa", "Asia", "Asia",…
$ subregion      <chr> "Caribbean", "Northern Europe", "Western Asia", "Western Asia", "Southern Europe", NA, "Middle Africa", NA, "Central Asia", "Eastern Af…
$ gdp_per_capita <dbl> 8000, 44836, 51397, 2506, 33419, 1702, 18413, 82814, 4879, 1381, 8096, 41078, 15219, 1400, 36103, 23813, 56597, 3169, 26908, 44659, 585…
$ urban_pop      <dbl> 76.930, 87.642, 83.401, 35.394, 69.855, 28.619, 88.559, 81.485, 35.944, 35.856, 79.540, 89.090, 88.165, 40.628, 81.562, 50.728, 62.737,…
$ pop_est        <dbl> 11333483, 5818553, 34268528, 29161922, 60297396, NA, 2172579, NA, 6456900, 26969307, 6777452, NA, 28515829, 8082366, 51709098, 5942089,…
$ economy        <chr> "5. Emerging region: G20", "2. Developed region: nonG7", "2. Developed region: nonG7", "7. Least developed region", "1. Developed regio…
$ income_grp     <chr> "3. Upper middle income", "1. High income: OECD", "2. High income: nonOECD", "4. Lower middle income", "1. High income: OECD", NA, "3. …

Data Preparation

#remotes::install_github("wmgeolab/rgeoboundaries")
library(tidyverse)

sf_world_ne <- rnaturalearth::ne_countries(scale = 110, returnclass = "sf")
sf_world_correct <- rgeoboundaries::gb_adm0()
owid_data <- readr::read_csv("data/owid-urbanization-vs-gdp.csv")
## via https://ourworldindata.org/urbanization

sf_world <- sf_world_correct %>% 
  left_join(sf::st_drop_geometry(sf_world_ne), by = c("shapeISO" = "iso_a3")) %>% 
  left_join(
    owid_data %>% 
      janitor::clean_names() %>% 
      rename(urban_pop = "urban_population_percent_long_run_to_2016_owid") %>% 
      filter(year == 2016), 
    by = c("shapeISO" = "code")
  ) %>% 
  dplyr::select(
    sovereignt = shapeName, iso_a3 = shapeISO, type, continent = continent.x, 
    region_un, subregion, gdp_per_capita, urban_pop, pop_est, economy, income_grp
  ) %>% 
  mutate(pop_est = as.numeric(pop_est)) %>% 
  filter(!sovereignt == "Antarctica")

df_world <- sf_world %>% 
  sf::st_drop_geometry() %>% 
  filter(!is.na(gdp_per_capita), !is.na(urban_pop))

readr::write_rds(sf_world, "data/urban-gdp-pop-sf.rds")
readr::write_csv(df_world, "data/urban-gdp-pop.csv")

The Chart

The Chart

p <- 
  ggplot(data = df_world, 
         mapping = aes(x = gdp_per_capita, y = urban_pop, size = pop_est)) + 
  coord_cartesian(expand = FALSE, clip = "off") +
  scale_x_log10(labels = scales::label_dollar()) + 
  scale_y_continuous(labels = function(y) paste0(y, "%"), limits = c(0, 100)) + 
  scale_size_area(max_size = 15, breaks = c(10, 100, 500, 1000) * 10^6,
                  labels = scales::label_number(scale = 1/10^6, suffix = "M")) +
  labs(x = NULL, y = NULL,
       fill = "Continent:", size = "Population:",
       title = "Urban population vs. GDP per capita, 2016",
       caption = "Sources: ourworldindata.org/urbanization; NaturalEarth; geoBoundaries API | x-axis on log scale") +
  guides(fill = guide_legend(override.aes = list(size = 3)),
         size = guide_legend(override.aes = list(color = "black", fill = "grey85")))

p + geom_point(aes(fill = region_un), shape = 21, alpha = .5, stroke = .7)

Using Pre-Defined Scales

{viridis}

p +
  geom_point(
    aes(fill = region_un), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_viridis_d()

{viridis}

{viridis}

p +
  geom_point(
    aes(fill = urban_pop), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_viridis_c()

{viridis}

p +
  geom_point(
    aes(fill = urban_pop), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_viridis_c(
    name = "Urban share:", 
    labels = function(x) paste0(x, "%")
  ) +
  guides(fill = guide_colorbar())

{rcartocolor}

p +
  geom_point(
    aes(fill = region_un), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  rcartocolor::scale_fill_carto_d()

{rcartocolor}

{rcartocolor}

p +
  geom_point(
    aes(fill = region_un), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  rcartocolor::scale_fill_carto_d(
    palette = "Bold"
  )

scale_color|fill_manual()

pal <- rcartocolor::carto_pal(
  name = "Bold", n = 5
)

p +
  geom_point(
    aes(fill = region_un), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(
    values = pal
  )

Customize Palettes

Customize Palette

pal <- rcartocolor::carto_pal(
  name = "Bold", n = 6
)

p +
  geom_point(
    aes(fill = region_un), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(
    values = pal
  )

Customize Palette

pal <- rcartocolor::carto_pal(
  name = "Bold", n = 8
)[c(1:3,5,7)]

p +
  geom_point(
    aes(fill = region_un), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(
    values = pal
  )

Customize Palette

pal_dark <- clr_darken(pal, .4)

p +
  geom_point(
    aes(fill = region_un), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(
    values = pal_dark
  )

after_scale()

Re-use Palettes

p +
  geom_point(
    aes(fill = region_un,
        color = after_scale(fill)), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(
    values = pal
  )

Re-use Palettes

p +
  geom_point(
    aes(fill = region_un,
        color = after_scale(
          clr_darken(fill, .6)
        )), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(
    values = pal
  )

Re-use Palettes

b <- ggplot(
    data = df_world, 
    mapping = aes(
      x = gdp_per_capita, 
      y = forcats::fct_reorder(
        region_un, -gdp_per_capita
      )
    )
  ) +
  scale_x_continuous(
    labels = scales::dollar_format()
  ) +
  guides(color = "none") +
  labs(x = "GDP per capita", y = NULL) +
  theme(panel.grid.major.y = element_blank())

b +
  geom_boxplot(
    aes(color = region_un), 
    width = .7
  ) +
  scale_color_manual(values = pal) 

Re-use Palettes

b +
  geom_boxplot(
    aes(color = region_un, 
        fill = after_scale(
          clr_lighten(color, .65)
        )), 
    width = .7
  ) +
  scale_color_manual(values = pal)

Use Colors for Highlighting

Highlight Colors

p +
  geom_point(
    aes(fill = region_un != "Africa"), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(
    values = c("#D00000", "grey80")
  )

Highlight Colors

p +
  geom_point(
    aes(fill = region_un != "Africa"), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(
    values = c("#D00000", "grey80"),
    labels = c("African countries", "Other countries"),
    name = NULL
  )

Highlight Colors

p +
  geom_point(
    aes(fill = region_un != "Africa"), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(
    values = c("#D00000", "grey80")
  ) +
  guides(fill = "none") +
  labs(subtitle = "of <b style='color:#D00000;'>African</b> and <b style='color:grey70;'>non-African</b> countries") +
  theme(
    plot.subtitle = ggtext::element_markdown(
      margin = margin(t = -15, b = 20),
      size = rel(1.3)
    )
  )

Highlight Colors

p +
  geom_point(
    aes(fill = region_un != "Africa",
        color = after_scale(fill)), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(
    values = c("#D00000", "grey80")
  ) +
  labs(subtitle = "of <b style='color:#D00000;'>African</b> and <b style='color:grey70;'>non-African</b> countries") +
  guides(fill = "none") +
  theme(
    plot.subtitle = ggtext::element_markdown(
      margin = margin(t = -15, b = 20),
      size = rel(1.3)
    )
  )

Highlight Colors

df_world <- df_world %>% 
  mutate(
    cont_lumped = ifelse(
      continent %in% c("Asia", "Europe"), continent, "Other"
    ),
    cont_lumped = forcats::fct_relevel(
      cont_lumped, "Other", after = Inf
    )
  )

Highlight Colors

pal_hl <- c("#7754BF", "#28A74D", "grey80")

p +
  geom_point(
    data = df_world,
    aes(fill = cont_lumped,
        color = after_scale(clr_darken(fill, .3))), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(values = pal_hl) +
  labs(subtitle = "of <b style='color:#7754BF;'>Asian</b> and <b style='color:#28A74D;'>European</b> countries compared to all <span style='color:grey60;'>other countries</span>") +
  guides(fill = "none") +
  theme(
    plot.subtitle = ggtext::element_markdown(
      margin = margin(t = -15, b = 20),
      size = rel(1.3)
    )
  )

Sequential and Diverging Color Palettes

The Map

The Map

m <- 
  ggplot(data = sf_world) +
  geom_sf(aes(fill = urban_pop), color = "white", lwd = .3) +
  coord_sf(crs = "+proj=eqearth") +
  labs(
    title = "Share of people living in urban areas, 2016",
    caption = "Sources: ourworldindata.org/urbanization; NaturalEarth; geoBoundaries API",
    fill = NULL
  ) +
  theme(
    axis.text = element_blank(),
    panel.grid.major = element_blank(),
    legend.key.width = unit(2.5, "lines"),
    legend.key.height = unit(.6, "lines"),
    legend.position = "bottom"
  )

m

A Sequential Palette

m + scale_fill_distiller(palette = "YlGnBu")

A (Reversed) Sequential Palette

m + scale_fill_distiller(palette = "YlGnBu", direction = 1)

A Diverging Palette

m + scale_fill_distiller(palette = "RdYlBu")

A (Reversed) Diverging Palette

m + scale_fill_distiller(palette = "RdYlBu", direction = 1)

A (Fixed) Diverging Palette

m + scale_fill_distiller(palette = "RdYlBu", direction = 1, limits = c(0, 100))

A (Styled) Diverging Palette

m + scale_fill_distiller(palette = "RdYlBu", direction = 1, limits = c(0, 100),
                         na.value = "grey85", labels = function(x) paste0(x, "%"))

A (Custom) Diverging Palette

m + scale_fill_gradient2(low = "#7754BF", mid = "grey90", high = "#208462", midpoint = 50, 
                         na.value = "grey85", labels = function(x) paste0(x, "%"))

Test Color Palettes

Test Color Palettes

pt <- p +
  geom_point(
    data = df_world,
    aes(fill = cont_lumped,
        color = after_scale(clr_darken(fill, .3))), 
    shape = 21, alpha = .5, stroke = .7
  ) +
  scale_fill_manual(values = pal_hl)

mt <- m + scale_fill_gradient2(
  low = "#7754BF", mid = "grey90", high = "#208462", midpoint = 50, 
  na.value = "grey85", labels = function(x) paste0(x, "%")
)

Test Color Palettes

colorblindr::cvd_grid(pt)

Test Color Palettes

colorblindr::cvd_grid(mt)

Evaluate Color Palettes

Create Some Palettes

pal_seq_1 <- viridis::cividis(n = 7)
pal_seq_2 <- scico::scico(palette = "bamako", n = 7)

pal_div_1 <- RColorBrewer::brewer.pal(name = "RdYlBu", n = 7)
pal_div_2 <- MetBrewer::met.brewer(name = "Hiroshige", n = 7)

pal_cat_1 <- rcartocolor::carto_pal(name = "Prism", n = 7)
pal_cat_2 <- ggthemes::excel_pal()(7)

Evaluate HCL Space

colorspace::specplot(pal_seq_1)

colorspace::specplot(pal_seq_2)

Evaluate HCL Space

colorspace::specplot(pal_div_1)

colorspace::specplot(pal_div_2)

Evaluate HCL Space

colorspace::specplot(pal_cat_1)

colorspace::specplot(pal_cat_2)

Color Tools

colorspace::hcl_color_picker()

Color Tools

colorspace::choose_color()

Color Palette Tools

colorspace::hcl_wizard()

Color Palette Tools

colorspace::choose_palette()

CVD Tool

colorspace::cvd_emulator()

Viz Palette Tool

Viz Palette

Palettte App

Chroma.js

Resources

Resources

Thank You!