Skip to content

Commit 6c280d0

Browse files
feat(plotly): implement volcano-basic (#2942)
## Implementation: `volcano-basic` - plotly Implements the **plotly** version of `volcano-basic`. **File:** `plots/volcano-basic/implementations/plotly.py` **Parent Issue:** #2924 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20612779383)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent b96d60a commit 6c280d0

2 files changed

Lines changed: 195 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
""" pyplots.ai
2+
volcano-basic: Volcano Plot for Statistical Significance
3+
Library: plotly 6.5.0 | Python 3.13.11
4+
Quality: 92/100 | Created: 2025-12-31
5+
"""
6+
7+
import numpy as np
8+
import plotly.graph_objects as go
9+
10+
11+
# Data - Simulated differential gene expression results
12+
np.random.seed(42)
13+
n_genes = 500
14+
15+
# Generate log2 fold changes (centered around 0, with some extreme values)
16+
log2_fold_change = np.random.normal(0, 1.5, n_genes)
17+
18+
# Generate p-values (most non-significant, some significant)
19+
# Use a mixture: mostly high p-values, some low
20+
base_pvalues = np.random.beta(1, 3, n_genes)
21+
# Genes with larger fold changes tend to have lower p-values
22+
effect_boost = np.abs(log2_fold_change) / 5
23+
pvalues = base_pvalues * np.exp(-effect_boost * 3)
24+
pvalues = np.clip(pvalues, 1e-20, 1)
25+
26+
# Transform to -log10 scale
27+
neg_log10_pvalue = -np.log10(pvalues)
28+
29+
# Define significance thresholds
30+
fc_threshold = 1.0 # log2 fold change threshold (2-fold)
31+
pval_threshold = 0.05 # p-value threshold
32+
neg_log10_threshold = -np.log10(pval_threshold)
33+
34+
# Classify points
35+
sig_up = (log2_fold_change > fc_threshold) & (neg_log10_pvalue > neg_log10_threshold)
36+
sig_down = (log2_fold_change < -fc_threshold) & (neg_log10_pvalue > neg_log10_threshold)
37+
non_sig = ~(sig_up | sig_down)
38+
39+
# Gene names for top hits
40+
gene_names = [f"Gene_{i}" for i in range(n_genes)]
41+
42+
# Create figure
43+
fig = go.Figure()
44+
45+
# Non-significant points (gray)
46+
fig.add_trace(
47+
go.Scatter(
48+
x=log2_fold_change[non_sig],
49+
y=neg_log10_pvalue[non_sig],
50+
mode="markers",
51+
marker=dict(size=10, color="#888888", opacity=0.5),
52+
name="Not Significant",
53+
hovertemplate="%{text}<br>log2FC: %{x:.2f}<br>-log10(p): %{y:.2f}<extra></extra>",
54+
text=[gene_names[i] for i in np.where(non_sig)[0]],
55+
)
56+
)
57+
58+
# Significant down-regulated (blue)
59+
fig.add_trace(
60+
go.Scatter(
61+
x=log2_fold_change[sig_down],
62+
y=neg_log10_pvalue[sig_down],
63+
mode="markers",
64+
marker=dict(size=12, color="#306998", opacity=0.8),
65+
name="Down-regulated",
66+
hovertemplate="%{text}<br>log2FC: %{x:.2f}<br>-log10(p): %{y:.2f}<extra></extra>",
67+
text=[gene_names[i] for i in np.where(sig_down)[0]],
68+
)
69+
)
70+
71+
# Significant up-regulated (using a warm color - Python Yellow-ish orange)
72+
fig.add_trace(
73+
go.Scatter(
74+
x=log2_fold_change[sig_up],
75+
y=neg_log10_pvalue[sig_up],
76+
mode="markers",
77+
marker=dict(size=12, color="#D35400", opacity=0.8),
78+
name="Up-regulated",
79+
hovertemplate="%{text}<br>log2FC: %{x:.2f}<br>-log10(p): %{y:.2f}<extra></extra>",
80+
text=[gene_names[i] for i in np.where(sig_up)[0]],
81+
)
82+
)
83+
84+
# Horizontal threshold line (p-value = 0.05)
85+
x_range = [min(log2_fold_change) - 0.5, max(log2_fold_change) + 0.5]
86+
fig.add_trace(
87+
go.Scatter(
88+
x=x_range,
89+
y=[neg_log10_threshold, neg_log10_threshold],
90+
mode="lines",
91+
line=dict(color="#333333", width=2, dash="dash"),
92+
name=f"p = {pval_threshold}",
93+
showlegend=True,
94+
)
95+
)
96+
97+
# Vertical threshold lines (fold change = ±1)
98+
y_range = [0, max(neg_log10_pvalue) * 1.05]
99+
fig.add_trace(
100+
go.Scatter(
101+
x=[-fc_threshold, -fc_threshold],
102+
y=y_range,
103+
mode="lines",
104+
line=dict(color="#333333", width=2, dash="dash"),
105+
name=f"log2FC = -{fc_threshold}",
106+
showlegend=False,
107+
)
108+
)
109+
fig.add_trace(
110+
go.Scatter(
111+
x=[fc_threshold, fc_threshold],
112+
y=y_range,
113+
mode="lines",
114+
line=dict(color="#333333", width=2, dash="dash"),
115+
name=f"log2FC = {fc_threshold}",
116+
showlegend=True,
117+
)
118+
)
119+
120+
# Label top significant genes
121+
top_indices = np.argsort(neg_log10_pvalue)[-5:] # Top 5 most significant
122+
annotations = []
123+
for idx in top_indices:
124+
if sig_up[idx] or sig_down[idx]:
125+
annotations.append(
126+
dict(
127+
x=log2_fold_change[idx],
128+
y=neg_log10_pvalue[idx],
129+
text=gene_names[idx],
130+
showarrow=True,
131+
arrowhead=2,
132+
arrowsize=1,
133+
arrowwidth=1.5,
134+
ax=30,
135+
ay=-30,
136+
font=dict(size=16, color="#333333"),
137+
)
138+
)
139+
140+
# Update layout
141+
fig.update_layout(
142+
title=dict(text="volcano-basic · plotly · pyplots.ai", font=dict(size=28), x=0.5, xanchor="center"),
143+
xaxis=dict(
144+
title=dict(text="log₂ Fold Change", font=dict(size=22)),
145+
tickfont=dict(size=18),
146+
zeroline=True,
147+
zerolinewidth=1,
148+
zerolinecolor="#CCCCCC",
149+
gridcolor="rgba(0,0,0,0.1)",
150+
),
151+
yaxis=dict(
152+
title=dict(text="-log₁₀(p-value)", font=dict(size=22)), tickfont=dict(size=18), gridcolor="rgba(0,0,0,0.1)"
153+
),
154+
template="plotly_white",
155+
legend=dict(
156+
font=dict(size=18), x=0.02, y=0.98, bgcolor="rgba(255,255,255,0.8)", bordercolor="#CCCCCC", borderwidth=1
157+
),
158+
annotations=annotations,
159+
margin=dict(l=80, r=40, t=80, b=80),
160+
)
161+
162+
# Save as PNG (4800 x 2700 px)
163+
fig.write_image("plot.png", width=1600, height=900, scale=3)
164+
165+
# Save interactive HTML version
166+
fig.write_html("plot.html", include_plotlyjs=True, full_html=True)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
library: plotly
2+
specification_id: volcano-basic
3+
created: '2025-12-31T05:32:16Z'
4+
updated: '2025-12-31T05:42:24Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20612779383
7+
issue: 2924
8+
python_version: 3.13.11
9+
library_version: 6.5.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/volcano-basic/plotly/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/volcano-basic/plotly/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/volcano-basic/plotly/plot.html
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- Excellent implementation of volcano plot with all required threshold lines and
17+
color coding
18+
- Proper use of Plotly hover templates showing gene name, fold change, and p-value
19+
- Clean three-color scheme (gray/blue/orange) that is colorblind accessible
20+
- Appropriate marker sizing and alpha transparency for the data density
21+
- Gene annotations with arrows for top significant hits
22+
- Both PNG and HTML outputs generated correctly
23+
weaknesses:
24+
- Legend includes threshold line entries (p = 0.05, log2FC = 1.0) which adds visual
25+
clutter; threshold lines are self-explanatory from context
26+
- Some overlap in the dense central gray point cluster could benefit from slightly
27+
smaller markers or more transparency
28+
- Could utilize more Plotly-specific features like updatemenus or rangeslider for
29+
enhanced interactivity

0 commit comments

Comments
 (0)