Lollipop & Dumbbell 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 lollipop plot replaces each bar of a bar chart with a thin line ending in a dot, cutting chart ink while keeping the ranking readable; a dumbbell plot connects two dots per row to expose the change between two conditions. In biomedical research, lollipops rank enrichment terms or effect sizes, and dumbbells compare two cohorts, time points, or treatment arms per feature. Both are assembled in matplotlib from primitive line and scatter layers.

Example

Setup

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

Data Preparation

We simulate a pathway-enrichment result (gene sets with โˆ’log10 adjusted p-values) and a two-condition differential-expression table used for the dumbbell.

np.random.seed(51)

pathways = ['T cell activation', 'Antigen processing', 'IFN-gamma response',
            'IL-6/JAK/STAT3', 'TNF-alpha signaling', 'Oxidative phosphorylation',
            'Fatty acid metabolism', 'DNA repair', 'Apoptosis', 'mTORC1 signaling']
enrichment = pd.DataFrame({
    'Pathway': pathways,
    'NegLogFDR': np.array([7.8, 6.4, 5.9, 4.7, 4.1, 3.2, 2.8, 2.1, 1.6, 1.2])
}).sort_values('NegLogFDR')

deseq = pd.DataFrame({
    'Gene': ['CD274', 'IFIT1', 'CXCL10', 'MKI67', 'STAT1', 'IRF1',
             'TGFB1', 'BCL2', 'MYC', 'CD8A'],
    'log2FC_sham': np.array([-0.3, 2.1, 2.8, 0.5, 1.9, 1.6, -0.8, -1.1, 0.9, 0.4]),
    'log2FC_treat': np.array([1.4, 3.9, 4.6, -1.2, 3.3, 2.9, 0.3, -2.6, -1.8, 1.1])
})

Visualization

Horizontal Lollipop Plot

A ranked lollipop is drawn with hlines (the stick) and plot markers (the candy); significant terms can be highlighted by color.

sig = enrichment['NegLogFDR'] >= 2  # -log10(FDR) >= 2  <=>  FDR <= 0.01
colors = np.where(sig, '#e63946', '#b8c4d0')

fig, ax = plt.subplots(figsize=(7, 4.8))
ax.hlines(enrichment['Pathway'], 0, enrichment['NegLogFDR'],
          color=colors, linewidth=1.6)
ax.scatter(enrichment['NegLogFDR'], enrichment['Pathway'],
           color=colors, s=55, zorder=3)
ax.axvline(2, color='0.5', linewidth=0.9, linestyle='--')
ax.text(2.08, 8.3, 'FDR = 0.01', color='0.35', fontsize=9)
ax.set_xlabel('-log$_{10}$ (adjusted p-value)')
ax.spines[['top', 'right']].set_visible(False)
plt.tight_layout()
plt.show()
Figureย 1: Pathway enrichment ranked as a horizontal lollipop plot

Vertical Lollipop Plot

The same construction rotated with vlines suits short labels and slide-friendly figures.

top8 = enrichment.tail(8)

fig, ax = plt.subplots(figsize=(6.5, 4.4))
ax.vlines(top8['Pathway'], 0, top8['NegLogFDR'],
          color='#8da0cb', linewidth=1.6)
ax.scatter(top8['Pathway'], top8['NegLogFDR'],
           color='#8da0cb', s=55, zorder=3)
ax.set_ylabel('-log$_{10}$ (adjusted p-value)')
ax.tick_params(axis='x', rotation=45)
for label in ax.get_xticklabels():
    label.set_ha('right')
ax.spines[['top', 'right']].set_visible(False)
plt.tight_layout()
plt.show()
Figureย 2: Vertical lollipop plot of the enrichment scores

Dumbbell Plot

For each gene, a connecting line spans the two condition fold changes; dots at both ends and a color per condition make direction and magnitude of change legible.

d = deseq.sort_values('log2FC_treat')

fig, ax = plt.subplots(figsize=(7, 4.8))
ax.hlines(d['Gene'], d['log2FC_sham'], d['log2FC_treat'],
          color='0.75', linewidth=2)
ax.scatter(d['log2FC_sham'], d['Gene'], color='#457b9d', s=55,
           label='Sham', zorder=3)
ax.scatter(d['log2FC_treat'], d['Gene'], color='#e63946', s=55,
           label='Treatment', zorder=3)
ax.axvline(0, color='0.4', linewidth=0.9)
ax.set_xlabel('log$_{2}$ fold change')
ax.legend(frameon=False, loc='lower right')
ax.spines[['top', 'right']].set_visible(False)
plt.tight_layout()
plt.show()
Figureย 3: Dumbbell plot comparing log2 fold changes between two conditions

References

  1. Hunter, J. D. (2007). Matplotlib: A 2D graphics environment. Computing in Science & Engineering, 9(3), 90-95.
  2. From Data to Viz. Lollipop plot. https://www.data-to-viz.com/graph/lollipop.html