Color is very effective when used well.

But using color well is not easy.

Some of the issues:

An internet “controversy” in 2015: The Dress (and a follow-up article)

A note on rainbow colors.

RGB, HSV, and HCL Color Spaces

Computer monitors and projectors work in terms of red, green, and blue light.

cols <- c("red", "green", "blue", "yellow", "cyan", "magenta")
rgbcols <- col2rgb(cols)
rgbcols
##       [,1] [,2] [,3] [,4] [,5] [,6]
## red    255    0    0  255    0  255
## green    0  255    0  255  255    0
## blue     0    0  255    0  255  255

Hue, saturation, value (HSV) is a simple transformation of RGB.

rgb2hsv(rgbcols)
##   [,1]      [,2]      [,3]      [,4] [,5]      [,6]
## h    0 0.3333333 0.6666667 0.1666667  0.5 0.8333333
## s    1 1.0000000 1.0000000 1.0000000  1.0 1.0000000
## v    1 1.0000000 1.0000000 1.0000000  1.0 1.0000000

HSV is a little more convenient since it allows the hue to be controlled separately.

But saturation and value attributes do not work particularly well.

A color wheel of fully saturated colors:

wheel <- function(col, radius = 1, ...)
    pie(rep(1, length(col)), col = col, radius = radius, ...)
wheel(rainbow(6))

Fully saturated yellow is brighter than red, which is brighter than blue.

Removing saturation:

library(colorspace)
## Loading required package: methods
wheel(desaturate(rainbow(6)))

The hue, chroma, luminance (HCL) space allows separate control of:

HCL is also more perceptually uniform:

pal <- function(col, border = "light gray", ...)
{
  n <- length(col)
  plot(0, 0, type="n", xlim = c(0, 1), ylim = c(0, 1),
    axes = FALSE, xlab = "", ylab = "", ...)
  rect(0:(n-1)/n, 0, 1:n/n, 1, col = col, border = border)
}

pal(rainbow(6))

pal(rainbow_hcl(6))

These colors have approximately equal chroma and luminance values.

For a fully saturated red,

red_hcl <- list(h = 12.17395, c = 179.04076, l = 53.24059)

varying only chroma to reduce the amount of color:

pal(hcl(red_hcl$h, red_hcl$c * seq(0, 1, len = 10), red_hcl$l))

and varying only luminance:

pal(hcl(red_hcl$h, red_hcl$c, red_hcl$l * seq(0, 1, len = 10)))

HCL is a transformation of the CIEluv color space designed for perceptual uniformity.

The definition of the luminance takes into account the light sensitivity of a standard human observer at various wave lengths.

For a given hue, not all combinations of chroma and luminance are possible.

For all hues, luminance of 100 should correspond to white and luminance of 0 to black.

Light sensitivity for different wave lengths in daylight conditions (photopic vision) and under dark adapted conditions (scotopic vision):

Opponent Process Theory

The Opponent Process Model of vision says that the brain divides the visual signal among three opposing contrast pairs:

The black/white pair corresponds to luminance in HCL

Hue and chroma in HCL span the two chromatic axes.

The luminance axis has higher resolution than the two chromatic axes.

The major form of color blindness reflects an inability to distinguish differences along the red/green axis.

Impairment along the yellow/blue axis does occur as well but is much rarer.

Contrast and Comparisons

Vision reacts to differences, not absolutes.

Small differences in shading or hue can be recognized when objects are contiguous but be much harder to see when they are separated.

Simultaneous brightness contrast: a grey patch on a dark background looks lighter than the same grey patch on a light background.

plot(0, 0, type="n", xlim = c(0, 1), ylim = c(0, 1),
    axes = FALSE, xlab = "", ylab = "")
rect(0, 0, 0.5, 1, col = "lightgrey", border = NA)
rect(0.5, 0, 1, 1, col = "darkgrey", border = NA)
rect(0.2, 0.3, 0.3, 0.7, col = "grey", border = NA)
rect(0.7, 0.3, 0.8, 0.7, col = "grey", border = NA)

