Mosaic Plot for Categorical Association Analysis — ggplot2

A mosaic plot visualizes contingency tables by dividing a rectangular area into smaller rectangles whose areas are proportional to cell frequencies. This statistical visualization technique effectively shows relationships and associations between two or more categorical variables, making it easy to identify patterns, dependencies, and deviations from expected frequencies in cross-tabulated data.

Mosaic Plot for Categorical Association Analysis rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' mosaic-categorical: Mosaic Plot for Categorical Association Analysis
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 86/100 | Created: 2026-05-19

library(ggplot2)
library(dplyr)
library(scales)
library(ragg)

set.seed(42)

# Theme tokens
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
IMPRINT   <- c("#009E73", "#C475FD", "#4467A3", "#BD8233",
                 "#AE3030", "#2ABCCD", "#954477")

# Data: product category vs customer satisfaction (retail survey, n = 500)
category_levels     <- c("Electronics", "Clothing", "Food", "Books")
satisfaction_levels <- c("Satisfied", "Neutral", "Unsatisfied")

freq_data <- data.frame(
  category     = factor(rep(category_levels, each = 3), levels = category_levels),
  satisfaction = factor(rep(satisfaction_levels, times = 4), levels = satisfaction_levels),
  freq         = c(
    80, 50, 20,  # Electronics: n = 150
    60, 55, 25,  # Clothing:    n = 140
    65, 35, 20,  # Food:        n = 120
    70, 15,  5   # Books:       n = 90
  )
)

# Marginal totals per category — determines column widths
category_totals <- freq_data %>%
  group_by(category) %>%
  summarize(total = sum(freq), .groups = "drop") %>%
  arrange(category) %>%
  mutate(
    width_prop = total / sum(total),
    xmax       = cumsum(width_prop),
    xmin       = lag(xmax, default = 0),
    xmin_plot  = xmin + 0.005,
    xmax_plot  = xmax - 0.005,
    x_center   = (xmin_plot + xmax_plot) / 2
  )

# Conditional heights within each column
plot_data <- freq_data %>%
  left_join(category_totals, by = "category") %>%
  arrange(category, satisfaction) %>%
  group_by(category) %>%
  mutate(
    height_prop = freq / total,
    ymax        = cumsum(height_prop)
  ) %>%
  mutate(
    ymin      = lag(ymax, default = 0),
    ymin_plot = ymin + 0.003,
    ymax_plot = ymax - 0.003
  ) %>%
  ungroup()

# Plot
p <- ggplot(plot_data) +
  geom_rect(
    aes(
      xmin = xmin_plot, xmax = xmax_plot,
      ymin = ymin_plot, ymax = ymax_plot,
      fill = satisfaction
    ),
    color = PAGE_BG, linewidth = 0.6
  ) +
  scale_fill_manual(
    values = setNames(IMPRINT[1:3], satisfaction_levels),
    name   = "Customer\nSatisfaction"
  ) +
  scale_x_continuous(
    breaks = category_totals$x_center,
    labels = paste0(category_totals$category, "\n(n = ", category_totals$total, ")"),
    expand = c(0.01, 0.01)
  ) +
  scale_y_continuous(
    labels = percent_format(accuracy = 1),
    expand = c(0.01, 0.01)
  ) +
  labs(
    title = "Customer Satisfaction by Product Category · mosaic-categorical · r · ggplot2 · anyplot.ai",
    x     = "Product Category  (column width proportional to group size)",
    y     = "Proportion of Respondents"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    plot.background   = element_rect(fill = PAGE_BG, color = PAGE_BG),
    panel.background  = element_rect(fill = PAGE_BG, color = NA),
    panel.grid        = element_blank(),
    axis.title        = element_text(color = INK,      size = 18),
    axis.text.x       = element_text(color = INK_SOFT, size = 14),
    axis.text.y       = element_text(color = INK_SOFT, size = 14),
    axis.ticks        = element_blank(),
    plot.title        = element_text(color = INK,      size = 18, hjust = 0.5),
    legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT),
    legend.text       = element_text(color = INK_SOFT, size = 14),
    legend.title      = element_text(color = INK,      size = 16),
    plot.margin       = margin(40, 60, 40, 40)
  )

ggsave(
  filename = sprintf("plot-%s.png", THEME),
  plot     = p,
  device   = ragg::agg_png,
  width    = 16,
  height   = 9,
  units    = "in",
  dpi      = 300
)

Part of Mosaic Plot for Categorical Association Analysis on anyplot.ai.

Other implementations