Fixing Unequal Radii and Wrong Proportions in ggplot2 Pie Charts
Creating a pie chart in R using ggplot2 is notorious for catching analysts off guard. Unlike dedicated plotting libraries, ggplot2 does not provide a native geom_pie() function. Instead, standard practice is to transform a stacked bar chart into polar coordinates using coord_polar(). However, missing a key mapping parameter often causes odd visual glitches, such as slices with mismatched radii (creating a coxcomb or rose plot) or weirdly sized angles with empty gaps.
The Problem: Why Slices Have Different Radii
Let's look at the two common attempts that lead to distorted pie charts:
Attempt 1: Mapping the Category to the y-Axis
df %>%
ggplot(mapping = aes(y = confidence_class, fill = confidence_class)) +
geom_bar() +
coord_polar(theta = "y")
In this code, y = confidence_class maps the categorical variable along the y-axis, and geom_bar() computes counts along the x-axis. When transformed by coord_polar(theta = "y"), the angle (theta) is controlled by the categories (giving each category an equal angular segment), while the radius (the x-axis) represents the count. This produces a Florence Nightingale / Coxcomb chart with differing radii rather than a conventional pie chart.
Attempt 2: Omitting stat = "identity" on Aggregated Data
signature_summary_2 %>%
ggplot(mapping = aes(y = n, fill = confidence_class)) +
geom_bar() +
coord_polar(theta = "y")
When passing pre-summarized data into geom_bar() without setting stat = "identity" (or using geom_col()), geom_bar() uses stat = "count" by default. It ends up counting the rows (which is 1 for each class) instead of summing the values in column n, leaving blank segments and identical angles.
The Solution: The Stacked Single-Bar Technique
To produce a classic pie chart in ggplot2, follow these core principles:
- Fix the X-axis: Map
x = ""(or any constant value likex = 1) to force all slices into a single stacked bar. - Map the values to Y: Map your counts or values to
y. - Use
geom_col(): When working with summarized counts, usegeom_col()(which defaults tostat = "identity"). If starting with raw rows, mapaes(x = "", fill = confidence_class)and letgeom_bar()tally the totals automatically. - Project to polar coordinates: Apply
coord_polar(theta = "y")to curl the single bar into a full 360-degree circle.
Method 1: Using Summarized Data (Recommended)
library(dplyr)
library(ggplot2)
# Sample data
df <- data.frame(
confidence_class = c("Low", "Low", "Low", "Low", "Medium",
"High", "High", "High", "High", "High"),
confidence_score = c(0.15, 0.2, 0.15, 0.3, 0.6,
0.8, 1.0, 1.0, 0.8, 0.9)
)
# Pre-calculate counts
summary_df <- df %>%
count(confidence_class, name = "count")
# Generate clean pie chart
ggplot(summary_df, aes(x = "", y = count, fill = confidence_class)) +
geom_col(width = 1, color = "white") +
coord_polar(theta = "y", start = 0) +
scale_fill_manual(values = c("indianred2", "tan1", "chartreuse3")) +
theme_void()
Method 2: Using Raw Data
If you prefer not to create an intermediate summary table, simply pass the raw dataframe into geom_bar() while keeping x = "":
ggplot(df, aes(x = "", fill = confidence_class)) +
geom_bar(width = 1, color = "white") +
coord_polar(theta = "y", start = 0) +
scale_fill_manual(values = c("indianred2", "tan1", "chartreuse3")) +
theme_void()
Key Takeaways
theme_void(): Removes coordinate lines, axes, and background grids that are unnecessary for radial charts.color = "white"ingeom_col(): Adds clear divider lines between slices.width = 1: Eliminates any inner hole, ensuring the pie chart fills cleanly from the center outward. If you want a donut chart instead, simply setx = 2and restrict the axis limits.