A default R plot is rarely a good plot, and the gap between the two is mostly a handful of arguments. This appendix collects the choices the book makes and what each one is for, so you can apply them to figures the book never draws. They are house style, and the theme running through them is that ink should carry information, since borders, bold titles, and full-strength colors draw the eye without telling the reader anything.
Distributions of Data
Whatever figure you have, show the raw data with it, such as a rug under a histogram or a stripchart on a boxplot.
A histogram of data summarizes the \(n\) observations you actually have. The two panels below plot the same data with R’s defaults and with the book’s conventions.
Code
data(Wages1, package='Ecdat')
x_hat <- Wages1[, 'wage'] # lab data: hourly wages in 1980 dollars
par(mfrow=c(1,2), mar=c(4, 4, 2, 1))
# Default settings
hist(x_hat)
# House style
hist(x_hat, breaks=25, border=NA, freq=FALSE, main=NA, # more bins, since n is 3294
xlab='Hourly wage (1980 dollars)')
rug(x_hat, col=grey(0, .5))
The bar borders are gone, so the distribution reads as one silhouette instead of twenty outlined boxes. The vertical axis is a density rather than a count, so a fitted curve can be drawn on the same axes. The axis label gives the variable and its units, and the rug along the bottom marks each of the observations that the bars summarize.
border |
NA |
Never use visible borders on bars |
freq |
FALSE |
Density scale, so the bars integrate to one and a density curve can be overlaid |
main |
NA or omit |
Add titles via title() instead |
breaks |
15–25 integer |
Fewer bins when \(n\) is small, so each bin holds several observations |
col |
Default grey, or group colors |
Use transparency for overlays |
xlab |
Short descriptive string |
Use expression() for math notation |
An ECDF shows the same observations without any bins, so it needs no choice of breaks.
Boxplots
A boxplot compresses the data into five numbers and any outliers, and stripchart() adds back the raw data. The two panels below plot the same groups with R’s defaults and with the book’s conventions.
Code
cols <- c(rgb(.8, 0, 0, .5), rgb(0, 0, .8, .5))
med_cols <- adjustcolor(cols, alpha.f=2, red.f=.5, green.f=.5, blue.f=.5) # darker shades, for the medians
par(mfrow=c(1,2), mar=c(4, 4, 2, 1))
# Default settings
boxplot(wage ~ sex, data=Wages1)
# House style
# The axis stops just past 20 dollars, so the 16 largest wages are not drawn
boxplot(wage ~ sex, data=Wages1, col=cols, border=NA, medcol=med_cols, main=NA, ylim=c(0, 20),
whisklty=0, staplelty=0, outline=FALSE,
xlab='Sex', ylab='Hourly wage (1980 dollars)')
# Raw Observations, in place of whiskers and outliers
stripchart(wage ~ sex, data=Wages1,
pch='-', col=grey(0, .2), cex=1,
vert=TRUE, add=TRUE)
Each dash is one observation, which exposes the gaps, clusters, and repeated values that five numbers hide. whisklty=0, staplelty=0, and outline=FALSE remove the whiskers and outliers, since the dashes now show those observations. outline=FALSE also drops the outliers from the axis range, so set ylim yourself. add=TRUE draws the dashes on the existing boxplot, and vert=TRUE matches its orientation.
Use main=NA in boxplot() and add titles with title(..., font.main=1). Color grouped boxplots with the group colors from The Grey and RGB System, set border=NA, and color the median a darker shade of the same color with medcol. The adjustcolor() call above gives that shade by halving the red, green, and blue values and doubling the alpha. border also colors the median, whiskers, and outliers, so medcol restores the median and the raw data replace the rest.
Histograms of Resampled Data
A histogram of resampled data summarizes statistics rather than observations: hundreds or thousands of bootstrap, jackknife, or permutation draws, or repeated samples from a known population. It keeps the default grey fill, border=NA, and freq=FALSE, but differs from a histogram of data in three ways.
breaks |
15–25, fewer when \(n\) is small |
25–100, since draws are plentiful |
rug() |
rug(x_hat, col=grey(0, .5)) |
None, since the draws would merge into a solid band |
| Vertical lines |
A sample statistic in red, lwd=2 |
Interval bounds in black and, for null bootstrap or permutation distributions, the observed statistic in red, both lwd=2 |
Code
# Bootstrap distribution of the mean hourly wage
set.seed(1) # to be replicable
boot_means <- rep(NA, 999)
for (b in seq_along(boot_means)) {
x_boot <- sample(x_hat, replace=TRUE)
boot_means[b] <- mean(x_boot)
}
boot_ci <- quantile(boot_means, probs=c(.025, .975))
hist(boot_means, breaks=50, border=NA, freq=FALSE, main=NA,
xlab='Bootstrap means of hourly wages (1980 dollars)')
abline(v=boot_ci, col=rgb(0, 0, 0, .8), lwd=2) # 95% bootstrap interval
A bootstrap confidence interval does not need the observed statistic marked. A null bootstrap or permutation distribution does, since the observed statistic is the value being tested. A statistic’s distribution is filled with rgb(1, 0, 0, .5) only when introducing the idea, to make clear it is not a histogram of data.
The same line styles apply to any figure that marks a value, not only to histograms.
| Observed statistic |
\(\hat{m}\), \(\hat{t}\), \(\hat{F}\), a bandwidth chosen by cross-validation |
rgb(1, 0, 0, .8) |
2 |
1 |
| Interval bounds |
CI limits, critical values, a significance level \(\alpha\), the middle 95% of a null distribution |
rgb(0, 0, 0, .8) |
2 |
1 |
| Second interval |
Normal against percentile, 90% against 95% |
rgb(0, 0, 0, .8) |
2 |
2 |
| Null or population value |
A hypothesized value, a null of 0, a true mean, a 45-degree line |
rgb(0, 0, 1, .8) |
1 |
2 |
| Theoretical density over a histogram |
A Normal or \(t\) curve |
rgb(0, 0, 1, .8) |
1 |
2, or 3 for a second curve |
| Neutral guide |
A baseline at 0, 0.5 on a CDF, \(\pm 1.96\) |
grey(.5) |
1 |
3 |
Color says what a line is: red is the observed statistic, black is an interval, and blue is a hypothesized, true, or theoretical value. Thickness says where it comes from: thick is computed from this sample, and thin is assumed or known. Line type separates variants within a role, such as a second interval on the same figure. So a reader who has seen one figure in the book can read the next without a legend.
A 0 that is being tested uses the null style, and a 0 that is only a baseline uses the guide style. A significance level \(\alpha\) is chosen rather than computed, but it acts as a cutoff, so it uses the interval style. A density that is the subject of its own figure, rather than a comparison for a histogram, stays solid.
Draw a confidence band around a curve with polygon(..., border=NA), filled with the fit’s color at an alpha of .3, or with grey(0, .25) when the fit has no color. Coverage plots of many intervals are an exception: intervals that cover are rgb(0, 0, 0, .3) and intervals that miss are rgb(1, 0, 0, .5), and the title says so.
Scatterplots and Regression Lines
plot(y_hat ~ x_hat, pch=16, col=grey(0, .5), data=xy_hat, main=NA, xlab='x', ylab='y')
pch |
16 (solid circle) |
Standard throughout the book |
col |
grey(0, alpha) |
Alpha of 0.5 for \(n < 100\), 0.25 up to 1000, 0.1 up to 10000, and 0.05 beyond |
main |
NA or omit |
Titles via title() if needed |
cex |
Reduce for large \(n\) (e.g., .5) |
Default is fine for small datasets |
Transparency is how a scatterplot survives a large sample, since transparent points darken where observations pile up instead of overplotting into a solid blob. To add the marginal distribution of each variable along its axis, see the figure in Joint Distributions, which uses layout() because its panels differ in size.
Regression Lines
Each model keeps one color, so a reader can follow it from figure to figure.
| Straight line |
OLS, through the origin, piecewise, local line |
rgb(1, 0, 0, .8) |
2 |
1 |
| Constant |
A mean, regressogram, local constant |
rgb(0, 0, 1, .8) |
2 |
1 |
| LOESS |
Any span or degree |
rgb(0, .7, .7, .8) |
2 |
1 |
Every fit is thick, since it is computed from this sample. A population line keeps the population style from Histograms of Resampled Data, so a thick blue constant is a fit and a thin dashed blue line is a known value. Variants of one model in the same figure, such as two bandwidths or a fit through the origin, share the model’s color and differ by line type: lty=1, then 2, then 3. Fits drawn separately by group take the group’s color instead of the model’s.
Draw a straight line with abline(reg) and any other fit with lines(x_sorted, y_fit), rather than curve(). lines() takes the fitted values you actually computed, whereas curve() re-evaluates a function, which can quietly differ from the model you fit. Sort by the horizontal variable first, or lines() will zigzag back and forth across the plot.
Fitted values and predictions share the model’s color and width, and differ only in their markers. Fitted values y_fit are a plain line. Predictions y_pred at a few design points use type='o', whose open circles mark where the curve was actually computed. Predictions on a dense grid stay a plain line, since the circles would merge. When a legend shows circles on dashed lines, set seg.len=4, or the circle hides the dashes.
Polishing
Use par(mfrow=c(1,2)) for two panels side by side, c(1,3) for three, and c(2,2) for a grid, with small top and right margins from par(mar=c(4, 4, 1, 1)). If you change par() inside a function, restore it on exit.
op <- par(no.readonly=TRUE)
on.exit(par(op), add=TRUE)
Position legends with a string such as 'topright' or 'topleft' rather than coordinates, so the legend moves with the data instead of landing on top of it. Add bty='n' to drop the box when it clutters the plot, keep entries to one to three words, and match lty, pch, and col to the plot symbols.
The figure below splits the workers by sex and shows the two groups in two panels, with one legend for both.
Code
female <- Wages1[, 'sex'] == 'female'
cols <- c(rgb(.8, 0, 0, .5), rgb(0, 0, .8, .5))
par(mfrow=c(1,2), mar=c(4, 4, 1, 1), oma=c(0, 0, 2, 0))
# Scatterplot
# Both panels stop near 20 dollars, so the 16 largest wages are not drawn
# alpha.f=.2 fades cols from an alpha of .5 to .1, since n is 3294
plot(Wages1[, 'wage'] ~ Wages1[, 'school'], ylim=c(0, 20),
pch=16, col=adjustcolor(ifelse(female, cols[1], cols[2]), alpha.f=.2),
xlab='Years of schooling', ylab='Hourly wage (1980 dollars)')
# ECDFs
F_female_hat <- ecdf(Wages1[female, 'wage'])
F_male_hat <- ecdf(Wages1[!female, 'wage'])
plot(F_female_hat, col=cols[1], lwd=2, verticals=TRUE, do.points=FALSE, col.01line=NA,
xlim=c(0, 20), main=NA,
xlab='Hourly wage (1980 dollars)', ylab='Proportion of workers')
plot(F_male_hat, add=TRUE, col=cols[2], lwd=2, verticals=TRUE, do.points=FALSE, col.01line=NA)
# One legend for both panels, in the outer margin above them
par(fig=c(0, 1, 0, 1), oma=c(0, 0, 0, 0), mar=c(0, 0, 0, 0), new=TRUE)
plot.new()
legend('top', legend=c('Women', 'Men'),
pch=16, lwd=2, col=cols, horiz=TRUE, bty='n',
text.width=.2) # room between entries, which horiz=TRUE packs tightly
Both panels share one cols vector, so a group has the same color everywhere and a single legend can serve both panels. oma=c(0, 0, 2, 0) leaves two lines of outer margin above the panels. The last par() call then lays an empty plot over the whole figure, so that legend('top', ...) lands in that margin rather than inside either panel. The legend sets both pch=16 and lwd=2, so its symbols match the points in one panel and the lines in the other.
Add titles with title(..., font.main=1) after the plot call, rather than with main= inside it. R’s default title is bold, and a bold title competes with the data for attention. Keep the text short, usually a statistic or a single phrase, and use title('label', outer=TRUE) for a label spanning a multi-panel figure. For text annotations with text(), match the color to the element being labeled, use expression() or bquote() for math, and set adj=0 to left-align or adj=0.5 to center.
The Grey and RGB System
Scatterpoints and histogram fills use grey(0, alpha), varying only the transparency, while anything you want the reader to look at uses rgb() with an explicit alpha channel.
rgb(1, 0, 0, .8) |
Red |
Means, straight-line fits, key statistics |
rgb(0, 0, 1, .8) |
Blue |
SD bounds, secondary reference lines, constant fits |
rgb(0, .7, .7, .8) |
Teal |
LOESS fits |
rgb(0, 0, 0, .8) |
Black |
Interval bounds, and horizontal and vertical reference lines |
Avoid numeric codes such as col=2, which depend on the current palette, and names such as 'red', since neither form lets you set transparency. To compare groups, store one color per group in cols.
| 2 |
cols <- c(rgb(.8, 0, 0, .5), rgb(0, 0, .8, .5)) |
| 3 |
cols <- c(rgb(.8, 0, 0, .5), rgb(0, .8, 0, .5), rgb(0, 0, .8, .5)) |
| 4 or more |
cols <- hcl.colors(k, alpha=.45), where k is the number of groups |
With four or more groups, hcl.colors() spaces the colors evenly in a perceptual space, so no one group looks louder than the others.
Reference Card
border=NA and freq=FALSE for every histogram.
- Raw data in whatever figure you have, such as a rug under a histogram or a stripchart on a boxplot.
- More bins and no rug for histograms of resampled data.
- Interval bounds in black and, for null bootstrap or permutation distributions, the observed statistic in red, both
lwd=2.
- Null values, population values, and theoretical densities over a histogram in blue, with
lwd=1 and lty=2.
pch=16 and grey(0, alpha) for scatterplots, with alpha matched to \(n\).
- One color per model at
lwd=2: red for straight lines, blue for constants, and teal for LOESS, with line type for variants and type='o' for predictions at design points.
font.main=1 for every title.
rgb() with alpha for emphasis colors, never col=2 or 'red'.
par(mfrow=...) for multi-panel layouts, not layout(), unless panels differ in size.