A grouped violin plot with individual data points overlaid as swarm points, showing distributions across two categorical dimensions simultaneously. Multiple violin plots are arranged by category on the x-axis and grouped by a secondary variable using color/hue, with swarm points revealing the underlying raw data. This visualization combines distribution shape visualization with complete data transparency, ideal for comparing how distributions differ across multiple factors.

#' anyplot.ai
#' violin-grouped-swarm: Grouped Violin Plot with Swarm Overlay
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 88/100 | Created: 2026-05-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 <- c("#009E73", "#C475FD", "#4467A3", "#BD8233",
"#AE3030", "#2ABCCD", "#954477")
# --- Data -------------------------------------------------------------------
# Response times (ms) across task types and expertise levels
set.seed(42)
n_per_group <- 50
df <- data.frame(
category = character(n_per_group * 6),
group = character(n_per_group * 6),
value = numeric(n_per_group * 6),
stringsAsFactors = FALSE
)
idx <- 1
for (cat in c("Data Entry", "Analysis", "Decision-Making")) {
for (grp in c("Junior", "Senior")) {
mean_val <- if (cat == "Data Entry") {
if (grp == "Junior") 800 else 520
} else if (cat == "Analysis") {
if (grp == "Junior") 1500 else 975
} else {
if (grp == "Junior") 2200 else 1430
}
sd_val <- if (cat == "Data Entry") 200 else if (cat == "Analysis") 300 else 350
if (grp == "Senior") sd_val <- sd_val * 0.75
for (i in 1:n_per_group) {
df$category[idx] <- cat
df$group[idx] <- grp
df$value[idx] <- max(100, rnorm(1, mean = mean_val, sd = sd_val))
idx <- idx + 1
}
}
}
# --- Plot -------------------------------------------------------------------
p <- ggplot(df, aes(x = category, y = value, fill = group, color = group)) +
geom_violin(
alpha = 0.5,
linewidth = 0.6,
position = position_dodge(width = 0.8)
) +
geom_point(
size = 2.5,
alpha = 0.6,
position = position_dodge(width = 0.8),
shape = 21,
stroke = 0.5
) +
scale_fill_manual(values = IMPRINT[1:2]) +
scale_color_manual(values = IMPRINT[1:2]) +
labs(
title = "violin-grouped-swarm · R · ggplot2 · anyplot.ai",
x = "Task Type",
y = "Response Time (ms)",
fill = "Expertise Level",
color = "Expertise Level"
) +
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.major = element_line(color = INK, linewidth = 0.3),
panel.grid.minor = element_blank(),
axis.title = element_text(color = INK, size = 20, face = "bold"),
axis.text = element_text(color = INK_SOFT, size = 16),
plot.title = element_text(color = INK, size = 24, face = "bold"),
legend.background = element_rect(fill = PAGE_BG, color = NA),
legend.text = element_text(color = INK_SOFT, size = 16),
legend.title = element_text(color = INK, size = 18, face = "bold"),
legend.position = "top",
panel.border = element_rect(color = INK_SOFT, fill = NA, linewidth = 0.4)
)
# --- Save -------------------------------------------------------------------
ggsave(
filename = sprintf("plot-%s.png", THEME),
plot = p,
device = ragg::agg_png,
width = 16,
height = 9,
units = "in",
dpi = 300
)
Part of Grouped Violin Plot with Swarm Overlay on anyplot.ai.