Implementing accessibility

Lecture 11

Dr. Benjamin Soltoff

Cornell University
INFO 3312/5312 - Fall 2026

October 8, 2026

Announcements

Announcements

  • Project 01 submitted
  • Sign up for mock interview sessions starting after Fall Break

Learning objectives

  • Identify why accessibility is important to data communication
  • Understand the principles of accessible design
  • Design accessible data visualizations
  • Write alternative text for charts

Visualization critique

  • What is the story?
  • How would you describe the chart to someone who could not see it?

Do you remember?

Flatten the curve

Accessible COVID-19 statistics tracker

Accessibility and screen readers

Alternative text

It is read by screen readers in place of images allowing the content and function of the image to be accessible to those with visual or certain cognitive disabilities.

It is displayed in place of the image in browsers if the image file is not loaded or when the user has chosen not to view images.

It provides a semantic meaning and description to images which can be read by search engines or be used to later determine the content of the image from page context alone.

Alt text in the wild

Alt and surrounding text

"CHART TYPE of TYPE OF DATA where REASON FOR INCLUDING CHART`

+ Link to data source somewhere in the text
  • CHART TYPE: It’s helpful for people with partial sight to know what chart type it is and gives context for understanding the rest of the visual.
  • TYPE OF DATA: What data is included in the chart? The x and y axis labels may help you figure this out.
  • REASON FOR INCLUDING CHART: Think about why you’re including this visual. What does it show that’s meaningful. There should be a point to every visual and you should tell people what to look for.
  • Link to data source: Don’t include this in your alt text, but it should be included somewhere in the surrounding text.

Data

  • Registered nurses by state and year
  • Number of nurses, salaries, employment
  • Source: TidyTuesday
nurses <- read_csv("data/nurses.csv") |> janitor::clean_names()
glimpse(nurses)
Rows: 1,242
Columns: 22
$ state                                        <chr> "Alabama", "Alaska", "Arizona", "Arkansas", "…
$ year                                         <dbl> 2020, 2020, 2020, 2020, 2020, 2020, 2020, 202…
$ total_employed_rn                            <dbl> 48850, 6240, 55520, 25300, 307060, 52330, 334…
$ employed_standard_error_percent              <dbl> 2.9, 13.0, 3.7, 4.2, 2.0, 2.8, 6.5, 11.4, 1.2…
$ hourly_wage_avg                              <dbl> 28.96, 45.81, 38.64, 30.60, 57.96, 37.43, 40.…
$ hourly_wage_median                           <dbl> 28.19, 45.23, 37.98, 29.97, 56.93, 36.78, 39.…
$ annual_salary_avg                            <dbl> 60230, 95270, 80380, 63640, 120560, 77860, 84…
$ annual_salary_median                         <dbl> 58630, 94070, 79010, 62330, 118410, 76500, 82…
$ wage_salary_standard_error_percent           <dbl> 0.8, 1.4, 0.9, 1.4, 1.0, 0.7, 1.0, 2.5, 1.5, …
$ hourly_10th_percentile                       <dbl> 20.75, 31.50, 27.66, 21.47, 36.62, 26.84, 29.…
$ hourly_25th_percentile                       <dbl> 23.73, 36.94, 32.58, 25.71, 45.18, 31.05, 34.…
$ hourly_75th_percentile                       <dbl> 33.15, 53.31, 44.67, 35.40, 71.07, 43.47, 47.…
$ hourly_90th_percentile                       <dbl> 38.67, 60.70, 50.14, 39.65, 83.35, 50.03, 54.…
$ annual_10th_percentile                       <dbl> 43150, 65530, 57530, 44660, 76180, 55820, 605…
$ annual_25th_percentile                       <dbl> 49360, 76830, 67760, 53490, 93970, 64580, 709…
$ annual_75th_percentile                       <dbl> 68960, 110890, 92920, 73630, 147830, 90410, 9…
$ annual_90th_percentile                       <dbl> 80420, 126260, 104290, 82480, 173370, 104070,…
$ location_quotient                            <dbl> 1.20, 0.98, 0.91, 1.00, 0.87, 0.95, 1.01, 1.2…
$ total_employed_national_aggregate            <dbl> 140019790, 140019790, 140019790, 140019790, 1…
$ total_employed_healthcare_national_aggregate <dbl> 8632190, 8632190, 8632190, 8632190, 8632190, …
$ total_employed_healthcare_state_aggregate    <dbl> 128600, 17730, 171010, 80410, 844740, 144490,…
$ yearly_total_employed_state_aggregate        <dbl> 1903210, 296300, 2835110, 1177860, 16430660, …

