![The word 'coloratio' in green text on a black background and vice versa to its right.](/post/2020-12-29-coloratio_files/coloratio.png)
This blog’s theme: insufficient contrast!
tl;dr
I made a small R package called {coloratio} to evaluate colour-contrast ratios for accessibility. Then I found out that {savonliquide} already exists to do this.
Accessible charts
The UK government’s website, GOV.UK, was developed with user needs and accessibility in mind. I’ve been using {ggplot2} to recreate the simple, accessible chart styles suggested for use on GOV.UK by the Government Statistical Service.
But I wondered: is it possible to programmatically select a high-contrast text colour to overlay the fill colours of a {ggplot2} barplot? You would want black text over white and vice versa, for example.
What is ‘high contrast’ anyway? GOV.UK’s Design System refers to W3C’s contrast guidance from WCAG 2.1, which suggests a ratio of 4.5:1 for regular text on a block-coloured background.
It isn’t a big deal to program this ‘manually’, but that’s not fun.
Ratio calculation
Is the contrast accessible?
How about a small package with some functions to derive colour contrast ratios? Introducing {coloratio}.
remotes::install_github("matt-dray/coloratio")
Pass two colours to cr_get_ratio()
as hex values or named colours—see colors()
—and it performs the necessary calculations to derive relative luminance and return a colour contrast ratio.
library(coloratio) # attach package
cr_get_ratio(
"papayawhip", "#000000", # colours to compare
view = TRUE # optional demo of colours
)
## [1] 18.55942
This contrast value is above the 4.5 threshold, so we’re good to go. You’ll get a warning if the contrast is insufficient.
cr_get_ratio("olivedrab", "olivedrab2")
## Warning in cr_get_ratio("olivedrab", "olivedrab2"): Aim for a value of 4.5 or higher.
## [1] 2.755693
Surprise: as stunning as an all-olivedrab palette might be, these colours aren’t distinct enough to be accessible.
Black or white?
cr_get_ratio()
in turn powers the function cr_choose_bw()
, which returns black or white depending on the greatest contrast with a supplied background colour.
cr_choose_bw("snow")
## [1] "black"
cr_choose_bw("saddlebrown")
## [1] "white"
To demonstrate better, let’s create a grouped barplot with lighter (lemonchiffon3
) and darker (hotpink4
) fill colours, then use cr_choose_bw()
to choose black or white for the overlaying text.
library(tidyverse) # for data manipulation
# Example data
d <- data.frame(
x_val = c("A", "A", "B", "B"),
y_val = c(3, 6, 4, 10),
z_val = c("a", "b", "a", "b")
) %>%
mutate( # add colour columns
fill_col = rep(c("hotpink4", "lemonchiffon3"), 2),
text_col = map_chr(fill_col, coloratio::cr_choose_bw)
)
d # preview
## x_val y_val z_val fill_col text_col
## 1 A 3 a hotpink4 white
## 2 A 6 b lemonchiffon3 black
## 3 B 4 a hotpink4 white
## 4 B 10 b lemonchiffon3 black
No surprise: white was returned for the darker fill and black for the lighter fill.
We can now refer to this information in the colour
argument of geom_text()
.
ggplot(d, aes(x_val, y_val, fill = z_val)) +
geom_bar(position = "dodge", stat = "identity") +
scale_fill_manual(values = d$fill_col) + # fill colour
geom_text(aes(y = 0.5, label = y_val),
position = position_dodge(0.9),
size = 5, colour = d$text_col) + # text colour
coord_flip() +
theme_minimal(base_size = 16) + # clean up the theme
theme(axis.text.x = element_blank(), axis.title = element_blank(),
legend.title = element_blank(), panel.grid = element_blank())
As desired: black on the lighter fill; white on the darker fill. The default would be black text, which would provide insufficient contrast for darker fills.
Aside: cr_choose_bw()
in geom_text()
?
Originally I wanted geom_text()
to choose text colours on the fly, rather than adding them to the input data. This roundabout solution—which outputs a similar plot to the one above—requires you to build the plot object, then interrogate it with ggplot_build()
to identify the bar-fill colours.
# Build simple grouped barplot again
p <- ggplot(d, aes(x_val, y_val, fill = z_val)) +
geom_bar(position = "dodge", stat = "identity") +
scale_fill_manual(values = c("hotpink4", "lemonchiffon3")) +
coord_flip()
# Extract the p-object fills and choose text overlay colour
p + geom_text(
aes(y = 0.5, label = y_val), position = position_dodge(0.9), size = 5,
colour = map_chr( # make text colour dependent on bar colour
ggplot_build(p)[[1]][[1]]$fill, # access p-object fills
coloratio::cr_choose_bw # choose black/white text based on fill
)
)
I put this to the RStudio Community with no answer to date. Let me know if you have any ideas.
A soapy slip-up
Having addressed my need, I was suspicious. Surely this has been done in R before? I put out a tweet to investigate.
Is there an #rstats package that calculates colour-contrast ratios for #accessibility? I've started {coloratio} to do this, but might be wasting time: https://t.co/TjUOIMYIbk pic.twitter.com/vUHUJrX0al
— Matt Dray (@mattdray) December 24, 2020
I soon realised my error. Merry Christmas!
🤦♂️ Haha, it already exists in the {savonliquide} package! Great work from @moh_fodil. Check it out: https://t.co/vbrtKVYR6q
— Matt Dray (@mattdray) December 25, 2020
Whoops. {savonliquide} by Ihaddaden M. EL Fodil can query the WebAIM contrast checker API to get the contrast ratio for two colours. And it’s on CRAN.
install.packages("savonliquide")
Maybe I missed it because of the name, which translates to ‘liquid soap’?
Anyway, like coloratio::cr_get_ratio()
, you can pass two hex values or named colours to {savonliquide}’s check_contrast()
function.
savonliquide::check_contrast("blanchedalmond", "bisque2")
##
## * The Contrast Ratio is 1.04
##
## * The result for the AA check is : FAIL
##
## * The result for the AALarge check is : FAIL
##
## * The result for the AAA check is : FAIL
##
## * The result for the AAALarge check is : FAIL
The output is richer than coloratio::cr_get_ratio()
. You can see here that the supplied colours fail additional accessibility checks from WCAG 2.1 that involve large text and more stringent contrast thresholds.
Handily, there’s also the savonliquide::check_contrast_raw()
variant that returns a list with each result as an element.
Acceptance
So… should you wash your hands of {coloratio}?1 Well, it fills the micro-niche of an R package that doesn’t require an internet connection to fetch colour contrast ratios. But it’s probably never going to go on CRAN, so you should use {savonliquide}.
I certainly learnt a lesson about due diligence during package development. Especially because I also discovered recently that I had also somehow managed to reinvent the {badger} package with my own {badgr} package.2 Whoops again.
At worst, I got to learn more about accessibility, practice some package building, and solve my initial problem (kinda).
I also got to admire the creativity of the names in the named-colour set. ‘Papayawhip’ sounds really appealing. Or perhaps painful. Just like package development.3
Session info
## ─ Session info ───────────────────────────────────────────────────────────────
## setting value
## version R version 4.0.2 (2020-06-22)
## os macOS Mojave 10.14.6
## system x86_64, darwin17.0
## ui X11
## language (EN)
## collate en_GB.UTF-8
## ctype en_GB.UTF-8
## tz Europe/London
## date 2020-12-30
##
## ─ Packages ───────────────────────────────────────────────────────────────────
## package * version date lib source
## assertthat 0.2.1 2019-03-21 [1] CRAN (R 4.0.0)
## backports 1.1.8 2020-06-17 [1] CRAN (R 4.0.0)
## blob 1.2.1 2020-01-20 [1] CRAN (R 4.0.2)
## blogdown 0.19 2020-05-22 [1] CRAN (R 4.0.0)
## bookdown 0.19 2020-05-15 [1] CRAN (R 4.0.0)
## broom 0.7.0 2020-07-09 [1] CRAN (R 4.0.2)
## cellranger 1.1.0 2016-07-27 [1] CRAN (R 4.0.2)
## cli 2.2.0 2020-11-20 [1] CRAN (R 4.0.2)
## coloratio * 0.0.0.9003 2020-12-28 [1] local
## colorspace 2.0-0 2020-11-11 [1] CRAN (R 4.0.2)
## crayon 1.3.4 2017-09-16 [1] CRAN (R 4.0.0)
## curl 4.3 2019-12-02 [1] CRAN (R 4.0.0)
## DBI 1.1.0 2019-12-15 [1] CRAN (R 4.0.0)
## dbplyr 1.4.4 2020-05-27 [1] CRAN (R 4.0.2)
## digest 0.6.27 2020-10-24 [1] CRAN (R 4.0.2)
## dplyr * 1.0.0 2020-08-10 [1] Github (tidyverse/dplyr@5e3f3ec)
## ellipsis 0.3.1 2020-05-15 [1] CRAN (R 4.0.0)
## evaluate 0.14 2019-05-28 [1] CRAN (R 4.0.0)
## fansi 0.4.1 2020-01-08 [1] CRAN (R 4.0.0)
## farver 2.0.3 2020-01-16 [1] CRAN (R 4.0.0)
## forcats * 0.5.0 2020-03-01 [1] CRAN (R 4.0.2)
## fs 1.5.0 2020-07-31 [1] CRAN (R 4.0.2)
## generics 0.1.0 2020-10-31 [1] CRAN (R 4.0.2)
## ggplot2 * 3.3.2 2020-06-19 [1] CRAN (R 4.0.2)
## glue 1.4.2 2020-08-27 [1] CRAN (R 4.0.2)
## gtable 0.3.0 2019-03-25 [1] CRAN (R 4.0.0)
## haven 2.3.1 2020-06-01 [1] CRAN (R 4.0.2)
## hms 0.5.3 2020-01-08 [1] CRAN (R 4.0.2)
## htmltools 0.5.0 2020-06-16 [1] CRAN (R 4.0.2)
## httr 1.4.2 2020-07-20 [1] CRAN (R 4.0.2)
## jsonlite 1.7.2 2020-12-09 [1] CRAN (R 4.0.2)
## knitr 1.30 2020-09-22 [1] CRAN (R 4.0.2)
## labeling 0.4.2 2020-10-20 [1] CRAN (R 4.0.2)
## lifecycle 0.2.0 2020-03-06 [1] CRAN (R 4.0.0)
## lubridate 1.7.9.2 2020-11-13 [1] CRAN (R 4.0.2)
## magrittr 2.0.1 2020-11-17 [1] CRAN (R 4.0.2)
## modelr 0.1.8 2020-05-19 [1] CRAN (R 4.0.2)
## munsell 0.5.0 2018-06-12 [1] CRAN (R 4.0.0)
## pillar 1.4.7 2020-11-20 [1] CRAN (R 4.0.2)
## pkgconfig 2.0.3 2019-09-22 [1] CRAN (R 4.0.0)
## purrr * 0.3.4 2020-04-17 [1] CRAN (R 4.0.0)
## R6 2.5.0 2020-10-28 [1] CRAN (R 4.0.2)
## Rcpp 1.0.5 2020-07-06 [1] CRAN (R 4.0.2)
## readr * 1.4.0 2020-10-05 [1] CRAN (R 4.0.2)
## readxl 1.3.1 2019-03-13 [1] CRAN (R 4.0.2)
## reprex 0.3.0 2019-05-16 [1] CRAN (R 4.0.2)
## rlang 0.4.9 2020-11-26 [1] CRAN (R 4.0.2)
## rmarkdown 2.5 2020-10-21 [1] CRAN (R 4.0.2)
## rstudioapi 0.13 2020-11-12 [1] CRAN (R 4.0.2)
## rvest 0.3.6 2020-07-25 [1] CRAN (R 4.0.2)
## savonliquide 0.1.0 2020-12-07 [1] CRAN (R 4.0.2)
## scales 1.1.1 2020-05-11 [1] CRAN (R 4.0.0)
## sessioninfo 1.1.1 2018-11-05 [1] CRAN (R 4.0.0)
## stringi 1.5.3 2020-09-09 [1] CRAN (R 4.0.2)
## stringr * 1.4.0 2019-02-10 [1] CRAN (R 4.0.0)
## tibble * 3.0.4 2020-10-12 [1] CRAN (R 4.0.2)
## tidyr * 1.1.2 2020-08-27 [1] CRAN (R 4.0.2)
## tidyselect 1.1.0 2020-05-11 [1] CRAN (R 4.0.0)
## tidyverse * 1.3.0 2019-11-21 [1] CRAN (R 4.0.2)
## vctrs 0.3.6 2020-12-17 [1] CRAN (R 4.0.2)
## withr 2.3.0 2020-09-22 [1] CRAN (R 4.0.2)
## xfun 0.19 2020-10-30 [1] CRAN (R 4.0.2)
## xml2 1.3.2 2020-04-23 [1] CRAN (R 4.0.0)
## yaml 2.2.1 2020-02-01 [1] CRAN (R 4.0.0)
##
## [1] /Library/Frameworks/R.framework/Versions/4.0/Resources/library
I assure you this is an excellent savon liquide pun.↩︎
{badger} has functions to generate a bunch of badges you’re likely to want. {badgr} focuses only on custom badges and has some extra options relative to
badger::badge_custom()
, like the ability to add an icon. But wow, how did I miss this?↩︎#deep↩︎