Waffles!?

How to make a waffle chart with images so that I don’t keep having to look it up!!

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:

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)

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 %% 5 gives us the row position (0-4, cycling)
  • id %/% 5 gives 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:

  1. Group by category — aggregate within each group
  2. Calculate mean and scale — get a reasonable number of tiles per group
  3. Arrange by count — order from highest to lowest
  4. 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 image
  • scale_pattern_filename_identity() — uses the file paths directly
  • pattern_type = "squish" — fits images properly within tiles
  • facet_wrap(~label, ...) — uses our formatted labels with counts and percentages
  • strip.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:

  1. Structure data with counts per category
  2. Use geom_waffle() to build the grid layout
  3. Extract plot data using ggplot_build() for customization
  4. Calculate summary statistics like counts and percentages
  5. Create formatted labels using glue() for clear communication
  6. Add image paths and join labels to plot data
  7. Apply geom_tile_pattern() with pattern = "image" and map filenames per category
  8. Style with theme_void() and custom theme elements
Back to top