library(tidyverse)
library(patchwork)
library(showtext)
library(ggtext)
library(glue)
library(waffle)
library(ggpattern)
# Load fonts
font_add_google("Sora")
showtext_auto()
showtext_opts(dpi = 300)Waffle charts are a great way to visualize proportions and counts in a more engaging way than traditional bar charts. In this post, I’ll walk through how to create waffle charts in R, from basic tiles to image-filled patterns. Note that this post was made for personal use and was largely inspired by other blog posts I have found on the topic including: this one and also this one.
Setup
First, load the packages:
Basic Waffle Chart
The simplest waffle chart is just a grid of tiles. We create a tibble with row and column positions calculated from a sequence of IDs, then use geom_tile() to draw the grid.
tibble(id = 0:22, row = id %% 5, col = id %/% 5) |>
ggplot(aes(x = col, y = row)) +
geom_tile(fill = "dodgerblue4", col = "white", linewidth = 1) +
coord_equal() +
labs(title = "Random waffle") +
theme_void(base_size = 18, base_family = "Sora")
The key here is:
id %% 5gives us the row position (0-4, cycling)id %/% 5gives us the column position (incrementing every 5 tiles)coord_equal()ensures our tiles are square
Adding Images with ggpattern
To make waffle charts more visually interesting, we can replace solid colors with images using ggpattern. The trick is to: 1. Use geom_tile_pattern() instead of geom_tile() 2. Set pattern = "image" 3. Point pattern_filename to image file
tibble(id = 0:22, row = id %% 5, col = id %/% 5) |>
ggplot(aes(x = col, y = row)) +
geom_tile_pattern(
fill = "transparent",
col = "white",
linewidth = 1,
pattern = "image",
pattern_filename = "imgs/smart.png"
) +
coord_equal() +
labs(
title = "Random waffle",
caption = "Flaticon Icon by Us and Up"
) +
theme_void(base_size = 18, base_family = "Sora")
Creating a Data-Driven Waffle Chart
Now let’s create a waffle chart that actually represents data. We’ll simulate some data with three categories.
Step 1: Create Sample Data
set.seed(123) # For reproducibility
fitz <- data.frame(
var1 = sample(416:565, size = 23, replace = TRUE),
names = rep(c("shades", "smart", "smoke"), length.out = 23)
)
head(fitz) var1 names
1 429 shades
2 465 smart
3 533 smoke
4 458 shades
5 429 smart
6 533 smoke
Step 2: Wrangle Data for Plotting
We need to summarize our data to get counts for each category. The steps are:
- Group by category — aggregate within each group
- Calculate mean and scale — get a reasonable number of tiles per group
- Arrange by count — order from highest to lowest
- Create factor levels — preserve the sorted order
fitz_dat <- fitz |>
group_by(names) |>
summarize(
mean_n = mean(var1, na.rm = TRUE),
fitz_n = round(mean_n / 10)
) |>
arrange(desc(fitz_n)) |>
mutate(
group_name = factor(names, levels = names)
)
fitz_dat# A tibble: 3 × 4
names mean_n fitz_n group_name
<chr> <dbl> <dbl> <fct>
1 smoke 505. 51 smoke
2 smart 495. 50 smart
3 shades 486. 49 shades
Step 3: Build the Waffle Structure
The waffle package provides geom_waffle() which handles the tile positioning for us. We’ll build the plot and extract the underlying data to customize it further.
basic_plot <- ggplot(
data = fitz_dat,
mapping = aes(fill = names, values = fitz_n)
) +
geom_waffle(
color = "white",
size = 0.25,
n_rows = 4,
flip = FALSE
) +
facet_wrap(
~names,
ncol = 1,
strip.position = "left"
)
# Extract the built plot data
basic_built <- ggplot_build(basic_plot)
# Create new_data with the correct structure
new_data <- basic_built$data[[1]] |>
rename(names = PANEL) |>
mutate(
names = factor(names, labels = levels(fitz_dat$group_name))
)
head(new_data) y x values colour fill names group xmin xmax ymin ymax alpha size linetype
1 1 1 NA white #F8766D smoke 1 0.5 1.5 0.5 1.5 NA 0.25 1
2 2 1 NA white #F8766D smoke 2 0.5 1.5 1.5 2.5 NA 0.25 1
3 3 1 NA white #F8766D smoke 3 0.5 1.5 2.5 3.5 NA 0.25 1
4 4 1 NA white #F8766D smoke 4 0.5 1.5 3.5 4.5 NA 0.25 1
5 1 2 NA white #F8766D smoke 5 1.5 2.5 0.5 1.5 NA 0.25 1
6 2 2 NA white #F8766D smoke 6 1.5 2.5 1.5 2.5 NA 0.25 1
width height
1 NA NA
2 NA NA
3 NA NA
4 NA NA
5 NA NA
6 NA NA
Step 4: Define Styling
Set up colors, fonts, and text elements for a polished look:
bg_col <- "#ffecb3"
text_col <- "#072e3c"
highlight_col <- "#ff6361"
body_font <- "Sora"Step 5: Calculate Labels with Counts and Percentages
Create informative labels that show both the count and percentage for each category:
fitz_labels <- fitz_dat |>
ungroup() |>
mutate(
total = sum(fitz_n),
pct = round(fitz_n / total * 100, 1),
label = glue("{str_to_title(names)} (n = {fitz_n}, {pct}%)")
)
fitz_labels# A tibble: 3 × 7
names mean_n fitz_n group_name total pct label
<chr> <dbl> <dbl> <fct> <dbl> <dbl> <glue>
1 smoke 505. 51 smoke 150 34 Smoke (n = 51, 34%)
2 smart 495. 50 smart 150 33.3 Smart (n = 50, 33.3%)
3 shades 486. 49 shades 150 32.7 Shades (n = 49, 32.7%)
This gives us labels like “Smart (n = 52, 35.4%)” that communicate both the raw count and the proportion.
Step 6: Add Image Paths and Join Labels
Now we prepare the plot data by adding the image file paths and joining our formatted labels:
new_data <- new_data |>
as_tibble() |>
mutate(
img_path = glue("imgs/{stringr::str_to_lower(names)}.png"),
names_clean = stringr::str_to_title(names)
) |>
left_join(
fitz_labels |>
mutate(names_clean = str_to_title(names)) |>
select(names_clean, label),
by = "names_clean"
)
new_data |>
select(x, y, names, label, img_path) |>
head()# A tibble: 6 × 5
x y names label img_path
<dbl> <dbl> <fct> <glue> <glue>
1 1 1 smoke Smoke (n = 51, 34%) imgs/smoke.png
2 1 2 smoke Smoke (n = 51, 34%) imgs/smoke.png
3 1 3 smoke Smoke (n = 51, 34%) imgs/smoke.png
4 1 4 smoke Smoke (n = 51, 34%) imgs/smoke.png
5 2 1 smoke Smoke (n = 51, 34%) imgs/smoke.png
6 2 2 smoke Smoke (n = 51, 34%) imgs/smoke.png
Key points: - img_path creates the file path for each category’s image - left_join() attaches the formatted labels we created earlier - Each tile now knows which image to display and what label its facet should have
Step 7: Create the Final Plot
Now we bring everything together into a polished visualization:
g2 <- ggplot(
data = new_data,
mapping = aes(x = x, y = y)
) +
ggpattern::geom_tile_pattern(
aes(pattern_filename = img_path),
fill = "transparent",
colour = text_col,
linewidth = 0.5,
pattern = "image",
pattern_type = "squish"
) +
scale_pattern_filename_identity() +
facet_wrap(
~label,
ncol = 1,
strip.position = "left"
) +
labs(
title = "Fitz!",
subtitle = "A waffle chart showing the distribution across categories",
caption = "Icons from Flaticon"
) +
coord_fixed(clip = "off") +
theme_void(base_family = body_font, base_size = 12) +
theme(
legend.position = "none",
plot.margin = margin(20, 30, 20, 20),
plot.title = element_text(
size = 24,
face = "bold",
colour = text_col,
hjust = 0,
margin = margin(b = 5)
),
plot.subtitle = element_text(
size = 12,
colour = text_col,
hjust = 0,
margin = margin(b = 20)
),
plot.caption = element_text(
size = 9,
colour = text_col,
hjust = 1,
margin = margin(t = 15)
),
plot.title.position = "plot",
plot.caption.position = "plot",
plot.background = element_rect(fill = bg_col, colour = NA),
panel.background = element_rect(fill = bg_col, colour = NA),
panel.spacing = unit(1, "lines"),
strip.text.y.left = element_text(
size = 12,
face = "bold",
colour = text_col,
angle = 0,
hjust = 1,
vjust = 0.5,
margin = margin(r = 15)
)
)
g2
Let’s break down the key elements:
aes(pattern_filename = img_path)— maps each tile to its category’s imagescale_pattern_filename_identity()— uses the file paths directlypattern_type = "squish"— fits images properly within tilesfacet_wrap(~label, ...)— uses our formatted labels with counts and percentagesstrip.text.y.left— styles the facet labels horizontally on the left
Step 8: Save the Plot
ggsave(
filename = "waffle.png",
plot = g2,
height = 6,
width = 8,
bg = bg_col,
units = "in",
dpi = 300
)Summary
To create an image-based waffle chart with informative labels:
- Structure data with counts per category
- Use
geom_waffle()to build the grid layout - Extract plot data using
ggplot_build()for customization - Calculate summary statistics like counts and percentages
- Create formatted labels using
glue()for clear communication - Add image paths and join labels to plot data
- Apply
geom_tile_pattern()withpattern = "image"and map filenames per category - Style with
theme_void()and custom theme elements