Using luminance or grey scale alone does not work well for encoding categorical variables against a key.

Grey scale can be effective for showing continuous transitions in pseudo-color images.

filled.contour(volcano, color.palette = grey.colors)

Grey scale is less effective for segmented maps, or choropleth maps; only a few levels can be accurately decoded.

Interactions with Size, Background and Proximity

For small items more contrast and more saturated colors are needed:

Variations in luminance are particularly helpful for seeing fine structure, such as small text or small symbols:

plot(0, type = "n", xlim = c(0, 1), ylim = c(0, 1),
     axes = FALSE, xlab = "", ylab = "")
rect(0, 0, 1, 1, col = hcl(0)) ## defaults: c = 35, l = 85
qbf <- "The quick brown fox jumps ..."
text(0.5, 0.3, label = qbf, col = hcl(180))
text(0.5, 0.5, label = qbf, col = hcl(0, c = 70))
text(0.5, 0.7, label = qbf, col = hcl(0, l = 50))

Chrominance (hue and chroma) differences alone are not sufficient for small items.

Ware recommends a luminance contrast of at least 3:1 for small text; 10:1 is preferable.

Small areas also need variation in more than hue:

Contrasting borders can help for larger areas with similar luminance:

N <- nrow(trees)
op <- palette(rainbow(N, end = 0.9))
f <- function(fg)
    with(trees,
         symbols(Height, Volume, circles = Girth/16, inches = FALSE, bg = 1:N,
         fg = fg, main = "symbols(*, circles = Girth/16, bg = 1:N)"))
f(NA)

f("gray30")

palette(op)

Color Specification in R

Hexadecimal form:

library(colorspace)
hex2RGB("#FF0000")
##      R G B
## [1,] 1 0 0

Named colors

col2rgb("red")
##       [,1]
## red    255
## green    0
## blue     0
col2rgb("forestgreen")
##       [,1]
## red     34
## green  139
## blue    34
colors()
demo(colors)

Color spaces:

rgb(1, 0, 0)
## [1] "#FF0000"
rgb(255, 0, 0, max = 255)
## [1] "#FF0000"
rgb2hsv(col2rgb("red"))
##   [,1]
## h    0
## s    1
## v    1

Converting to HCL:

rgb2hcl <- function(col) {
    ## ignores alpha
    col <- RGB(t(col[1 :3, ]) / 255)
    col <- as(col, "polarLUV")
    col <- t(col@coords[, 3 : 1, drop = FALSE])
    rownames(col) <- tolower(rownames(col))
    col
}

rgb2hcl(col2rgb("red"))
##        [,1]
## h  12.17395
## c 179.04076
## l  53.24059
rgb2hcl(col2rgb("green"))
##       [,1]
## h 127.7235
## c 135.7811
## l  87.7351
rgb2hcl(col2rgb("blue"))
##        [,1]
## h 265.87278
## c 130.67593
## l  32.29567
rgb2hcl(col2rgb("yellow"))
##        [,1]
## h  85.87351
## c 107.06462
## l  97.13951
rgb2hcl(col2rgb("cyan"))
##        [,1]
## h 192.16714
## c  72.09794
## l  91.11330
rgb2hcl(col2rgb("magenta"))
##        [,1]
## h 307.72618
## c 137.40166
## l  60.32351

hcl(12.17, 179.04, 53.24)
## [1] "#FF0000"

Color pickers:

colourPicker()

Munsell Color Space

Another color space, similar to HCL, is the Munsell system developed in the early 1900s.

This system uses a Hue, Value, Chroma encoding.

The munsell package provides an R interface and is used in ggplot.