Bar chart

The figure is a bar chart titled 'Total employed Registered Nurses' that displays the numbers of registered nurses in three states (California, New York, and North Carolina) over a 20 year period, with data recorded in three time points (2000, 2010, and 2020). In each state, the numbers of registered nurses increase over time. The following numbers are all approximate. California started off with 200K registered nurses in 2000, 240K in 2010, and 300K in 2020. New York had 150K in 2000, 160K in 2010, and 170K in 2020. Finally North Carolina had 60K in 2000, 90K in 2010, and 100K in 2020.

  • Provide the title and axis labels
  • Briefly describe the chart and give a summary of any trends it displays
  • Convert bar charts to accessible tables or lists
  • Avoid describing visual attributes of the bars (e.g., dark blue, gray, yellow) unless there’s an explicit need to do so

Developing the alt text

  • Total employed registered nurses in three states over time.
  • Total employed registered nurses in California, New York, and North Carolina, in 2000, 2010, and 2020.
  • A bar chart of total employed registered nurses in California, New York, and North Carolina, in 2000, 2010, and 2020, showing increasing numbers of nurses over time.
  • The figure is a bar chart titled ‘Total employed Registered Nurses’ that displays the numbers of registered nurses in three states (California, New York, and North Carolina) over a 20 year period, with data recorded in three time points (2000, 2010, and 2020). In each state, the numbers of registered nurses increase over time. The following numbers are all approximate. California started off with 200K registered nurses in 2000, 240K in 2010, and 300K in 2020. New York had 150K in 2000, 160K in 2010, and 170K in 2020. Finally North Carolina had 60K in 2000, 90K in 2010, and 100K in 2020.

Can we outsource to LLMs?

Generated using Claude Sonnet 5

Title: Total Employed Registered Nurses

Axes: State (horizontal) versus Number of Registered Nurses (vertical)

Description: This grouped column chart compares the number of employed registered nurses across three states—California, New York, and North Carolina—for the years 2000, 2010, and 2020.

California shows the highest nurse employment throughout, rising from approximately 205,000 in 2000 to about 235,000 in 2010, then increasing sharply to roughly 307,000 in 2020.

New York shows steady growth, starting at approximately 163,000 in 2000, rising to about 172,000 in 2010, and reaching roughly 178,000 in 2020.

North Carolina has the lowest totals among the three states, beginning at approximately 60,000 in 2000, increasing to about 89,000 in 2010, and reaching roughly 98,000 in 2020.

Across all three states, the number of registered nurses increased consistently from 2000 to 2020, with California showing the most substantial growth in absolute numbers.

Adding alt text to plots

  • Short:
```{r}
#| fig-alt: Alt text goes here.

# code for plot goes here
```
  • Longer:
```{r}
#| fig-alt: |
#|   Longer alt text goes here. Make sure to add line breaks ~roughly
#|   80 characters.

# code for plot goes here
```

Making captions accessible

```{r}
#| fig-cap: Total employed Registered Nurses

nurses_subset |>
  filter(year %in% c(2000, 2010, 2020)) |>
  ggplot(aes(x = state, y = total_employed_rn, fill = factor(year))) +
  geom_col(position = "dodge") +
  scale_fill_viridis_d(option = "E") +
  scale_y_continuous(labels = label_number(scale_cut = cut_short_scale())) +
  labs(x = "State", y = "Number of Registered Nurses", fill = "Year") +
  theme(
    legend.background = element_rect(fill = "white", color = "white"),
    legend.position = c(0.85, 0.75)
  )
```

Total employed Registered Nurses

Developing the alt table

nurses_subset |>
  filter(year %in% c(2000, 2010, 2020)) |>
  arrange(year) |>
  select(state, year, total_employed_rn) |>
  pivot_wider(
    names_from = year,
    values_from = total_employed_rn
  ) |>
  gt() |>
  fmt_number(
    columns = -state,
    decimals = 0
  ) |>
  cols_label(state = "State") |>
  tab_spanner(
    label = "Total employed registered nurses",
    columns = everything()
  ) |>
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_column_spanners()
  ) |>
  tab_options(
    table.width = "90%",
    table.font.size = "90%"
  )
