import numpy as np
import pandas as pd
from scipy.stats import norm
rng = np.random.default_rng(1)
n = 1000
genes = [f"Gene{i+1}" for i in range(n)]
# Simulate a realistic volcano: most genes have no real effect, a minority are
# differentially expressed. Significance follows effect size / standard error, which
# is what gives a volcano its shape.
de = rng.random(n) < 0.12 # ~12% truly changed
sign = rng.choice([-1, 1], n)
effect = np.where(de, sign * (0.8 + 0.9 * rng.random(n)), 0.0) # true log2FC
se = 0.22 + 0.5 * rng.random(n) # per-gene standard error
logfc = effect + rng.normal(0, se) # observed log2FC
pval = 2 * norm.sf(np.abs(logfc / se)) # z-test p-value
neglogp = np.minimum(-np.log10(pval), 16)
df = pd.DataFrame({"logFC": logfc.round(3), "-log-pVal": neglogp.round(3)}, index=genes)
def classify(fc, p):
if fc > 1 and p > 1.3: return "Increased"
if fc < -1 and p > 1.3: return "Decreased"
return "NoChange"
group = [classify(fc, p) for fc, p in zip(df["logFC"], df["-log-pVal"])]
fold = (2 ** df["logFC"].abs()).round(2)Interactive volcano plots in Python with canvasxpress
A volcano plot is the workhorse of differential-expression analysis: log fold change on x, statistical significance on y, and the interesting genes pushed to the top corners. Here we build one in Python with canvasxpress — interactive (zoom, hover, filter) and reproducible: the whole figure is one portable specification you can store and re-render from Python, R or JavaScript.
Install
pip install canvasxpressA differential-expression table
Swap this simulated block for your own results — the output of any DE pipeline works, as long as you have a log fold change and a significance value per gene.
Build the spec
canvasxpress takes a data object (a value matrix plus annotations) and a config. We put the two measurements as the variable’s samples and carry the regulation call and fold change as variable annotations (z).
from canvasxpress.canvas import CanvasXpress
from canvasxpress.render.jupyter import CXNoteBook
from IPython.display import display
data = {
# Genes are the variables (rows); each row holds [logFC, -log-pVal].
"y": {"vars": genes, "smps": ["logFC", "-log-pVal"],
"data": [[fc, p] for fc, p in zip(df["logFC"], df["-log-pVal"])]},
"z": {"Group": group, "FC": fold.tolist()},
}
config = {
"graphType": "Scatter2D",
"title": "Publication-ready volcano plot",
"xAxis": ["logFC"], "yAxis": ["-log-pVal"],
"xAxisTitle": "log2 fold change", "yAxisTitle": "-log10 p-value",
"colorBy": "Group",
"colorKey": {"Group": {"Increased": "rgba(197,27,38,0.75)",
"Decreased": "rgba(33,102,172,0.75)",
"NoChange": "rgba(150,150,150,0.35)"}},
"sizeBy": "FC", "sizes": [3, 6, 9, 12, 15],
"showDecorations": True,
"decorations": {"line": [
{"color": "rgba(120,120,120,0.8)", "width": 1, "x": 1},
{"color": "rgba(120,120,120,0.8)", "width": 1, "x": -1},
{"color": "rgba(120,120,120,0.8)", "width": 1, "y": 1.301},
]},
}
cx = CanvasXpress(render_to="volcano", data=data, config=config, width=650, height=500)
for part in CXNoteBook(cx).render():
display(part)The chart is interactive — hover a point for its gene and values, drag to zoom, filter by regulation group in the toolbar. The dashed guides mark the ±1 log2FC and p = 0.05 cutoffs.
Why “reproducible”
Everything above serializes to one JSON specification — the data and the config. That spec is portable: the identical figure renders from the R (canvasXpress) and JavaScript packages, and it can be validated against a published schema and re-rendered later, so a figure in a paper can be regenerated and audited rather than just viewed.
Next
- Gallery of examples: https://canvasxpress.org/examples.html
- Coming from Plotly/Matplotlib: https://canvasxpress.org/migrate.html
- Package: https://pypi.org/project/canvasxpress/