Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

1Objectives

By the end of this lesson you will be able to:

  1. Choose the right graph for a variable (histogram/boxplot for numerical, bar chart for categorical) — a review and extension of Lesson 7.

  2. Add clear titles and axis labels to a gf_ plot so it stands on its own.

  3. Build a graph that stays readable for a colorblind reader and in grayscale, using an Okabe–Ito-safe palette instead of relying on color alone.

  4. Write one honest, meaningful sentence describing what a graph shows (its “alt text”) — the same standard this book itself is held to.

Every plot in this lesson uses the running coffee_wait_sim.csv data from Lesson 6 and the built-in KidsFeet dataset from Lessons 6–7. Every number and every picture below was produced by one committed script, data/make_L10_figures.R — run it yourself (Rscript data/make_L10_figures.R from the r-help/ folder) to reproduce everything on this page exactly.

library(mosaic)
library(BSDA)
set.seed(1209)
coffee <- read.csv("data/coffee_wait_sim.csv")
data(KidsFeet)

2Quick review: picking the right graph

Lesson 7 introduces the gf_ (“ggformula”) plotting family, using the exact same y ~ x formula grammar as favstats() and tally() (Lesson 6). The choice of which gf_ function almost always comes down to one question — what kind of variable are you plotting?

QuestionPlotFunction
What’s the shape of one numerical variable?Histogramgf_histogram(~x, data=)
How does one numerical variable compare across groups?Boxplotgf_boxplot(y ~ g, data=)
How many cases fall in each category?Bar chartgf_bar(~x, data=)

This lesson does not introduce a new plot type — it takes plots you already know how to build and makes them readable by everyone: a classmate with color-vision deficiency, a reader who printed your homework in grayscale, or a screen-reader user who cannot see the picture at all. A plot with no title, no axis labels, and only-by-color groups might make sense to you, sitting right next to the code that built it — but a graph has to stand on its own.

3Step 1: a title and axis labels

Compare gf_boxplot(wait_minutes ~ day_type, data = coffee) with nothing added, to the same plot with three extra pieces piped on:

gf_boxplot(wait_minutes ~ day_type, data = coffee, fill = ~day_type) %>%
  gf_labs(title = "Coffee-cart wait times, weekday vs. weekend",
          x = "Day type", y = "Wait time (minutes)",
          fill = "Day type")

gf_labs() (short for “labels”) takes named inputs — title, x, y, and even fill (which relabels the legend) — the same named-input pattern you saw with round(x, digits = 2) back in Lesson 4. Without it, ggplot2 (the plotting engine underneath every gf_ function) falls back to raw column names like wait_minutes and day_type as the axis text — technically correct, but not written in plain English for whoever reads it next.

fill = ~day_type also colors each box by group — but by default, ggplot2 picks colors that are not guaranteed to be tellable apart by a colorblind reader. That’s Step 2.

4Step 2: a colorblind-safe palette

This book’s fix is the Okabe–Ito palette: eight specific colors, chosen by vision researchers, that stay distinguishable under the most common forms of color blindness (and hold up in grayscale far better than default software colors do).

okabe_ito <- c("#E69F00", "#56B4E9", "#009E73", "#F0E442",
               "#0072B2", "#D55E00", "#CC79A7", "#000000")

