Violin Plot with Overlaid Swarm Points — Seaborn

A violin plot with individual data points overlaid as a swarm plot, combining smooth kernel density estimation with raw data visibility. The violin shape shows the distribution density while the swarm points reveal actual observations, enabling viewers to see both the overall distribution pattern and individual data values simultaneously. This hybrid approach provides maximum transparency, showing exactly how many observations exist at each level while maintaining the smooth distribution visualization.

Violin Plot with Overlaid Swarm Points rendered with Seaborn

Python source (Seaborn)

""" anyplot.ai
violin-swarm: Violin Plot with Overlaid Swarm Points
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 90/100 | Updated: 2026-05-18
"""

import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Okabe-Ito palette - first series always #009E73
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233"]

# Data - Gene expression levels (arbitrary units) across cell types
np.random.seed(42)

cell_types = ["Neuron", "Astrocyte", "Oligodendrocyte", "Microglia"]
n_per_type = 60

data = []
for cell_type in cell_types:
    # Different distributions per cell type
    if cell_type == "Neuron":
        values = np.random.normal(85, 12, n_per_type)
    elif cell_type == "Astrocyte":
        values = np.random.normal(72, 15, n_per_type)
    elif cell_type == "Oligodendrocyte":
        # Bimodal distribution
        values = np.concatenate([np.random.normal(55, 10, n_per_type // 2), np.random.normal(92, 8, n_per_type // 2)])
    else:  # Microglia
        values = np.random.normal(65, 14, n_per_type)

    for v in values:
        data.append({"Cell Type": cell_type, "Expression Level": v})

df = pd.DataFrame(data)

# Plot
sns.set_theme(
    style="ticks",
    rc={
        "figure.facecolor": PAGE_BG,
        "axes.facecolor": PAGE_BG,
        "axes.edgecolor": INK_SOFT,
        "axes.labelcolor": INK,
        "text.color": INK,
        "xtick.color": INK_SOFT,
        "ytick.color": INK_SOFT,
        "grid.color": INK,
        "grid.alpha": 0.10,
        "legend.facecolor": ELEVATED_BG,
        "legend.edgecolor": INK_SOFT,
    },
)

fig, ax = plt.subplots(figsize=(16, 9))

# Violin plot with transparency
sns.violinplot(
    data=df,
    x="Cell Type",
    y="Expression Level",
    hue="Cell Type",
    palette=IMPRINT,
    alpha=0.4,
    inner=None,
    legend=False,
    ax=ax,
)

# Swarm plot overlay with darker shades
darker_palette = ["#006B50", "#A63F00", "#004D7A", "#8B5A7C"]
sns.swarmplot(
    data=df, x="Cell Type", y="Expression Level", hue="Cell Type", palette=darker_palette, size=6, legend=False, ax=ax
)

# Styling
ax.set_title("violin-swarm · Python · seaborn · anyplot.ai", fontsize=24, fontweight="medium", color=INK)
ax.set_xlabel("Cell Type", fontsize=20, color=INK)
ax.set_ylabel("Expression Level", fontsize=20, color=INK)
ax.tick_params(axis="both", labelsize=16, colors=INK_SOFT)
ax.grid(True, axis="y", alpha=0.15, linewidth=0.8)

# Spine styling
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
for spine in ["left", "bottom"]:
    ax.spines[spine].set_color(INK_SOFT)

plt.tight_layout()
plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)

Part of Violin Plot with Overlaid Swarm Points on anyplot.ai.

Other implementations