import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as npLollipop & Dumbbell Plot (Python)
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
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()
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()
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()
References
- Hunter, J. D. (2007). Matplotlib: A 2D graphics environment. Computing in Science & Engineering, 9(3), 90-95.
- From Data to Viz. Lollipop plot. https://www.data-to-viz.com/graph/lollipop.html
