Waffle Chart in R: Display Proportions as a Grid of Squares
A waffle chart (also called a square pie chart) displays proportions as a grid of unit squares — each square represents one unit or one percent. In R, the waffle package provides geom_waffle() as a ggplot2 layer.
Introduction
Pie charts and bar charts communicate proportions. But both have a readability limitation: the human eye struggles to compare areas (pie) or lengths (bar) precisely when differences are small.
A waffle chart sidesteps this by making proportions countable. If each square represents 1%, a 37% category has exactly 37 squares — readers can verify the proportion by counting. This makes waffle charts particularly effective for general audiences who need to grasp "about 1 in 3" or "more than half" at a glance.
The tradeoff: waffle charts don't work well for many categories (a 10x10 grid divided among 8 slices becomes a confusing patchwork). They shine for 2-4 categories with meaningful proportional differences.
The waffle package by Bob Rudis extends ggplot2 with geom_waffle(), fitting naturally into the ggplot2 grammar.
How do you create a basic waffle chart in R?
geom_waffle() takes counts in the values aesthetic and fills by a categorical variable. It arranges squares in rows across a grid.
With n_rows = 10 and total votes summing to 100, each square represents exactly 1%. coord_equal() ensures squares don't get stretched into rectangles. theme_void() removes axis elements that are meaningless in a grid chart.
Try it: Change n_rows = 10 to n_rows = 5 — you now have a 5×20 grid instead of 10×10. The proportions are identical, but the shape changes. Which layout reads more intuitively?
How do you make each square represent exactly 1%?
The classic waffle chart convention: a 10×10 grid where each of the 100 squares = 1%. This requires your values to sum to exactly 100. Use make_proportional = TRUE to let geom_waffle rescale automatically.
make_proportional = TRUE internally rescales the counts so they sum to 100 (or the total grid squares defined by n_rows). This lets you pass raw counts instead of pre-calculated percentages.
Try it: Add na.rm = FALSE inside geom_waffle() to see what happens when rounding causes the total to be slightly off from 100. Then remove it and see the chart fill cleanly.
How do you compare groups with faceted waffle charts?
A single waffle chart shows one snapshot. Facets create multiple grids side by side — one per group — making comparison across time periods or categories intuitive.
Each year gets its own 10×10 grid. The reader can compare Android's green area vs iOS's blue area across years. The stability of the proportions becomes immediately obvious — barely any change year over year.
Try it: Change the pct values for 2023 to c(68, 31, 1) (iOS gaining share) and see how the visual change in the 2023 panel immediately communicates the shift.
How do you use the base waffle() function for quick charts?
The waffle package also provides a standalone waffle() function (no ggplot2 required) for rapid creation.
The base waffle() function takes a named vector of values (not a data frame). It's quicker for exploratory work, but offers less styling flexibility than geom_waffle().
Try it: Change rows = 5 to rows = 10. What does xlab = "1 square = 2%" tell the reader when rows = 5? Calculate: total = 100 values / 50 squares = 2 per square. For rows = 10 (100 squares total), each square would = 1%.
Complete Example: Polished Waffle with Annotations
Common Mistakes and How to Fix Them
Mistake 1: Forgetting coord_equal()
Without coord_equal(), squares get stretched into rectangles based on the plot aspect ratio. Always add it.
Mistake 2: Values don't sum correctly
If you're not using make_proportional = TRUE, your values must sum to a multiple of n_rows (e.g., 100 for a 10×10 grid). Partial squares create visual artifacts.
Mistake 3: Too many categories
✅ Collapse small categories into "Other" and keep 3-4 groups maximum.
Mistake 4: Not labeling what each square represents
Always add a subtitle or caption telling readers what one square represents: "Each square = 1% of respondents" or "Each square = 100 employees".
Mistake 5: Using waffle for continuous distributions
Waffle charts encode counts, not distributions. For continuous data (age distributions, measurement ranges), use histograms or density plots instead.
Practice Exercises
Exercise 1: Create a vote share waffle
Using the data below, create a 10×10 waffle chart. Use custom colors and add a subtitle explaining what each square represents.
| Party | Votes |
|---|---|
| Green | 24 |
| Blue | 38 |
| Red | 28 |
| Orange | 10 |
Show solution
Exercise 2: Faceted comparison
Extend Exercise 1 to show the same data for three election years with different results. Create a faceted waffle with facet_wrap(~ year).
Show solution
Summary
| Task | Code |
|---|---|
| Basic waffle | geom_waffle(n_rows = 10, size = 0.3, colour = "white") |
| Square grid | + coord_equal() (always required) |
| Auto-scale to 100 | make_proportional = TRUE |
| Faceted comparison | + facet_wrap(~ group) |
| Clean background | + theme_void() |
| Quick base version | waffle(c(A = 40, B = 60), rows = 10) |
When waffle charts work well:
- 2-4 categories with clear proportional differences
- Audience needs to count or verify proportions
- "1 in 3" or "more than half" narratives
- Comparing two time periods side by side (facets)
When to use other charts:
- More than 4-5 categories (too many small squares)
- Continuous distributions (use histogram or density plot)
- Precise comparisons needed (use bar chart)
- Very small proportions (< 2-3%) — too few squares to see
FAQ
What is the difference between the waffle package and ggwaffle? waffle (by hrbrmstr) is the original package with both a standalone waffle() function and geom_waffle(). ggwaffle (by liamgilbey) is a closer ggplot2-idiomatic alternative. Both work well; waffle is more widely used.
How do I make each square represent a specific unit (e.g., 1,000 employees)? Keep your values as multiples of the unit. If n = 10,000 employees and you want 1 square = 100 employees, divide all values by 100 before plotting. Add xlab = "1 square = 100 employees" to the plot.
Can I use icons instead of squares? Yes — the waffle package supports Font Awesome icons via geom_pictogram(). Install font-awesome fonts and use geom_pictogram(aes(label = icon_name)) to replace squares with people icons, cars, etc.
How do I remove the gap between squares? Set size = 0 inside geom_waffle() — this removes the gap entirely. Squares touch each other, creating a solid block. A small gap (size = 0.3) usually reads better.
Why are my squares rectangular instead of square? You forgot coord_equal(). Add + coord_equal() to the plot.
References
- waffle package: github.com/hrbrmstr/waffle
- R Graph Gallery — Waffle chart: r-graph-gallery.com/waffle.html
- Wilke C. (2019). Fundamentals of Data Visualization — Chapter 10: Visualizing proportions
- r-charts.com — Waffle chart in ggplot2
What's Next?
- ggplot2 Bar Charts — precise comparisons for categorical data
- Pie Chart and Donut Chart in R — classic proportional charts for 3-5 categories
- Treemap in R — hierarchical proportions for many categories