R Color Theory: Choose Palettes, Use ColorBrewer, and Design for Colorblind Readers
Color in data visualization is not decoration — it encodes information. The right palette makes patterns visible instantly; the wrong one obscures them or misleads. In ggplot2, three palette families handle three fundamentally different data situations: sequential, diverging, and qualitative.
Introduction
Choosing colors for a chart feels like an aesthetic decision, but it is actually a data decision. When you color a heatmap by temperature, a continuous gradient from blue to red communicates direction and magnitude simultaneously. When you color countries on a choropleth by political party, you need distinct, unordered colors that don't imply any ranking between parties. When you mark values that deviate above and below a neutral midpoint (like growth vs. decline), you need two opposing colors that meet at a meaningful zero.
Each of those situations requires a different palette type. Using the wrong one — for example, a qualitative palette on continuous data — forces readers to mentally convert discrete color steps into a continuous quantity, adding cognitive load and risking misinterpretation.
In this tutorial you will learn:
- The three palette families and when each applies
- How to apply ColorBrewer palettes with
scale_color_brewer()andscale_fill_distiller() - How viridis palettes provide perceptually uniform, colorblind-safe color for continuous data
- How to specify custom colors with
scale_color_manual() - How to design charts that remain readable for colorblind readers
What Are the Three Types of Color Palettes?
Every color scale used in data visualization falls into one of three categories. Understanding which type your data needs is the most important color decision you will make.
Sequential palettes encode magnitude with a gradient from light (low) to dark (high). Use them for continuous data with a natural ordering and no meaningful midpoint — population density, temperature, price.
Diverging palettes encode deviation from a midpoint. Two contrasting hue families (e.g., blue and red) meet at a neutral center (white or beige). Use them when values can be meaningfully positive or negative, or above/below some reference — correlation coefficients, budget surplus/deficit, political lean.
Qualitative palettes use distinct, unordered hues to encode categorical membership. No hue should appear "more" or "less" than another. Use them for nominal categories — country, species, product type.
The output groups palettes into three rows: sequential (top), qualitative (middle), and diverging (bottom). Refer to this when deciding which family fits your data.
| Palette Type | Data Type | Example Variable | ggplot2 Function |
|---|---|---|---|
| Sequential | Continuous, one direction | Price, density, count | scale_fill_distiller(type="seq") |
| Diverging | Continuous, two directions | Correlation, surplus/deficit | scale_fill_distiller(type="div") |
| Qualitative | Categorical, unordered | Species, region, category | scale_color_brewer(type="qual") |
KEY INSIGHT: The most common palette mistake is using a qualitative palette (distinct colors) for continuous data, or a sequential palette for nominal categories. The first makes continuous variation look stepwise; the second implies ranking where none exists.
Try it: Run brewer.pal(n = 5, name = "Blues") to see the 5-color sequential blue palette. Then try brewer.pal(n = 5, name = "RdYlGn") for a 5-color diverging palette.
How Do You Apply ColorBrewer Palettes in ggplot2?
ColorBrewer palettes are built into ggplot2 via two scale functions:
scale_color_brewer()/scale_fill_brewer()— for discrete (categorical) variablesscale_color_distiller()/scale_fill_distiller()— for continuous variables (interpolates between palette colors)
Sequential palette on a heatmap:
Diverging palette — correlations that go both positive and negative:
Notice how the diverging palette immediately shows that variables positively correlated (red) vs. negatively correlated (blue) with others — information the sequential palette obscures.
Qualitative palette for categorical groups:
TIP: ColorBrewer's most colorblind-friendly qualitative palette is
"Set2"."Dark2"is also good for print. Avoid"Set1"when a colorblind reader matters — its red-green combination is problematic for the most common form of color blindness.
Try it: Change palette = "Set2" to palette = "Paired" in p_qual. How does the chart's readability change?
How Does the Viridis Palette Family Work?
Viridis is a family of perceptually uniform color scales — meaning equal steps in data value correspond to equal perceived steps in color, across the full range. This is a property most palettes (including many ColorBrewer ones) don't have.
Viridis was also designed from the start to be:
- Readable in grayscale (for black-and-white printing)
- Distinguishable for the three main types of color blindness (deuteranopia, protanopia, tritanopia)
- Beautiful
The viridis family includes five palettes: viridis (purple-blue-yellow), magma (black-red-yellow), plasma (purple-orange-yellow), inferno (black-red-yellow, higher contrast), and cividis (optimized for deuteranopia).
For discrete variables, use scale_color_viridis_d() or scale_fill_viridis_d():
KEY INSIGHT:
viridisis the safest default for continuous color scales in published work. It looks good on screen, prints in grayscale without losing information, and is accessible to colorblind readers — properties that most visually appealing palettes (like rainbow) fail on all three counts.
Try it: Change option = "plasma" to option = "magma" in p_vir. Then try option = "cividis". Which looks best for the diamond density data?
How Do You Specify Custom Colors?
When built-in palettes don't fit your brand or data story, you can specify exact colors with scale_color_manual() or scale_fill_manual().
R accepts colors as hex codes ("#2166ac"), named colors ("steelblue", "tomato"), or RGB values via rgb(0.1, 0.5, 0.8). To see all named colors available in R, run colors() — there are 657 of them.
TIP: When picking custom colors, check them at https://colorbrewer2.org or use the
prismaticpackage'scheck_color_blindness()function to simulate how your palette appears under different types of color vision deficiency.
Try it: Replace the three hex codes in p_manual with named R colors ("navy", "coral", "forestgreen"). Do the resulting colors look as distinct?
How Do You Design Charts for Colorblind Readers?
Approximately 8% of men and 0.5% of women have some form of color vision deficiency. The most common form is deuteranopia (red-green), where red and green are indistinguishable. This means any chart that encodes meaning with red vs. green alone is unreadable for ~1 in 12 male readers.
Three practical strategies:
Strategy 1: Use colorblind-safe palettes. Viridis, ColorBrewer's "Set2", "Dark2", and "Okabe-Ito" are safe by design.
Strategy 2: Dual-encode with shape or linetype. Map the same variable to both color and shape — even in grayscale or with color blindness, the shapes are distinguishable.
The three hex colors above (#0072B2, #E69F00, #009E73) are from the Okabe-Ito palette — specifically designed for colorblind accessibility by statistician Masataka Okabe and color scientist Kei Ito.
Strategy 3: Simulate colorblind vision. The colorspace package includes deutan(), protan(), and tritan() functions to simulate how your color choices appear under different deficiencies.
WARNING: Never rely on red vs. green as the only differentiator in a chart. This combination fails for deuteranopia (the most common form of color blindness) and also looks poor in grayscale printing. If you must use red and green, ensure they also differ in lightness or saturation.
Try it: Change the scale_color_manual colors in p_cb to use red ("#CC0000") and green ("#009900") for the f and r groups. Can you tell them apart if you squint — simulating a colorblind reader?
Common Mistakes and How to Fix Them
Mistake 1: Using rainbow or jet palettes for continuous data
❌ The rainbow() palette has no perceptual uniformity — regions of similar data values appear very different in color, and some regions (yellow-green) look similar despite very different values.
✅ Use scale_fill_viridis_c() for continuous data. Viridis is perceptually uniform and colorblind-safe by design.
Mistake 2: Wrong palette type for data type
❌ Applying a qualitative palette (scale_fill_brewer(palette = "Set1")) to a continuous variable. Each color implies a distinct category, not a smooth gradient.
✅ Match palette type to data: sequential/diverging for continuous, qualitative for categorical.
Mistake 3: Applying a diverging palette without centering at zero
❌ Using scale_fill_distiller(palette = "RdBu") without setting limits = c(-max, max) causes the midpoint (white) to appear at the data mean, not at zero — misrepresenting the zero crossing.
✅ Always set symmetric limits around zero for diverging scales: limits = c(-1, 1) for correlations, limits = c(-max_abs, max_abs) for other data.
Mistake 4: Using color as the only channel for critical information
❌ Differentiating two groups only by red vs. green — invisible to colorblind readers and lost in grayscale.
✅ Dual-encode: map the same variable to both color and shape (for scatter plots) or both color and linetype (for line charts).
Mistake 5: Not specifying direction in scale_fill_distiller
❌ scale_fill_distiller(palette = "Blues") uses the palette in the default direction, which puts the darkest color at the low end. For many heatmaps, you want dark = high.
✅ Add direction = 1 to reverse to light-low, dark-high: scale_fill_distiller(palette = "Blues", direction = 1).
Practice Exercises
Exercise 1: Match palette to data
The airquality dataset has daily temperature (Temp) and ozone (Ozone) measurements. Create three charts:
- A scatter plot of
WindvsTempcolored byMonth(categorical) — choose an appropriate qualitative palette - A scatter plot of
WindvsTempcolored byOzone(continuous, one direction) — choose a sequential palette - Create a new column
Ozone_diff = Ozone - mean(Ozone, na.rm=TRUE)and color by it — choose a diverging palette
Exercise 2: Make a chart colorblind-accessible
Take the following basic scatter plot and make it accessible:
Improve it by: (1) switching to a colorblind-safe palette, (2) adding shape = fl as a second encoding, and (3) reducing to the 3 most common fuel types for clarity.
Complete Example
This example builds a polished heatmap of monthly average temperature from the airquality dataset, using a perceptually uniform sequential palette:
Summary
| Situation | Palette Type | ggplot2 Function | Recommended Palette |
|---|---|---|---|
| Continuous, one direction | Sequential | scale_fill_distiller(type="seq") or scale_fill_viridis_c() |
"Blues", "YlOrRd", "viridis", "plasma" |
| Continuous, above/below zero | Diverging | scale_fill_distiller(type="div") |
"RdBu", "RdYlGn", "BrBG" |
| Categorical, unordered | Qualitative | scale_color_brewer(type="qual") |
"Set2", "Dark2", "Okabe-Ito" |
| Any — colorblind-safe | Any | scale_*_viridis_*() |
"viridis", "cividis", "plasma" |
| Custom colors | Any | scale_*_manual(values = ...) |
Hex codes or R named colors |
Key rules:
- Match palette type to data type — this is the most important decision
- Use viridis as your default for continuous scales — it's safe for colorblind, grayscale, and screen
- For diverging scales, always center the midpoint at zero with symmetric limits
- Dual-encode critical information with shape or linetype — never rely on color alone
FAQ
What is the difference between scale_color_brewer() and scale_fill_brewer()?
scale_color_brewer() applies to the color aesthetic (point and line outlines). scale_fill_brewer() applies to the fill aesthetic (bar and polygon interiors). For geom_point() with solid shapes, use color. For geom_col(), geom_histogram(), or geom_tile(), use fill.
Can I see all ColorBrewer palettes in one view?
Yes: RColorBrewer::display.brewer.all() shows all palettes grouped by type. Add colorblindFriendly = TRUE to filter to the 8 palettes that are safe for all colorblind types.
How many colors can I get from a ColorBrewer palette?
It depends on the palette. Most qualitative palettes support 3–8 or 3–12 colors. Check brewer.pal.info for each palette's maximum. If you need more colors, switch to viridis or use scales::hue_pal() which generates arbitrary numbers of distinct hues.
What is the Okabe-Ito palette and how do I use it in ggplot2?
Okabe-Ito is a colorblind-safe 8-color qualitative palette. In ggplot2 3.5+, it's available as scale_color_okabeito() (via the ggokabeito package). Manually: c("#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7", "#000000").
Is the rainbow/jet palette ever appropriate?
Almost never in scientific or analytical contexts. Rainbow has no perceptual uniformity, fails colorblind readers, and looks wrong in grayscale. The only case where it might be acceptable is decorative visualization where accurate data reading isn't the goal. For all analytical work, use viridis, ColorBrewer, or another principled alternative.
References
- Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis, Chapter 11: Colour Scales. Springer. https://ggplot2-book.org/scale-colour
- ColorBrewer — Cynthia Brewer, Mark Harrower. https://colorbrewer2.org/
- Smith, N. & van der Walt, S. (2015). A Better Default Colormap for Matplotlib (viridis). SciPy 2015.
- Okabe, M. & Ito, K. (2008). Color Universal Design. https://jfly.uni-koeln.de/color/
- Wilke, C. O. (2019). Fundamentals of Data Visualization, Chapter 4: Color Scales. https://clauswilke.com/dataviz/color-basics.html
- ggplot2 reference —
scale_color_brewer(). https://ggplot2.tidyverse.org/reference/scale_brewer.html - ggplot2 reference —
scale_color_viridis_*(). https://ggplot2.tidyverse.org/reference/scale_viridis.html
What's Next?
- ggplot2 Scales — control axis formatting, breaks, labels, and color scales with the full
scale_*()system. - ggplot2 Distribution Charts — choose histograms, density plots, boxplots, and violin plots with colorblind-safe palettes applied throughout.
- ggplot2 Scatter Plots — apply the color mapping strategies from this tutorial to real exploratory analysis.