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:

  1. Fix the X-axis: Map x = "" (or any constant value like x = 1) to force all slices into a single stacked bar.
  2. Map the values to Y: Map your counts or values to y.
  3. Use geom_col(): When working with summarized counts, use geom_col() (which defaults to stat = "identity"). If starting with raw rows, map aes(x = "", fill = confidence_class) and let geom_bar() tally the totals automatically.
  4. 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" in geom_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 set x = 2 and restrict the axis limits.