Basic Raincloud Plot — Matplotlib

A raincloud plot combines three visualization elements—a half-violin (the "cloud"), jittered strip points (the "rain"), and a box plot—to provide a comprehensive view of data distribution. This hybrid approach shows distribution shape, summary statistics, and individual observations simultaneously, addressing the limitation of box plots that can hide multimodal distributions. Often called the "gold standard" for transparent statistical visualization in scientific publications.

Basic Raincloud Plot rendered with Matplotlib

Python source (Matplotlib)

""" anyplot.ai
raincloud-basic: Basic Raincloud Plot
Library: matplotlib 3.10.9 | Python 3.13.13
Quality: 93/100 | Updated: 2026-05-26
"""

import os

import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import gaussian_kde


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"

# Imprint palette positions 1-3
COLORS = ["#009E73", "#C475FD", "#4467A3"]

# Data: Reaction times (ms) for three experimental conditions
np.random.seed(42)

# Control: roughly normal around 350 ms
control = np.random.normal(350, 50, 80)

# Treatment A: clearly bimodal — two response strategies
treatment_a = np.concatenate([np.random.normal(250, 25, 45), np.random.normal(340, 25, 35)])

# Treatment B: mostly central with a slow tail and a few outliers
treatment_b = np.concatenate([np.random.normal(300, 40, 60), np.random.normal(400, 25, 15), np.array([500, 520, 480])])

categories = ["Control", "Treatment A", "Treatment B"]
data = [control, treatment_a, treatment_b]

fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)

# Raincloud layout: cloud above baseline, boxplot on baseline, rain below
cloud_offset = 0.05
cloud_height = 0.32
rain_offset = -0.22
box_width = 0.08

for i, (d, color) in enumerate(zip(data, COLORS, strict=True)):
    pos = i + 1

    # Cloud — half-violin via scipy KDE, Silverman's rule
    kde = gaussian_kde(d, bw_method="silverman")
    x_min, x_max = d.min() - 30, d.max() + 30
    x_vals = np.linspace(x_min, x_max, 256)
    density = kde(x_vals)
    density_scaled = density / density.max() * cloud_height

    ax.fill_between(
        x_vals,
        pos + cloud_offset,
        pos + cloud_offset + density_scaled,
        color=color,
        alpha=0.75,
        edgecolor=color,
        linewidth=0.8,
        zorder=1,
    )

    # Boxplot — sits on the category baseline
    bp = ax.boxplot([d], positions=[pos], widths=box_width, vert=False, patch_artist=True, showfliers=False, zorder=3)
    bp["boxes"][0].set_facecolor(ELEVATED_BG)
    bp["boxes"][0].set_edgecolor(INK_SOFT)
    bp["boxes"][0].set_linewidth(1.2)
    bp["medians"][0].set_color(INK)
    bp["medians"][0].set_linewidth(1.6)
    for whisker in bp["whiskers"]:
        whisker.set_color(INK_SOFT)
        whisker.set_linewidth(1.0)
    for cap in bp["caps"]:
        cap.set_color(INK_SOFT)
        cap.set_linewidth(1.0)

    # Rain — jittered points below the baseline
    jitter = np.random.uniform(-0.07, 0.07, len(d))
    ax.scatter(
        d,
        np.full(len(d), pos + rain_offset) + jitter,
        s=28,
        alpha=0.6,
        color=color,
        edgecolor=PAGE_BG,
        linewidth=0.4,
        zorder=2,
    )

# Axes & ticks
ax.set_yticks([1, 2, 3])
ax.set_yticklabels(categories, fontsize=10, color=INK_SOFT)
ax.set_xlabel("Reaction Time (ms)", fontsize=10, color=INK)
ax.set_ylabel("Experimental Condition", fontsize=10, color=INK)

title = "raincloud-basic · python · matplotlib · anyplot.ai"
ax.set_title(title, fontsize=12, fontweight="medium", color=INK, pad=10)

ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)

# Range with breathing room
all_data = np.concatenate(data)
ax.set_xlim(all_data.min() - 40, all_data.max() + 40)
ax.set_ylim(0.45, 3.55)

# Subtle x-axis grid only
ax.xaxis.grid(True, alpha=0.15, color=INK, linewidth=0.8)
ax.set_axisbelow(True)

# L-frame: keep left + bottom
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_color(INK_SOFT)
ax.spines["bottom"].set_color(INK_SOFT)
ax.spines["left"].set_linewidth(0.8)
ax.spines["bottom"].set_linewidth(0.8)

fig.subplots_adjust(left=0.12, right=0.97, top=0.90, bottom=0.13)
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

Part of Basic Raincloud Plot on anyplot.ai.

Other implementations