Each entry is a hex code — a six-digit code standing for one exact color (#0072B2 is a strong blue, #E69F00 a warm orange, and so on). You apply the palette to a plot’s fill or color with scale_fill_manual() or scale_color_manual(), wrapped inside gf_refine() (a general-purpose “add one more ggplot2 layer” function):

gf_boxplot(wait_minutes ~ day_type, data = coffee, fill = ~day_type) %>%
  gf_refine(scale_fill_manual(values = okabe_ito[c(5, 1)])) %>%
  gf_labs(title = "Coffee-cart wait times, weekday vs. weekend",
          x = "Day type", y = "Wait time (minutes)",
          fill = "Day type")

okabe_ito[c(5, 1)] pulls the 5th and 1st colors from the palette (blue and orange) — one per group. Here is the real plot that code produces:

Side-by-side boxplots of coffee-cart wait times for weekday and weekend customers. The weekday box is higher, with a median around 4.5 minutes and a range from about 2.3 to 8.1 minutes. The weekend box is lower, with a median around 2.7 minutes and one outlier point at 6.2 minutes.

Figure 1:Boxplots of coffee-cart wait time by day type, colorblind-safe palette, matching the favstats() table below.

favstats(wait_minutes ~ day_type, data = coffee)
  day_type min    Q1 median  Q3 max     mean       sd  n missing
1  Weekday 2.3 3.675   4.45 5.9 8.1 4.716667 1.752833 12       0
2  Weekend 1.4 2.100   2.70 3.1 6.2 2.925000 1.521043  8       0

Notice the picture and the table tell the same story — a median wait around 4.45 minutes on weekdays versus 2.70 on weekends — because a good graph is always a picture of numbers you could also print in a table. Neither box’s color is doing work the x-axis labels (Weekday, Weekend) aren’t already doing — the color is a helpful extra cue, never the only one.

5A styled bar chart, for comparison

The same two ingredients — gf_labs() for text, gf_refine(scale_fill_manual(...)) for color — apply identically to a categorical variable’s bar chart:

gf_bar(~ day_type, data = coffee, fill = ~day_type) %>%
  gf_refine(scale_fill_manual(values = okabe_ito[c(5, 1)])) %>%
  gf_labs(title = "Customers timed, by day type",
          x = "Day type", y = "Number of customers", fill = "Day type")
Bar chart of 20 coffee-cart customers by day type. The Weekday bar is taller, at 12 customers; the Weekend bar is shorter, at 8 customers.

Figure 2:Bar chart of customers timed, by day type, matching the tally() counts below.

tally(~ day_type, data = coffee)
day_type
Weekday Weekend 
     12       8 

Bar heights (12, 8) match the tally exactly — a bar chart is simply a picture of a frequency table, the same relationship gf_histogram() and favstats() have.

6Step 3: never color alone — add shape, linetype, or a facet

A palette swap fixes which colors you use, but a reader with total color blindness, or reading a black-and-white printout, still can’t use color at all. The fix: back up color with a second, non-color cue — shape, linetype, direct text labels, or separate panels (faceting, below).

gf_point(width ~ length, data = KidsFeet, color = ~sex, shape = ~sex, size = 2.5) %>%
  gf_refine(scale_color_manual(values = okabe_ito[c(5, 6)])) %>%
  gf_labs(title = "Foot width vs. length, by sex",
          x = "Foot length (cm)", y = "Foot width (cm)",
          color = "Sex (B/G)", shape = "Sex (B/G)")
Scatterplot of foot width against foot length for 39 kids, boys shown as blue circles and girls as orange triangles. Both groups overlap heavily across the whole range, with a loose positive trend — wider feet tend to go with longer feet — and no clear separation between boys and girls.

Figure 3:Scatterplot of foot width vs. length from KidsFeet, grouped by sex with both color and shape.

shape = ~sex (added right alongside color = ~sex) draws boys as circles and girls as triangles, in addition to two different colors. Now a reader who cannot distinguish blue from orange — or a copy printed on a black-and-white printer — can still read the groups apart by shape alone. This is exactly what the WCAG 2.1 AA rule from Step 2 means by “never distinguish groups by hue alone.”

7Step 4: faceting — one panel per group

Faceting splits a plot into a small grid of panels, one per group, instead of overlaying groups with color at all. It is often the single most accessible way to compare more than two groups:

gf_histogram(~ length, data = KidsFeet, binwidth = 1,
             fill = "#009E73", color = "white") %>%
  gf_facet_wrap(~ sex) %>%
  gf_labs(title = "Foot length, by sex (faceted)",
          x = "Foot length (cm)", y = "Number of kids")
Two histograms of foot length side by side, one panel labeled B and one labeled G. The B panel peaks at 7 kids between 24 and 25 centimeters. The G panel has a taller, narrower peak of 8 kids between 23 and 24 centimeters, with smaller counts spread from 22 to 27 centimeters.

Figure 4:Foot length faceted by sex — one histogram panel per group, no color grouping needed.

gf_facet_wrap(~ sex) is simply piped onto an ordinary histogram — faceting is an add-on step, not a different plot type, and it works after gf_boxplot, gf_bar, or gf_point exactly the same way. Each panel’s label (B, G) is real text, not a color-coded legend entry, so the group identity never depends on being able to see color at all.

8Saving a plot to a file

Every figure in this lesson was saved from R with ggsave(), so it could be embedded in this page. You’ll use the exact same function to save a plot for a lab write-up or homework submission:

my_plot <- gf_boxplot(wait_minutes ~ day_type, data = coffee, fill = ~day_type) %>%
  gf_refine(scale_fill_manual(values = okabe_ito[c(5, 1)])) %>%
  gf_labs(title = "Coffee-cart wait times, weekday vs. weekend",
          x = "Day type", y = "Wait time (minutes)", fill = "Day type")

ggsave("images/L10-box-wait-daytype.png", my_plot, width = 6.5, height = 4.2, dpi = 150)

Two things make this work. First, %>% (piping) only builds a plot object — nothing is saved to a file until you actually assign the whole pipe to a name (my_plot <-) and hand that object to ggsave(). Second, ggsave()'s first input is the file path to write, using the same kind of relative path you’ve used since Lesson 6: "images/L10-box-wait-daytype.png" saves inside an images folder sitting right next to your script or notebook. On CSUB JupyterHub, that new file shows up in the file browser panel on the left, where you can right-click it to download.

9Writing alt text: the same standard as this book

Alt text (short for “alternative text”) is a one- or two-sentence written description of what a figure shows, attached to the image so a screen reader can read it aloud, and so it still communicates something if the image itself never loads. Every {figure} block in this lesson carries one — look back at any image above and you’ll find an :alt: line right next to it.

Good alt text is not “a boxplot” or “a graph of the data” — that tells a reader nothing they couldn’t already guess. It answers one specific question:

10Summary

11Check your understanding

  1. You build gf_bar(~ major, data = students) with no other code. List two specific things this lesson says to add before it’s ready to show someone else, and the exact R that adds each one.

  2. Why is fill = ~group alone not enough to meet this book’s accessibility standard, even after you apply the Okabe–Ito palette?

  3. Give one example of a non-color cue you could add to a scatterplot with two groups, and the R argument that adds it.

  4. Write real alt text (one or two honest sentences) for the bar chart of day-type counts earlier in this lesson, without re-reading its :alt: text first.