Data Visualization
Designing clear graphics.
A default ggplot2 graphic is a great start for exploratory data analysis. It quickly shows you distributions, groupings, and trends. However, default plots are rarely optimized for communication.
When you present a graphic to an audience, your goal is to minimize cognitive load—the amount of mental effort required by the reader to decode your chart. Every redundant label, unnatural text rotation, unnecessary gridline, or poorly chosen color adds friction.
In this tutorial, we will take a standard plot using the penguins dataset and refine it step-by-step. By the end, you will understand how small, deliberate design choices dramatically improve clarity and impact.
The Starting Point: The Raw Default Plot
Let’s start with a common task: comparing the mean body mass across the three penguin species (Adelie, Chinstrap, and Gentoo), broken down by sex.
Here is how most people write the initial code:
The graphic looks okay but there are a handful of details that need to be fixed:
Orientation Friction: The vertical bars force the reader to evaluate vertical heights across separated groups.
Default Gray Background & Gridlines: The heavy gray panel competes with the data for attention.
Cryptic/Raw Variable Names:
species,mean_mass, andsexare internal data names, not publication-ready labels.Redundant Units:
mean_massgives numbers like 3700, but omits the units (grams vs. kilograms).
Principle 1: Reorient for Readability & Perception
Human perception evaluates horizontal alignment and width with slightly higher precision and comfort than vertical bar heights—especially when category labels are long.
Furthermore, vertical bar charts often lead to rotated x-axis text (e.g., angled at 45 degrees), which significantly slows reading speed. Flipping the axes solves both problems instantly.
Principle 2: Intentional Color & Dual-Encoding
The default "ggplot2" color palette uses evenly spaced hues around the color wheel, which lack natural hierarchy.
Dual-encoding means representing the same data attribute using two distinct visual aesthetics (e.g., using both y-position and color hue to distinguish species, or using line type and marker shape). Dual-encoding strengthens pattern recognition and makes plots accessible to colorblind readers or grayscale printing.
Functional Palette Selection
Qualitative: For unordered categories (e.g., species, island).
Sequential: For low-to-high quantities (e.g., body mass, temperature).
Diverging: For deviations from a center point (e.g., profit/loss, anomaly vs. baseline).
Below we use hexadecimal notation to specify better color hues:
Now that we have colors for female and male values we can update the fill colors of the bars in our graphic. This is where the family of scale_...() functions comes handy. In the code below we use scale_fill_manual() which allows us to color-code the fill attribute with our manual selection of colors.
Principle 3: Maximize Data-Ink Ratio
Edward Tufte introduced the Data-Ink Ratio: the proportion of a graphic’s ink devoted to the non-redundant display of data information.
\[\text{Data-Ink Ratio} = \frac{\text{Data-Ink}}{\text{Total Ink Used to Print the Graphic}}\]
The goal is to maximize this ratio without removing context necessary for comprehension. To accomplish this we can:
Remove dark gray backgrounds (
theme_minimal()ortheme_classic()).Remove major vertical gridlines when numerical value labels are direct, or soft light-gray horizontal lines if axis values remain.
Remove redundant axis titles if the chart title or axis tick units make them self-explanatory (e.g., if tick marks say
"Adelie, Chinstrap, Gentoo", you don’t need an axis title saying"Species").
Principle 4: Direct Labeling
We can add direct value labels inside or beside the bars, and remove the x-axis entirely! How can we do this? With geom_text() and the following arguments:
position: position adjustmenthjust: horizontal justificationsize: size of textfontface: type of font face
Principle 5: Adjusting Axis Scales
In the preceding graphic, the x-axis scale needs some adjustment so that it ranges from 0 to a number beyond 4000. With some trial and error we chose 6600 as the upper limit.
To update the scale of the axis we use another scale_...() function: in this case we need to call scale_x_continuous() because the x-axis comes from mapping a continuous variable (i.e. mean_mass)
Principle 6: Labels for Clarity
FInally, we just need to add a descriptive title, and optionally a subtitle.