Overlapping Histograms in ggplot2 (R)

The same plot in 14 other libraries — Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, plotnine, Pygal, Seaborn; Julia: Makie.jl; JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts, MUI X Charts. Compare all 15 side by side: Overlapping Histograms in Python, R, Julia and JavaScript.

Overlapping histograms display multiple distributions on the same axes using semi-transparent bars, enabling direct visual comparison between groups. This technique reveals differences in central tendency, spread, and shape across categories while maintaining the familiar histogram format. The transparency allows viewers to see where distributions overlap and diverge.

Overlapping Histograms rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' histogram-overlapping: Overlapping Histograms
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 89/100 | Created: 2026-08-18

library(ggplot2)
library(ragg)

set.seed(42)

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

# --- Data -----------------------------------------------------------------
n <- 200
completion_time <- c(
  rnorm(n, mean = 45, sd = 8),
  rnorm(n, mean = 39, sd = 7),
  rnorm(n, mean = 51, sd = 9)
)
design <- factor(
  rep(c("Design A", "Design B", "Design C"), each = n),
  levels = c("Design A", "Design B", "Design C")
)
df <- tibble::tibble(completion_time = completion_time, design = design)

binwidth <- diff(range(df$completion_time)) / 28

# Group means drive the dashed reference lines and the fastest-design callout
group_means <- tapply(df$completion_time, df$design, mean)
mean_df <- tibble::tibble(
  design    = factor(names(group_means), levels = levels(df$design)),
  mean_time = as.numeric(group_means)
)
fastest <- mean_df[which.min(mean_df$mean_time), ]

# --- Plot -------------------------------------------------------------------
p <- ggplot(df, aes(x = completion_time, fill = design)) +
  geom_histogram(
    position  = "identity",
    binwidth  = binwidth,
    alpha     = 0.5,
    color     = INK_SOFT,
    linewidth = 0.15
  ) +
  geom_vline(
    xintercept = fastest$mean_time,
    color      = IMPRINT_PALETTE[which(levels(df$design) == fastest$design)],
    linetype   = "dashed",
    linewidth  = 0.6
  ) +
  annotate(
    "text",
    x        = fastest$mean_time,
    y        = Inf,
    label    = sprintf("%s: fastest avg (%.0fs)", fastest$design, fastest$mean_time),
    hjust    = -0.05,
    vjust    = 1.6,
    size     = 3,
    fontface = "bold",
    color    = IMPRINT_PALETTE[which(levels(df$design) == fastest$design)]
  ) +
  scale_fill_manual(values = IMPRINT_PALETTE[1:3], name = "UI design") +
  scale_y_continuous(expand = expansion(mult = c(0, 0.08))) +
  labs(
    title = "histogram-overlapping · r · ggplot2 · anyplot.ai",
    x     = "Task Completion Time (seconds)",
    y     = "Number of Users"
  ) +
  theme_minimal(base_size = 8) +
  theme(
    plot.background   = element_rect(fill = PAGE_BG, color = PAGE_BG),
    panel.background  = element_rect(fill = PAGE_BG, color = NA),
    panel.grid.major.x = element_blank(),
    panel.grid.minor  = element_blank(),
    panel.grid.major.y = element_line(color = INK, linewidth = 0.15),
    axis.title        = element_text(color = INK, size = 10),
    axis.text         = element_text(color = INK_SOFT, size = 8),
    axis.line         = element_line(color = INK_SOFT),
    plot.title        = element_text(color = INK, size = 12),
    legend.position   = "right",
    legend.title      = element_text(color = INK, size = 10),
    legend.text       = element_text(color = INK_SOFT, size = 8),
    legend.background = element_blank(),
    legend.key        = element_blank()
  )

# --- Save -------------------------------------------------------------------
ggsave(
  filename = sprintf("plot-%s.png", THEME),
  plot     = p,
  device   = ragg::agg_png,
  width    = 8,
  height   = 4.5,
  units    = "in",
  dpi      = 400
)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/histogram-overlapping/ggplot2/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "histogram-overlapping",
  "language": "r",
  "library": "ggplot2",
  "page": "https://anyplot.ai/histogram-overlapping/r/ggplot2",
  "hub": "https://anyplot.ai/histogram-overlapping",
  "code_json": "https://api.anyplot.ai/specs/histogram-overlapping/ggplot2/code",
  "spec_json": "https://api.anyplot.ai/specs/histogram-overlapping",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/histogram-overlapping/r/ggplot2/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/histogram-overlapping/r/ggplot2/plot-dark.png",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Overlapping Histograms on anyplot.ai.

Other implementations