Munsell specifications are of the form "H V/C", such as5R 5/10`. Possible hues are

library(munsell)
mnsl_hues()
##  [1] "2.5R"  "5R"    "7.5R"  "10R"   "2.5YR" "5YR"   "7.5YR" "10YR" 
##  [9] "2.5Y"  "5Y"    "7.5Y"  "10Y"   "2.5GY" "5GY"   "7.5GY" "10GY" 
## [17] "2.5G"  "5G"    "7.5G"  "10G"   "2.5BG" "5BG"   "7.5BG" "10BG" 
## [25] "2.5B"  "5B"    "7.5B"  "10B"   "2.5PB" "5PB"   "7.5PB" "10PB" 
## [33] "2.5P"  "5P"    "7.5P"  "10P"   "2.5RP" "5RP"   "7.5RP" "10RP"

Adjusting colors in the value, chroma, and hue dimensions:

my_blue <- "5PB 5/8"
plot_mnsl(c(
  lighter(my_blue, 2),      my_blue,   darker(my_blue, 2),
  desaturate(my_blue, 2),   my_blue,   saturate(my_blue, 2),
  rygbp(my_blue, 2),        my_blue,   pbgyr(my_blue, 2)))

Creating scales:

plot_mnsl(sapply(0:6, darker, col = "5PB 7/4")) + facet_wrap(~ num, nrow = 1)

Examining available colors:

hue_slice("5R")
## Warning: Removed 19 rows containing missing values (geom_text).

value_slice(5)

complement_slice("5R")

Color Palettes

Color palettes are collections of colors that work well together.

It is useful to distinguish

Tools for selecting palettes include:

R color palette functions:

These all take the number of colors as an argument, as well as some additional optional arguments.

colorRampPalette can be used to create a palette function that interpolates between a set of colors using

rwb <- colorRampPalette(c("red", "white", "blue"))
rwb(5)
## [1] "#FF0000" "#FF7F7F" "#FFFFFF" "#7F7FFF" "#0000FF"
filled.contour(volcano, color.palette = rwb, asp = 1)

With more perceptually comparable extremes (from the Blue-Red palette of HCL Wizard):

rwb1 <- colorRampPalette(c("#8E063B", "white", "#023FA5"))
filled.contour(volcano, color.palette = rwb1, asp = 1)

Most base and lattice functions allow a vector of colors to be specified.

Some, like filled.contour and levelplot allow a palette function to be provided.

ggplot provides a framework for specifying palette functions to use.

RColorBrewer Palettes

Display all available palettes:

library(RColorBrewer)
display.brewer.all()

Show a particular palette:

display.brewer.pal(9, "Blues")

A palette as RGB values:

brewer.pal(9, "Blues")
## [1] "#F7FBFF" "#DEEBF7" "#C6DBEF" "#9ECAE1" "#6BAED6" "#4292C6" "#2171B5"
## [8] "#08519C" "#08306B"

The palettes are limited to a maximum number of levels. To obtain more levels you can interpolate:

brewer.pal(10, "Blues")
## Warning in brewer.pal(10, "Blues"): n too large, allowed maximum for palette Blues is 9
## Returning the palette you asked for with that many colors
## [1] "#F7FBFF" "#DEEBF7" "#C6DBEF" "#9ECAE1" "#6BAED6" "#4292C6" "#2171B5"
## [8] "#08519C" "#08306B"

pbrbl <- colorRampPalette(brewer.pal(9, "Blues"), interpolate = "spline")
pbrbl
## function (n) 
## {
##     x <- ramp(seq.int(0, 1, length.out = n))
##     if (ncol(x) == 4L) 
##         rgb(x[, 1L], x[, 2L], x[, 3L], x[, 4L], maxColorValue = 255)
##     else rgb(x[, 1L], x[, 2L], x[, 3L], maxColorValue = 255)
## }
## <bytecode: 0x560b27403ee8>
## <environment: 0x560b25b76408>
pbrbl(10)
##  [1] "#F7FBFF" "#E0ECF7" "#CCDEF1" "#ADD0E5" "#81BBDA" "#57A1CF" "#3687C0"
##  [8] "#1A69B0" "#064D98" "#08306B"

The vignette for the colorspace describes how to choose their palettes.

Palettes in R Graphics

Playfair <- read.table("Playfair.dat")
Playfair$city <- rownames(Playfair)
rownames(Playfair) <- NULL

Specifying a vector of colors in base and lattice graphics:

with(Playfair, plot(population, col = rainbow(length(population))))
with(Playfair, plot(population, col = rainbow(2)))
with(Playfair, barplot(population, col = rainbow(length(population))))
with(Playfair, barplot(population, col = rainbow(2)))

with(Playfair,
     barchart(reorder(city, population) ~ population,
              col = rainbow(length(population))))
with(Playfair,
     barchart(reorder(city, population) ~ population, col = rainbow(2)))

A better approach for lattice is to use par.settings; this will make sure a legend is consistent with the plot colors.

filled.contour and levelplot also accept a palette function:

filled.contour(volcano, color.palette = terrain.colors)
levelplot(volcano, col.regions=terrain.colors)

ggplot uses scale_color_xyz or scale_fill_xyz.

For discrete scales the choices for xyz are

For continuous scales the choices for xyz are

The default for discrete scales is hue.

p <- ggplot(Playfair, aes(reorder(city, population), population))
p + geom_bar(aes(fill = city),stat = "identity") + coord_flip()
p + geom_point(aes(color = city))

Discrete examples for brewer and manual:

ggplot(diamonds) + geom_bar(aes(cut, fill = cut)) +
    scale_fill_brewer(palette = "Spectral")

p + geom_bar(aes(fill = city),stat = "identity") + coord_flip() +
    scale_fill_brewer(palette = "Spectral")

pbrsp <- colorRampPalette(brewer.pal(9, "Spectral"), interpolate = "spline")
p + geom_bar(aes(fill = city),stat = "identity") + coord_flip() +
    scale_fill_manual(values = pbrsp(nrow(Playfair)))

The default for continuous scales is gradient from a light blue to a dark blue:

V <- data.frame(x = rep(1 : nrow(volcano), ncol(volcano)),
                y = rep(1 : ncol(volcano), each = nrow(volcano)),
                z = as.vector(volcano))
p <- ggplot(V, aes(x, y, fill = z)) + geom_raster()
p

p + scale_fill_gradient2(low = "red", mid = "white", high = "blue",
                         midpoint = median(volcano))
p + scale_fill_gradientn(colors = terrain.colors(8))

nc <- 8
pd <- ggplot(V, aes(x, y, fill = cut(z, nc))) + geom_raster()
pd
pd + scale_fill_manual(values = terrain.colors(nc))

filled.contour(volcano, color.palette = terrain.colors, nlevels = nc)

HCL Wizard Notes

The HCL space as show in the ggplot book. Hue is mapped to angle, chroma is mapped to radius, luminance to facets. The origins with zero chroma are shades of grey.

Changing the Background Color

In base graphics, you can use par to set the background for the entire plot:

opar <- par(bg = 'lightblue')
plot(1 : 10)

par(opar)

In principle, you can use the panel.first argument to plot to provide an expression that fills a rectangle covering the usr region:

panel.color <- function(col) {
    usr <- par("usr")
    rect(usr[1], usr[3], usr[2], usr[4], col = col)
}

plot(1:10, panel.first = panel.color("grey90"))

plot(1:10, type = "n")
panel.color("grey90")
points(1 : 10)

Lattice supports use of themes.

To change plot and panel backgrounds:

thm <- list(background = list(col = "lightblue"),
            panel.background = list(col = "grey90"))

show.settings()

show.settings(thm)

xyplot(1 : 10 ~ 1 : 10, par.settings = thm)

To adjust the default settings you could use

trellis.par.set(thm)

ggplot also supports the notion of themes:

gthm <- theme(plot.background = element_rect(fill = "lightblue", color = NA),
              panel.background=element_rect(fill = "lightblue2"))

p <- ggplot(NULL) + geom_point(aes(1:10, 1:10))
p + gthm

You can use theme_set to change the default settings.

ggplot provides a number of alternative themes. Some examples:

p + theme_bw()
p + theme_classic()

The ggthemes package provides some additional themes, including

library(ggthemes)
p + theme_economist()
p + theme_wsj()
p + theme_tufte()
p + theme_few()
p + theme_map()

Bivariate Palettes

It is possible to encode two variables in a palette. Some sample palettes:

Reduced Color Vision

Color blindness affects about 10% of males, a smaller percentage of females.

Culture, Tradition, and Conventions

Colors can have different meanings in different cultures.

Conventions can also give colors particular meanings.

Some conventions:

Traffic Lights

  • Traffic lights use red/green, even though this is a maijor axis of color blindness.

  • The convention comes from railroads.

  • The red used generally contains some orange and the green contains blue to help with red/green color blindness.

  • Position provides an alternate encoding. Orientations do vary.

Microarray Heatmaps

  • Microarrays are used for the analysis of gene-level changes and differences in bio-medical research.
  • Dyes are used that result in genes with a high response appearing red and genes with a low response appearing green.
  • In keeping with this physical characteristic of microarrays, a common visualization of the data is as a red/green heat map.

Red States and Blue States

  • Since the 2000 presidential election it has become standard to refer to Republican-leaning states as red states and Democrat leaning states as blue states.

  • Prior to 1980 it was somewhat more traditional to use red for more left-leaning Democrats. A map of the 1960 election results uses these more traditional colors.

  • In 1996 the New York TImes used blue for Dempcrat, red for Republican, but the Washington Post used the opposite color scheme.

  • The long, drawn out process of the 2000 election may have contributed to fixing the color schema at the current convention.

Notes

References

Coloring Political Statements

I scraped the data as of April 11, 2017, from POLITIFACT; they are available in https://www.stat.uiowa.edu/~luke/data/polfac.dat

if (! file.exists("polfac.dat"))
    download.file("https://www.stat.uiowa.edu/~luke/data/polfac.dat",
                  "polfac.dat")
pft <- read.table("polfac.dat", stringsAsFactors=FALSE)
vcp <- prop.table(as.matrix(pft), 1)[, 6:1]
colnames(vcp) <- gsub("\\.", " ", colnames(vcp))

head(vcp)
##          Pants on Fire     False Mostly False  Half True Mostly True
## Trump       0.16279070 0.3281654    0.1989664 0.14470284  0.12403101
## Bachmann    0.26229508 0.3606557    0.1311475 0.09836066  0.06557377
## Cruz        0.06779661 0.2796610    0.3050847 0.12711864  0.16101695
## Gingrich    0.13924051 0.1898734    0.2025316 0.25316456  0.12658228
## Palin       0.09523810 0.3015873    0.1428571 0.14285714  0.09523810
## Santorum    0.08333333 0.2833333    0.2000000 0.21666667  0.11666667
##                True
## Trump    0.04134367
## Bachmann 0.08196721
## Cruz     0.05932203
## Gingrich 0.08860759
## Palin    0.22222222
## Santorum 0.10000000

The Daily Kos chart is ordered by the percentage of statements that are more false than true. A function to produce a bar chart with a specified color palette:

polbars <- function(col = cm.colors(6)) {
    barchart(vcp[order(rowSums(vcp[, 1 : 3])),], auto.key = TRUE,
             par.settings = list(superpose.polygon=list(col = col)))
}
polbars()

The original Daily Kos chart seems to use a slightly modified version of the Color Brewer Spectral palette, a diverging palette.

polbars(brewer.pal(6, "Spectral"))

dkcols <- brewer.pal(6, "Spectral")
dkcols[4] <- "lightgrey"
polbars(dkcols)

The JunkCharts plot uses another diverging palette, close to the Blue-Red palette available in hclwizard.

rwbcols <- c("#4A6FE3", "#8595E1", "#B5BBE3", "#E2E2E2",
             "#E6AFB9", "#E07B91", "#D33F6A")
polbars(rwbcols)

polbars(rev(rwbcols))

Another diverging palette:

polbars(brewer.pal(7, "PiYG"))

A sequential palette:

polbars(rev(brewer.pal(6, "Oranges")))