Ignore if you don't need this bit of support.
This is one in a series of tutorials in which we explore basic data import, exploration and much more using data from the Gapminder project. Now is the time to make sure you are working in the appropriate directory on your computer, perhaps through the use of an RStudio project. To ensure a clean slate, you may wish to clean out your workspace and restart R (both available from the RStudio Session menu, among other methods). Confirm that the new R process has the desired working directory, for example, with the getwd()
command or by glancing at the top of RStudio's Console pane.
Open a new R script (in RStudio, File > New > R Script). Develop and run your code from there (recommended) or periodicially copy "good" commands from the history. In due course, save this script with a name ending in .r or .R, containing no spaces or other funny stuff, and evoking "colors".
Assuming the data can be found in the current working directory, this works:
gDat <- read.delim("gapminderDataFiveYear.txt")
Plan B (I use here, because of where the source of this tutorial lives):
## data import from URL
gdURL <- "http://www.stat.ubc.ca/~jenny/notOcto/STAT545A/examples/gapminder/data/gapminderDataFiveYear.txt"
gDat <- read.delim(file = gdURL)
Basic sanity check that the import has gone well:
str(gDat)
## 'data.frame': 1704 obs. of 6 variables:
## $ country : Factor w/ 142 levels "Afghanistan",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ year : int 1952 1957 1962 1967 1972 1977 1982 1987 1992 1997 ...
## $ pop : num 8425333 9240934 10267083 11537966 13079460 ...
## $ continent: Factor w/ 5 levels "Africa","Americas",..: 3 3 3 3 3 3 3 3 3 3 ...
## $ lifeExp : num 28.8 30.3 32 34 36.1 ...
## $ gdpPercap: num 779 821 853 836 740 ...
The color demos below will be more effective if the default plotting symbol is a solid circle. We limit ourselves to base R graphics in this tutorial, therefore we use par()
, the function that queries and sets base R graphical parameters. In an interactive session or in a plain R script, do this:
## how to change the plot symbol in a simple, non-knitr setting
opar <- par(pch = 19)
Technically, you don't need to make the assignment, but it's a good practice. We're killing two birds with one stone:
opar
.When you change a graphical parameter via par()
, the original values are returned and we're capturing them via assignment to opar
. At the very bottom of this tutorial, we use opar
to restore the original state.
Big picture, it is best practice to restore the original, default state of hidden things that affect an R session. This is polite if you plan to inflict your code on others. Even if you live on an R desert island, this practice will prevent you from creating maddening little puzzles for yourself to solve in the middle of the night before a deadline.
Because of the way figures are handled by knitr
, it is more complicated to change the default plotting symbol throughout, e.g., an R Markdown document. To see how I've done it, check out a hidden chunk around here in the source of this page.
I need a small well-behaved excerpt from the Gapminder data for demonstration purposes. I randomly draw 8 countries, keep their data from 2007, and sort the rows based on GDP per capita. Meet jDat
.
jDat
## country year pop continent lifeExp gdpPercap
## 504 Eritrea 2007 4906585 Africa 58.04 641.4
## 1080 Nepal 2007 28901790 Asia 63.78 1091.4
## 276 Chad 2007 10238807 Africa 50.65 1704.1
## 792 Jamaica 2007 2780132 Americas 72.57 7320.9
## 396 Cuba 2007 11416987 Americas 78.27 8948.1
## 360 Costa Rica 2007 4133884 Americas 78.78 9645.1
## 576 Germany 2007 82400996 Europe 79.41 32170.4
## 1152 Norway 2007 4627926 Europe 80.20 49357.2
A simple scatterplot, using plot()
from the base package graphics
.
jXlim <- c(460, 60000)
jYlim <- c(47, 82)
plot(lifeExp ~ gdpPercap, jDat, log = 'x', xlim = jXlim, ylim = jYlim,
main = "Start your engines ...")
You can specify color explicitly by name by supplying a character vector with one or more color names (more on those soon). If you need a color for 8 points and you input fewer, recycling will kick in. Here's what happens when you specify one or two colors via the col =
argument of plot()
.
plot(lifeExp ~ gdpPercap, jDat, log = 'x', xlim = jXlim, ylim = jYlim,
col = "red", main = 'col = "red"')
plot(lifeExp ~ gdpPercap, jDat, log = 'x', xlim = jXlim, ylim = jYlim,
col = c("blue", "orange"), main = 'col = c("blue", "orange")')
You can specify color explicitly with a small positive integer, which is interpreted as indexing into the current palette, which can be inspected via palette()
. I've added these integers and the color names as labels to the figures below. The default palette contains 8 colors, which is why we're looking at data from eight countries. The default palette is ugly.
plot(lifeExp ~ gdpPercap, jDat, log = 'x', xlim = jXlim, ylim = jYlim,
col = 1:nC, main = paste0('col = 1:', nC))
with(jDat, text(x = gdpPercap, y = lifeExp, pos = 1))
plot(lifeExp ~ gdpPercap, jDat, log = 'x', xlim = jXlim, ylim = jYlim,
col = 1:nC, main = 'the default palette()')
with(jDat, text(x = gdpPercap, y = lifeExp, labels = palette(),
pos = rep(c(1, 3, 1), c(5, 1, 2))))
You can provide your own vector of colors instead. I am intentionally modelling best practice here too: if you're going to use custom colors, store them as an object in exactly one place, and use that object in plot calls, legend-making, etc. This makes it much easier to fiddle with your custom colors, which few of us can resist.
jColors <- c('chartreuse3', 'cornflowerblue', 'darkgoldenrod1', 'peachpuff3',
'mediumorchid2', 'turquoise3', 'wheat4', 'slategray2')
plot(lifeExp ~ gdpPercap, jDat, log = 'x', xlim = jXlim, ylim = jYlim,
col = jColors, main = 'custom colors!')
with(jDat, text(x = gdpPercap, y = lifeExp, labels = jColors,
pos = rep(c(1, 3, 1), c(5, 1, 2))))
Who would have guessed that R knows about "peachpuff3"? To see the names of all 657 the built-in colors, use colors()
.
head(colors())
## [1] "white" "aliceblue" "antiquewhite" "antiquewhite1"
## [5] "antiquewhite2" "antiquewhite3"
tail(colors())
## [1] "yellow" "yellow1" "yellow2" "yellow3" "yellow4"
## [6] "yellowgreen"
But it's much more exciting to see the colors displayed! Lots of people have tackled this -- for colors, plotting symbols, line types -- and put their work on the internet. Some examples:
Most of us are pretty lousy at choosing colors and it's easy to spend too much time fiddling with them. Cynthia Brewer, a geographer and color specialist, has created sets of colors for print and the web and they are available in the add-on package RColorBrewer
. You will need to install and load this package to use.
#install.packages("RColorBrewer")
library(RColorBrewer)
Let's look at all the associated palettes.
display.brewer.all()
They fall into three classes. From top to bottom, they are
You can view a single RColorBrewer palette by specifying its name:
display.brewer.pal(n = 8, name = 'Dark2')
The package is, frankly, rather clunky, as evidenced by the requirement to specify n
above. Sorry folks, you'll just have to cope.
Here we revisit specifying custom colors as we did above, but using a palette from RColorBrewer instead of our artisanal "peachpuff3" work of art. As before, I display the colors themselves but you'll see we're not getting the friendly names you've seen before, which brings us to our next topic.
jBrewColors <- brewer.pal(n = 8, name = "Dark2")
plot(lifeExp ~ gdpPercap, jDat, log = 'x', xlim = jXlim, ylim = jYlim,
col = jBrewColors, main = 'Dark2 qualitative palette from RColorBrewer')
with(jDat, text(x = gdpPercap, y = lifeExp, labels = jBrewColors,
pos = rep(c(1, 3, 1), c(5, 1, 2))))
Instead of small positive integers and Crayola-style names, a more general and machine-readable approach to color specification is as hexadecimal triplets. Here is how the RColorBrewer Dark2 palette is actually stored:
brewer.pal(n = 8, name = "Dark2")
## [1] "#1B9E77" "#D95F02" "#7570B3" "#E7298A" "#66A61E" "#E6AB02" "#A6761D"
## [8] "#666666"
The leading #
is just there by convention. Parse the hexadecimal string like so: #rrggbb
, where rr
, gg
, and bb
refer to color intensity in the red, green, and blue channels, respectively. Each is specified as a two-digit base 16 number, which is the meaning of "hexadecimal" (or "hex" for short). Here's a table relating base 16 numbers to the beloved base 10 system.
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
hex | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Example: the first color in the palette is specified as "#1B9E77", so the intensity in the green channel is 9E. What does that mean? \[
9E = 9 * 16^1 + 14 * 16^0 = 9 * 16 + 14 = 158
\] Note that the lowest possible channel intensity is 00
= 0 and the highest is FF
= 255.
Important special cases that help you stay oriented. Here are the saturated RGB colors, red, blue, and green:
colorName | hex | red | green | blue |
---|---|---|---|---|
blue | #0000FF | 0 | 0 | 255 |
green | #00FF00 | 0 | 255 | 0 |
red | #FF0000 | 255 | 0 | 0 |
Here are shades of gray:
colorName | hex | red | green | blue |
---|---|---|---|---|
white, gray100 | #FFFFFF | 255 | 255 | 255 |
gray67 | #ABABAB | 171 | 171 | 171 |
gray33 | #545454 | 84 | 84 | 84 |
black, gray0 | #000000 | 0 | 0 | 0 |
Note that everywhere you see "gray" above, you will get the same results if you substitute "grey". We see that white corresponds to maximum intensity in all channels and black to the minimum.
To review, here are the ways to specify colors in R:
palette()
)colors()
Here are some functions to read up on if you want to learn more -- don't forget to mine the "See Also" section of the help to expand your horizons: rgb()
, col2rgb()
, convertColor()
.
The RGB color space or model is by no means the only or best one. It's natural for describing colors for display on a computer screen but some really important color picking tasks are hard to execute in this model. For example, it's not obvious how to construct a qualitative palette where the colors are easy for humans to distinguish, but are also perceptually comparable to one other. Appreciate this: we can use RGB to describe colors to the computer but we don't have to use it as the space where we construct color systems.
Color models generally have three dimensions, as RGB does, due to the physiological reality that humans have three different receptors in the retina. (Here is an informative blog post on RGB and the human visual system.) The closer a color model's dimensions correspond to distinct qualities people can perceive, the more useful it is. This correspondence facilitates the deliberate construction of palettes and paths through color space with specific properties. RGB lacks this concordance with human perception. Just because you have photoreceptors that detect red, green, and blue light, it doesn't mean that your perceptual experience of color breaks down that way. Do experience the color yellow as a mix of red and green light? No, of course not, but that's the physiological reality. An RGB alternative you may have encountered is the Hue-Saturation-Value (HSV) model. Unfortunately, it is also quite problematic for color picking, due to its dimensions being confounded with each other.
What are the good perceptually-based color models? CIELUV and CIELAB are two well-known examples. We will focus on a variant of CIELUV, namely the Hue-Chroma-Luminance (HCL) model. It is written up nicely for an R audience in Zeileis, et al (see References for citation and link). There is a companion R package colorspace
, which will help you to explore and exploit the HCL color model. Finally, this color model is fully embraced in ggplot2
(as are the RColorBrewer
palettes).
Here's what I can tell you about the HCL model's three dimensions:
Full disclosure: I have a hard time really grasping and distinguishing chroma and luminance. As we point out above, they are not entirely independent, which speaks to the weird shape of the 3 dimensional HCL space.
Figure 6.6 in Wickham's ggplot2
book is helpful for understanding the HCL color space.
Figure 6.6 of Wickham's ggplot2 book
JB re-phrasing and combining Wickham's description and caption for this figure: Each facet or panel depicts a slice through HCL space for a specific luminance, going from low to high. The extreme luminance values of 0 and 100 are omitted because they would, respectively, be a single black point and a single white point. Within a slice, the centre has chroma 0, which corresponds to a shade of grey. As you move toward the slice's edge, chroma increases and the color gets more pure and intense. Hue is mapped to angle.
A valuable contribution of the colorspace
package is that it provides functions to create color palettes traversing color space in a rational way. In contrast, the palettes offered by RColorBrewer
, though well-crafted, are unfortunately fixed.
Here I plan to insert/recreate some visuals from the Zeileis et al paper or from the
colorspace
vignette. For the moment, that stuff is sitting in the PDF slides. So go there!
the dichromat
package (on CRAN)
obviously need to do some writing here!
## NOT RUN
## execute this if you followed my code for
## changing the default plot symbol in a simple, non-knitr setting
## reversing the effects of this: opar <- par(pch = 19)
par(opar)
Achim Zeileis, Kurt Hornik, Paul Murrell (2009). Escaping RGBland: Selecting Colors for Statistical Graphics. Computational Statistics & Data Analysis, 53(9), 3259-3270. DOI | PDF
Vignette for the colorspace
package
Earl F. Glynn (Stowers Institute for Medical Research)
colors()
Blog post My favorite RGB color on the Many World Theory blog
ggplot2: Elegant Graphics for Data Analysis available via SpringerLink by Hadley Wickham, Springer (2009) | online docs (nice!) | author's website for the book, including all the code | author's landing page for the package
Consider incorporating in future version of this material: