Basic Swarm Plot — ggplot2

A swarm plot (beeswarm plot) displays individual data points for categorical comparisons, with points spread horizontally to avoid overlap. This reveals the full distribution shape and density while preserving exact values - combining the benefits of strip plots (individual points) and violin plots (density visualization). Ideal when you need to see every observation rather than just summary statistics.

Basic Swarm Plot rendered with ggplot2

R source (ggplot2)

#' anyplot.ai
#' swarm-basic: Basic Swarm Plot
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 84/100 | Created: 2026-07-26

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

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 ----------------------------------------------------------------
# Reaction times across a cognitive-load experiment (psychology research)
conditions <- c("Baseline", "Low Load", "High Load", "Time Pressure")
means <- c(420, 460, 540, 580)
sds   <- c(45, 50, 65, 70)
n_per_condition <- 45

reaction_df <- bind_rows(lapply(seq_along(conditions), function(i) {
  tibble::tibble(
    condition     = conditions[i],
    reaction_time = rnorm(n_per_condition, mean = means[i], sd = sds[i])
  )
})) %>%
  mutate(condition = factor(condition, levels = conditions))

# --- Swarm layout ----------------------------------------------------------
# Bin observations along the value axis, then rank within each bin so
# points that fall close in value are spread symmetrically around the
# category's center line - a lightweight beeswarm approximation built
# entirely from geom_point (ggplot2 has no native beeswarm geom).
bin_width     <- diff(range(reaction_df$reaction_time)) / 24
point_spacing <- 0.05
max_offset    <- 0.42

reaction_df <- reaction_df %>%
  mutate(value_bin = round(reaction_time / bin_width)) %>%
  group_by(condition, value_bin) %>%
  mutate(
    bin_rank = rank(reaction_time, ties.method = "first"),
    bin_n    = n(),
    x_offset = pmin(pmax((bin_rank - (bin_n + 1) / 2) * point_spacing, -max_offset), max_offset)
  ) %>%
  ungroup() %>%
  mutate(condition_x = as.numeric(condition) + x_offset)

condition_means <- reaction_df %>%
  group_by(condition) %>%
  summarise(mean_rt = mean(reaction_time), x_pos = as.numeric(condition[1]), .groups = "drop")

# --- Plot ------------------------------------------------------------------
p <- ggplot(reaction_df, aes(x = condition_x, y = reaction_time)) +
  geom_point(
    aes(fill = condition), shape = 21, size = 3.1, stroke = 0.3,
    color = PAGE_BG, alpha = 0.85
  ) +
  geom_line(
    data = condition_means,
    aes(x = x_pos, y = mean_rt),
    color = scales::alpha(INK, 0.4), linewidth = 0.5, linetype = "dashed"
  ) +
  geom_point(
    data = condition_means,
    aes(x = x_pos, y = mean_rt),
    shape = 18, size = 3.4, color = INK, alpha = 0.9
  ) +
  scale_fill_manual(values = IMPRINT_PALETTE[seq_along(conditions)], guide = "none") +
  scale_x_continuous(
    breaks = seq_along(conditions),
    labels = conditions,
    expand = expansion(mult = c(0.1, 0.1))
  ) +
  labs(
    title = "swarm-basic · r · ggplot2 · anyplot.ai",
    x = "Experimental Condition",
    y = "Reaction Time (ms)"
  ) +
  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 = scales::alpha(INK, 0.15), linewidth = 0.3),
    axis.title          = element_text(color = INK, size = 10),
    axis.text           = element_text(color = INK_SOFT, size = 8),
    axis.ticks          = element_blank(),
    axis.line           = element_line(color = INK_SOFT, linewidth = 0.3),
    plot.title          = element_text(color = INK, size = 12)
  )

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

Part of Basic Swarm Plot on anyplot.ai.

Other implementations