Ridgeline Plot (Python)

Authors

[Editor] Shixiang Wang;

[Contributors] Claude Code โ€” AI-generated, independently AI-reviewed;

Modified

2026-10-19

๐Ÿค– AI Skill โ€” Download the Bizard unified skill for your AI assistant โฌ‡๏ธ Download Skill ZIP
๐Ÿ”Œ MCP Server โ€” Let AI agents (Claude Code, Cursor, โ€ฆ) query Bizard live: claude mcp add bizard -- npx @openbiox/bizard-mcp โ€” or the free hosted instance (no install, faster from mainland China): claude mcp add --transport http bizard http://biotree.top:38123/bizard-mcp/mcp ๐Ÿ“– MCP Docs
๐Ÿ“– Cite Bizard โ€” if Bizard helps your research, please cite: Li, K., Zheng, H., Huang, K., et al. (2026). Bizard: A Community-Driven Platform for Accelerating and Enhancing Biomedical Data Visualization. iMetaMed, e70038. ๐Ÿ”— DOI / BibTeX

A ridgeline plot (joyplot) stacks one density curve per category along a shared x-axis, exposing how a distribution shifts across many groups at a glance. In biomedical research it is a favorite for comparing marker-expression distributions across cell clusters, or any many-group distribution comparison that would overwhelm a grid of faceted histograms. In Python it composes cleanly from per-group kernel density estimates (scipy.stats.gaussian_kde) layered on shared matplotlib axes.

Example

Setup

  • System Requirements: Cross-platform (Linux/MacOS/Windows)
  • Programming Language: Python
  • Dependencies: matplotlib, pandas, numpy, scipy
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from scipy.stats import gaussian_kde

Data Preparation

We simulate the expression of a surface marker across eight PBMC cell clusters, with cluster-specific means that produce a visible gradient along the stacking order.

np.random.seed(31)

clusters = ['CD4 T', 'CD8 T', 'NK', 'B', 'Monocyte', 'DC', 'CD34+', 'Plasma']
n_per_cluster = 400
base_means = np.linspace(2.0, 7.5, len(clusters))

records = []
for cluster, mu in zip(clusters, base_means):
    records.append(pd.DataFrame({
        'Cluster': cluster,
        'Marker': np.random.normal(mu, 1.1, n_per_cluster)
    }))
marker = pd.concat(records, ignore_index=True)

# Shared evaluation grid and ridge spacing
xs = np.linspace(-1, 11, 300)
offset = 0.55

Visualization

Basic Ridgeline Plot

Each cluster contributes one kernel density estimate; ridges are drawn bottom-up so the upper curves partially cover the ones beneath them.

n = len(clusters)
fig, ax = plt.subplots(figsize=(7, 5))
for i in reversed(range(n)):                    # bottom first, top last
    d = marker.loc[marker['Cluster'] == clusters[i], 'Marker']
    ys = gaussian_kde(d)(xs)
    base = offset * (n - 1 - i)
    ax.plot(xs, base + ys, color='0.3', linewidth=1.1)
    ax.fill_between(xs, base, base + ys, color='#8da0cb', alpha=0.9)
ax.set_yticks([offset * (n - 1 - i) for i in range(n)], labels=clusters)
ax.set_xlabel('Marker expression (a.u.)')
ax.spines[['top', 'right', 'left']].set_visible(False)
ax.tick_params(axis='y', length=0)
plt.tight_layout()
plt.show()
Figureย 1: Marker-expression distribution across eight cell clusters

Fading Ridge with Colormap

Encoding the stacking order with a colormap and fading the fill toward the back helps when many groups make labels hard to trace.

n = len(clusters)
fig, ax = plt.subplots(figsize=(7, 5))
for i in reversed(range(n)):
    d = marker.loc[marker['Cluster'] == clusters[i], 'Marker']
    ys = gaussian_kde(d)(xs)
    base = offset * (n - 1 - i)
    shade = plt.cm.viridis(i / (n - 1))
    ax.plot(xs, base + ys, color=shade, linewidth=1.1)
    ax.fill_between(xs, base, base + ys, color=shade,
                    alpha=0.35 + 0.6 * i / (n - 1))
ax.set_yticks([offset * (n - 1 - i) for i in range(n)], labels=clusters)
ax.set_xlabel('Marker expression (a.u.)')
ax.spines[['top', 'right', 'left']].set_visible(False)
ax.tick_params(axis='y', length=0)
plt.tight_layout()
plt.show()
Figureย 2: Ridgeline with colormap-encoded order and fading fill

Ridgeline from Histograms

Replacing the KDE with per-group histograms keeps the discrete sampling visible in smaller cohorts.

n = len(clusters)
bins = np.linspace(-1, 11, 25)
fig, ax = plt.subplots(figsize=(7, 5))
for i in reversed(range(n)):
    d = marker.loc[marker['Cluster'] == clusters[i], 'Marker']
    counts, edges = np.histogram(d, bins=bins)
    base = offset * (n - 1 - i)
    ax.bar(edges[:-1], counts, width=np.diff(edges), bottom=base,
           align='edge', color='#66c2a5', edgecolor='0.2', linewidth=0.6)
ax.set_yticks([offset * (n - 1 - i) for i in range(n)], labels=clusters)
ax.set_xlabel('Marker expression (a.u.)')
ax.set_ylabel('Count (offset per cluster)')
ax.spines[['top', 'right', 'left']].set_visible(False)
ax.tick_params(axis='y', length=0)
plt.tight_layout()
plt.show()
Figureย 3: Histogram-based ridgeline (binned counts per cluster)

References

  1. Hunter, J. D. (2007). Matplotlib: A 2D graphics environment. Computing in Science & Engineering, 9(3), 90-95.
  2. Virtanen, P., et al.ย (2020). SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3), 261-272.
  3. From Data to Viz. Ridgeline. https://www.data-to-viz.com/graph/ridgeline.html