Total employed registered nurses
State 2000 2010 2020
California 203,390 240,030 307,060
New York 159,670 169,710 178,550
North Carolina 60,940 90,730 99,110

Accessibility and colors

The default {ggplot2} color scale

Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.

Warning in geom_text(data = data.frame(text, x, y), aes(x = x, y = y, label = text), : Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.

Color scales

Use colorblind friendly color scales (e.g., Okabe Ito, {viridis})

nurses_subset |>
  ggplot(aes(x = year, y = hourly_wage_median, color = state)) +
  geom_line(size = 2) +
  scale_color_colorblind(black = FALSE) +
  scale_y_continuous(labels = label_currency()) +
  labs(
    x = "Year",
    y = "Median hourly wage",
    color = "State",
    title = "Median hourly wage of Registered Nurses"
  ) +
  theme(
    legend.position = c(0.15, 0.75),
    legend.background = element_rect(fill = "white", color = "white")
  )

Testing for colorblind friendliness

  • Best way to test is with users (or collaborators) who have these color deficiencies

  • colorblindr::cvd_grid()

  • Simulation software also helps, e.g. Sim Daltonism for Mac and PC

Color contrast

  • Background and foreground text should have sufficient contrast to be distinguishable by users with different vision

  • App for checking color contrast checking: Color Contrast Analyser

  • An R package for checking for color contrast: {savonliquide}

Color contrast

check_contrast("#000000", "#FFFFFF")

* The Contrast Ratio is 21

* The result for the AA check is : PASS

* The result for the AALarge check is : PASS

* The result for the AAA check is : PASS

* The result for the AAALarge check is : PASS

check_contrast("#000000", "#424242")

* The Contrast Ratio is 2.08

* 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

check_contrast("#ff0000", "#ffff00")

* The Contrast Ratio is 3.72

* The result for the AA check is : FAIL

* The result for the AALarge check is : PASS

* The result for the AAA check is : FAIL

* The result for the AAALarge check is : FAIL

Double encoding

Use multiple channels where possible

Warning in geom_text(data = data.frame(text, x, y), aes(x = x, y = y, label = text), : Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.

Use direct labeling

  • Prefer direct labeling where color is used to display information over a legend

  • Quicker to read

  • Ensures graph can be understood without reliance on color

Without direct labeling

Warning in geom_text(data = data.frame(text, x, y), aes(x = x, y = y, label = text), : Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.

With direct labeling

Warning in geom_text(data = data.frame(text, x, y), aes(x = x, y = y, label = text), : Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.

Use whitespace or pattern to separate elements

  • Separate elements with whitespace or pattern

  • Allows for distinguishing between data without entirely relying on contrast between colors

Without whitespace/pattern

Warning in geom_text(data = data.frame(text, x, y), aes(x = x, y = y, label = text), : Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.

With whitespace

Warning in geom_col(position = "fill", color = "white", size = 20): Ignoring unknown parameters:
`size`
Warning in geom_text(data = data.frame(text, x, y), aes(x = x, y = y, label = text), : Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.

With pattern via {ggpattern}

Warning in geom_text(data = data.frame(text, x, y), aes(x = x, y = y, label = text), : Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.
Ignoring empty aesthetic: `family`.

Accessibility and fonts

Accessibility and fonts

  • Use a font that has been tested for accessibility (e.g., Atkinson Hyperlegible, Lexend, Public Sans)

  • Keep plot labels and annotations similarly sized as the rest of your text

    theme_minimal(base_size = 16)

Accessibility and fonts

nurses_subset |>
  ggplot(aes(x = year, y = hourly_wage_median, color = state)) +
  geom_line(size = 2) +
  scale_color_colorblind(black = FALSE) +
  scale_y_continuous(labels = label_currency()) +
  labs(
    x = "Year",
    y = "Median hourly wage",
    color = "State",
    title = "Median hourly wage of Registered Nurses"
  ) +
  theme_minimal(
    base_size = 16,
    base_family = "Atkinson Hyperlegible"
  )

Application exercise

Application exercise

Instructions

  • Write alternative text for the provided chart
  • Identify at least three design improvements that could be made to the chart to make it more accessible

Additional resources

Additional resources

Wrap-up

Wrap-up

  • When you design for accessibility, you benefit everyone
  • Use alternative text for images and figures
  • Use colorblind-friendly palettes
  • Use whitespace or pattern to separate elements
  • Use a font that has been tested for accessibility

Acknowledgements