关于 plotly-interactive-plots
Plotly Interactive Plots 是使用 Plotly Express 和 graph_objects 创建交互式网页图表的 Python 技能,适用于数据分析人员和研究者。它涵盖数据探索、自定义布局,以及 HTML 和静态图像导出。
主要功能
- 01使用 px 创建面向 DataFrame 的图表
- 02使用 go 精细控制轨迹和布局
- 03支持悬停、缩放、平移和点击
- 04支持 3D、动画及专用图表
- 05创建子图并导出 HTML 或图像
适用场景
- 01通过悬停详情探索科学数据
- 02构建多面板剂量反应仪表板
- 03以 HTML 分享交互式图表
- 04导出用于发表的图表
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jaechang-hits/ sciagent-skills --skill plotly-interactive-plots安装后在 Claude Code 中输入/plotly-interactive-plots确认已加载。
详细介绍
Plotly 交互式图表
概述
Plotly 是一个 Python 库,可用于生成基于 HTML 和 JavaScript 的交互式网页就绪图表。它提供两个互补的 API:plotly.express(px)提供面向 DataFrame 的高级接口,可用一行代码生成常见图表类型;plotly.graph_objects(go)则可对每条轨迹、坐标轴和布局属性进行精细控制。图表默认完全交互,支持悬停提示、缩放、平移和点击事件;还可嵌入网页、Jupyter Notebook,或使用 Dash 框架构建成 Web 应用。
适用场景
- 需要悬停提示来显示基因名称、p 值或样本元数据,同时避免静态图表过于杂乱。
- 正在为剂量-反应曲线、患者队列或多条件比较构建多面板交互式仪表板。
- 想要将图表分享为独立的 HTML 文件,让非程序员也能在浏览器中查看和交互。
- 需要用于结构生物学、构象景观或高维数据 PCA 的 3D 散点图或曲面图。
- 正在创建基因表达热图或相关性矩阵,用户需要缩放查看特定基因簇。
- 需要使用动画帧展示时间序列或治疗响应轨迹。
- 如果需要以尽可能少的代码自动进行统计汇总(置信区间、回归拟合),请改用
seaborn。 - 如果需要对每个坐标轴元素进行精细控制,以便按照期刊的精确规范制作适合印刷发表的图表,请使用
matplotlib。
前提条件
- Python 软件包:
plotly、kaleido(静态图像导出)、pandas、numpy - 数据要求:pandas DataFrame 或 NumPy 数组;使用
px时,长格式(整洁)数据效果最佳 - 环境:Jupyter Lab/Notebook(内联渲染),或保存为 HTML 以便在浏览器中显示
pip install plotly kaleido pandas numpy
如需在 Jupyter Lab 中内联渲染(若未自动启用):
pip install "jupyterlab>=3" ipywidgets
快速入门
import plotly.express as px
import pandas as pd
# Gene expression scatter with hover info
df = pd.DataFrame({
"log2FC": [-3.1, 0.2, 1.8, 2.5, -0.5, 4.1],
"neg_log10_padj": [8.2, 0.4, 2.1, 6.8, 0.1, 9.3],
"gene": ["BRCA1", "MYC", "TP53", "EGFR", "CDKN1A", "KRAS"],
"significance": ["sig", "ns", "ns", "sig", "ns", "sig"],
})
fig = px.scatter(
df, x="log2FC", y="neg_log10_padj",
color="significance", hover_name="gene",
title="Volcano Plot — Treatment vs Control",
)
fig.show()
核心 API
模块 1:px 散点图和折线图——关系图
px.scatter() 和 px.line() 将 DataFrame 列映射为视觉编码(颜色、符号、大小),并根据 hover_data 自动填充悬停提示。
import plotly.express as px
import pandas as pd
import numpy as np
# Dose-response scatter: color by drug, symbol by cell line
np.random.seed(42)
df = pd.DataFrame({
"dose_uM": np.tile([0.01, 0.1, 1, 10, 100], 4),
"viability": np.clip(np.random.normal(
[100, 90, 70, 40, 10] * 4, 5), 0, 110),
"drug": ["DrugA"] * 5 + ["DrugA"] * 5 + ["DrugB"] * 5 + ["DrugB"] * 5,
"cell_line": ["HCT116"] * 10 + ["MCF7"] * 10,
"replicate": np.tile([1, 2, 3, 4, 5], 4),
})
fig = px.scatter(
df, x="dose_uM", y="viability",
color="drug", symbol="cell_line",
log_x=True,
hover_data={"replicate": True, "dose_uM": ":.2f"},
labels={"viability": "Cell Viability (%)", "dose_uM": "Dose (µM)"},
title="Dose-Response by Drug and Cell Line",
)
fig.show()
print(f"Figure has {len(fig.data)} traces")
# Time-course gene expression line plot
time_df = pd.DataFrame({
"hour": list(range(0, 25, 4)) * 3,
"expression": [1.0, 1.8, 3.2, 4.5, 3.8, 2.1, 1.2,
1.0, 2.5, 5.1, 6.8, 5.5, 3.2, 1.8,
1.0, 1.1, 1.0, 1.2, 1.1, 1.0, 0.9],
"gene": ["MYC"] * 7 + ["EGFR"] * 7 + ["GAPDH"] * 7,
})
fig = px.line(
time_df, x="hour", y="expression",
color="gene", markers=True,
labels={"expression": "Relative Expression (log2)", "hour": "Time (h)"},
title="Time-Course Gene Expression",
)
fig.update_traces(line=dict(width=2.5), marker=dict(size=8))
fig.show()
模块 2:px 统计图——分布与类别
px.box()、px.violin()、px.histogram() 和 px.strip() 可生成适合发表的分布汇总图,并内置分组功能。
import plotly.express as px
import pandas as pd
import numpy as np
# Violin + strip overlay: expression by cell type
np.random.seed(7)
n = 60
cell_data = pd.DataFrame({
"expression": np.concatenate([
np.random.normal(4.2, 0.8, n),
np.random.normal(6.5, 1.2, n),
np.random.normal(2.8, 0.6, n),
]),
"cell_type": ["T cell"] * n + ["B cell"] * n + ["NK cell"] * n,
"patient_id": np.tile([f"P{i:02d}" for i in range(1, 11)], 18),
})
fig = px.violin(
cell_data, x="cell_type", y="expression",
color="cell_type", box=True, points="all",
hover_data=["patient_id"],
labels={"expression": "CD3E Expression (log2 CPM)"},
title="CD3E Expression Across Cell Types",
)
fig.update_traces(jitter=0.3, pointpos=-1.5)
fig.show()
print(f"Cells per type: {cell_data.groupby('cell_type').size().to_dict()}")
# Histogram with rug: distribution of fold changes
fc_df = pd.DataFrame({
"log2FC": np.concatenate([
np.random.normal(0.1, 0.8, 500), # not DE genes
np.random.normal(2.5, 0.4, 50), # upregulated
np.random.normal(-2.3, 0.4, 40), # downregulated
]),
"category": ["background"] * 500 + ["up"] * 50 + ["down"] * 40,
})
fig = px.histogram(
fc_df, x="log2FC", color="category",
nbins=60, barmode="overlay", opacity=0.7,
marginal="rug",
labels={"log2FC": "log2 Fold Change", "count": "Gene Count"},
title="Distribution of Fold Changes (DESeq2 Results)",
color_discrete_map={"background": "gray", "up": "crimson", "down": "steelblue"},
)
fig.show()
模块 3:px 热图和矩阵——基因表达与相关性
px.imshow() 可将二维数组或 DataFrame 渲染为颜色编码矩阵,非常适合表达热图和相关性矩阵。
import plotly.express as px
import pandas as pd
import numpy as np
# Gene expression heatmap (genes × samples)
np.random.seed(12)
genes = [f"Gene_{g}" for g in ["BRCA1", "TP53", "EGFR", "MYC", "KRAS",
"CDKN1A", "RB1", "PTEN", "VHL", "APC"]]
samples = [f"S{i:02d}" for i in range(1, 9)]
expr_matrix = pd.DataFrame(
np.random.normal(0, 1.5, (10, 8)) +
np.array([2, -1, 3, -2, 1, -3, 0, 2, -1, 3]).reshape(-1, 1),
index=genes, columns=samples,
)
fig = px.imshow(
expr_matrix,
color_continuous_scale="RdBu_r",
color_continuous_midpoint=0,
aspect="auto",
labels={"color": "log2 Expression (z-score)"},
title="Gene Expression Heatmap",
)
fig.update_xaxes(side="top")
fig.update_layout(width=600, height=500)
fig.show()
print(f"Heatmap shape: {expr_matrix.shape} (genes × samples)")
# Correlation matrix heatmap
from itertools import combinations
markers = ["IL6", "TNF", "CXCL10", "IFNg", "IL10", "IL1B", "CCL2", "IL17A"]
np.random.seed(3)
raw = np.random.multivariate_normal(
mean=np.zeros(8),
cov=np.eye(8) * 0.3 + 0.7,
size=80,
)
corr_df = pd.DataFrame(raw, columns=markers).corr()
fig = px.imshow(
corr_df,
color_continuous_scale="RdBu_r",
color_continuous_midpoint=0,
zmin=-1, zmax=1,
text_auto=".2f",
title="Cytokine Correlation Matrix (n=80 patients)",
)
fig.update_traces(textfont_size=10)
fig.show()
模块 4:go 图形对象——全面控制轨迹
plotly.graph_objects 可精细控制每项轨迹属性:标记符号、误差条、填充区域和多轨迹布局。当 px 无法满足所需的灵活性时,它必不可少。
import plotly.graph_objects as go
import numpy as np
# Volcano plot built from scratch with go.Scatter
np.random.seed(99)
n_genes = 5000
log2fc = np.random.normal(0, 1.2, n_genes)
pval = np.random.uniform(0, 1, n_genes) ** 2 # skew toward low p-values
neg_log10_p = -np.log10(pval + 1e-300)
gene_names = [f"Gene_{i:04d}" for i in range(n_genes)]
# Classify genes
sig_mask = (np.abs(log2fc) > 1.5) & (neg_log10_p > 3)
up_mask = sig_mask & (log2fc > 0)
down_mask = sig_mask & (log2fc < 0)
ns_mask = ~sig_mask
fig = go.Figure()
# Non-significant background
fig.add_trace(go.Scatter(
x=log2fc[ns_mask], y=neg_log10_p[ns_mask],
mode="markers",
name="Not significant",
marker=dict(color="lightgray", size=4, opacity=0.5),
text=[gene_names[i] for i in np.where(ns_mask)[0]],
hovertemplate="<b>%{text}</b><br>log2FC: %{x:.2f}<br>-log10(p): %{y:.2f}<extra></extra>",
))
# Upregulated
fig.add_trace(go.Scatter(
x=log2fc[up_mask], y=neg_log10_p[up_mask],
mode="markers",
name=f"Up ({up_mask.sum()} genes)",
marker=dict(color="crimson", size=7, opacity=0.8),
text=[gene_names[i] for i in np.where(up_mask)[0]],
hovertemplate="<b>%{text}</b><br>log2FC: %{x:.2f}<br>-log10(p): %{y:.2f}<extra></extra>",
))
# Downregulated
fig.add_trace(go.Scatter(
x=log2fc[down_mask], y=neg_log10_p[down_mask],
mode="markers",
name=f"Down ({down_mask.sum()} genes)",
marker=dict(color="steelblue", size=7, opacity=0.8),
text=[gene_names[i] for i in np.where(down_mask)[0]],
hovertemplate="<b>%{text}</b><br>log2FC: %{x:.2f}<br>-log10(p): %{y:.2f}<extra></extra>",
))
# Threshold lines
fig.add_hline(y=3, line_dash="dash", line_color="black", line_width=1)
fig.add_vline(x=1.5, line_dash="dash", line_color="black", line_width=1)
fig.add_vline(x=-1.5, line_dash="dash", line_color="black", line_width=1)
fig.update_layout(
title="Volcano Plot (Treatment vs Control, n=5000 genes)",
xaxis_title="log2 Fold Change",
yaxis_title="-log10(adjusted p-value)",
legend=dict(x=0.01, y=0.99),
width=750, height=550,
)
fig.show()
print(f"Up: {up_mask.sum()}, Down: {down_mask.sum()}, NS: {ns_mask.sum()}")
# Bar chart with error bars: mean ± SEM per treatment group
groups = ["Vehicle", "DrugA 1µM", "DrugA 10µM", "DrugB 1µM", "DrugB 10µM"]
means = [100.0, 82.3, 54.7, 91.2, 68.5]
sems = [3.2, 4.1, 3.8, 3.5, 4.7]
fig = go.Figure(go.Bar(
x=groups, y=means,
error_y=dict(type="data", array=sems, visible=True),
marker_color=["gray", "lightsalmon", "crimson", "lightblue", "steelblue"],
hovertemplate="%{x}<br>Mean: %{y:.1f}%<br>SEM: ±%{error_y.array:.1f}%<extra></extra>",
))
fig.update_layout(
title="Cell Viability by Treatment (Mean ± SEM, n=6)",
yaxis_title="Viability (%)", yaxis_range=[0, 120],
xaxis_title="Treatment Group",
showlegend=False,
)
fig.show()
模块 5:3D 和专用图表
Plotly 支持 3D 散点图、曲面图、平行坐标图和树状图,这些图表类型在 seaborn 或标准 matplotlib 中不可用。
import plotly.express as px
import numpy as np
import pandas as pd
# 3D PCA scatter: cell clusters in embedding space
np.random.seed(42)
n_per_cluster = 80
cluster_centers = {"T cell": [3, 2, 1], "B cell": [-3, 1, 2], "Monocyte": [0, -3, -1]}
records = []
for ctype, center in cluster_centers.items():
coords = np.random.normal(center, 0.8, (n_per_cluster, 3))
for row in coords:
records.append({
"PC1": row[0], "PC2": row[1], "PC3": row[2],
"cell_type": ctype,
"score": np.random.uniform(0.5, 1.0),
})
pca_df = pd.DataFrame(records)
fig = px.scatter_3d(
pca_df, x="PC1", y="PC2", z="PC3",
color="cell_type", size="score", opacity=0.7,
hover_data={"score": ":.3f"},
title="3D PCA — Single-Cell Transcriptomics",
)
fig.update_traces(marker=dict(sizeref=0.04))
fig.show()
print(f"Total cells: {len(pca_df)}, clusters: {pca_df['cell_type'].nunique()}")
import plotly.graph_objects as go
import numpy as np
import pandas as pd
# Parallel coordinates: multi-parameter drug screen
np.random.seed(5)
n_compounds = 200
drug_df = pd.DataFrame({
"MW": np.random.normal(380, 60, n_compounds),
"logP": np.random.uniform(-1, 6, n_compounds),
"HBA": np.random.randint(2, 10, n_compounds),
"HBD": np.random.randint(0, 6, n_compounds),
"IC50_nM": np.random.lognormal(4, 1.5, n_compounds),
"selectivity": np.random.uniform(1, 100, n_compounds),
})
fig = px.parallel_coordinates(
drug_df,
color="IC50_nM",
color_continuous_scale="RdYlGn_r",
dimensions=["MW", "logP", "HBA", "HBD", "IC50_nM", "selectivity"],
labels={
"MW": "MW (Da)", "logP": "logP",
"HBA": "H-Bond Acceptors", "HBD": "H-Bond Donors",
"IC50_nM": "IC50 (nM)", "selectivity": "Selectivity Index",
},
title="Drug Candidate Properties — Parallel Coordinates",
)
fig.show()
模块 6:子图与导出
make_subplots() 可创建多面板布局,支持共享坐标轴、混合图表类型,以及每个面板使用独立轨迹。fig.write_html() 可导出交互式图表;fig.write_image() 可通过 kaleido 导出静态文件。
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import numpy as np
# Two-panel: raw data + summary statistics
np.random.seed(77)
doses = [0.01, 0.1, 1, 10, 100]
drugs = {"DrugA": {"EC50": 1.0, "hill": 1.5}, "DrugB": {"EC50": 8.0, "hill": 0.9}}
def hill_curve(dose, ec50, hill, top=100, bottom=0):
return bottom + (top - bottom) / (1 + (ec50 / dose) ** hill)
fig = make_subplots(
rows=1, cols=2,
subplot_titles=["Dose-Response Curves", "IC50 Comparison"],
shared_yaxes=False,
)
colors = {"DrugA": "crimson", "DrugB": "steelblue"}
ic50_values = []
for drug, params in drugs.items():
# Smooth fit curve
x_fit = np.logspace(-2, 2, 200)
y_fit = hill_curve(x_fit, params["EC50"], params["hill"])
fig.add_trace(go.Scatter(
x=x_fit, y=y_fit, mode="lines",
name=f"{drug} fit", line=dict(color=colors[drug], width=2.5),
), row=1, col=1)
# Noisy data points
y_data = [hill_curve(d, params["EC50"], params["hill"]) +
np.random.normal(0, 4) for d in doses]
fig.add_trace(go.Scatter(
x=doses, y=y_data, mode="markers",
name=f"{drug} data", marker=dict(color=colors[drug], size=9),
showlegend=False,
), row=1, col=1)
ic50_values.append(params["EC50"])
# Bar chart of IC50 values
fig.add_trace(go.Bar(
x=list(drugs.keys()), y=ic50_values,
marker_color=list(colors.values()),
showlegend=False,
hovertemplate="%{x}<br>IC50: %{y:.2f} µM<extra></extra>",
), row=1, col=2)
fig.update_xaxes(type="log", title_text="Dose (µM)", row=1, col=1)
fig.update_yaxes(title_text="Viability (%)", row=1, col=1)
fig.update_xaxes(title_text="Drug", row=1, col=2)
fig.update_yaxes(title_text="IC50 (µM)", row=1, col=2)
fig.update_layout(title="Dose-Response Dashboard", height=450, width=850)
fig.show()
print(f"Subplots: {len(fig.data)} traces across 2 panels")
# Export to HTML (interactive) and PNG (static)
# Requires: pip install kaleido
fig.write_html("dose_response_dashboard.html")
print("Saved: dose_response_dashboard.html (interactive, shareable)")
fig.write_image("dose_response_dashboard.png", width=1200, height=600, scale=2)
print("Saved: dose_response_dashboard.png (300 DPI equivalent with scale=2)")
fig.write_image("dose_response_dashboard.svg")
print("Saved: dose_response_dashboard.svg (vector, editable in Inkscape/Illustrator)")
常见工作流
工作流 1:带基因注释的交互式火山图
目标:根据 DESeq2 结果绘制完全注释的火山图,包含基因名称悬停提示、阈值线和高亮的命中标签,并以 HTML 格式分享。
import plotly.graph_objects as go
import pandas as pd
import numpy as np
# Simulate DESeq2 output (replace with pd.read_csv("deseq2_results.csv"))
np.random.seed(42)
n = 3000
df = pd.DataFrame({
"gene": [f"GENE_{i:04d}" for i in range(n)],
"log2FC": np.random.normal(0, 1.0, n),
"padj": np.clip(np.random.exponential(0.1, n), 1e-20, 1.0),
"baseMean": np.random.lognormal(5, 1.5, n),
})
# Inject some hits
df.loc[:20, "log2FC"] = np.random.uniform(2.5, 5, 21)
df.loc[:20, "padj"] = np.random.uniform(1e-15, 1e-5, 21)
df.loc[21:35, "log2FC"] = np.random.uniform(-4, -2, 15)
df.loc[21:35, "padj"] = np.random.uniform(1e-12, 1e-4, 15)
df["neg_log10_padj"] = -np.log10(df["padj"].clip(1e-300))
# Classify
FC_THRESH, P_THRESH = 1.5, 2.0 # |log2FC| > 1.5, -log10(padj) > 2
df["category"] = "NS"
df.loc[(df["log2FC"] > FC_THRESH) & (df["neg_log10_padj"] > P_THRESH), "category"] = "Up"
df.loc[(df["log2FC"] < -FC_THRESH) & (df["neg_log10_padj"] > P_THRESH), "category"] = "Down"
color_map = {"NS": "lightgray", "Up": "crimson", "Down": "steelblue"}
size_map = {"NS": 4, "Up": 7, "Down": 7}
opacity_map = {"NS": 0.4, "Up": 0.85, "Down": 0.85}
fig = go.Figure()
for cat in ["NS", "Up", "Down"]:
sub = df[df["category"] == cat]
fig.add_trace(go.Scatter(
x=sub["log2FC"], y=sub["neg_log10_padj"],
mode="markers",
name=f"{cat} (n={len(sub)})",
marker=dict(
color=color_map[cat],
size=size_map[cat],
opacity=opacity_map[cat],
),
customdata=sub[["gene", "padj", "baseMean"]].values,
hovertemplate=(
"<b>%{customdata[0]}</b><br>"
"log2FC: %{x:.3f}<br>"
"padj: %{customdata[1]:.2e}<br>"
"baseMean: %{customdata[2]:.1f}<extra></extra>"
),
))
# Threshold lines
fig.add_hline(y=P_THRESH, line_dash="dot", line_color="black", line_width=1.2,
annotation_text=f"padj=0.01", annotation_position="right")
fig.add_vline(x=FC_THRESH, line_dash="dot", line_color="black", line_width=1.2)
fig.add_vline(x=-FC_THRESH, line_dash="dot", line_color="black", line_width=1.2)
# Label top 5 upregulated hits by significance
top_up = df[df["category"] == "Up"].nlargest(5, "neg_log10_padj")
for _, row in top_up.iterrows():
fig.add_annotation(
x=row["log2FC"], y=row["neg_log10_padj"],
text=row["gene"], showarrow=True,
arrowhead=2, arrowsize=1, arrowcolor="crimson",
font=dict(size=9, color="crimson"),
xshift=8, yshift=5,
)
fig.update_layout(
title="Volcano Plot — Treatment vs Control (DESeq2)",
xaxis_title="log2 Fold Change",
yaxis_title="-log10(adjusted p-value)",
legend=dict(x=0.01, y=0.99, bordercolor="lightgray", borderwidth=1),
width=800, height=560,
plot_bgcolor="white",
)
fig.update_xaxes(showgrid=True, gridcolor="lightgray", zeroline=True, zerolinecolor="darkgray")
fig.update_yaxes(showgrid=True, gridcolor="lightgray")
fig.write_html("volcano_interactive.html")
print(f"Up: {(df.category=='Up').sum()}, Down: {(df.category=='Down').sum()}")
print("Saved: volcano_interactive.html")
工作流 2:使用 make_subplots 构建多面板剂量-反应仪表板
目标:在网格布局中展示多种药物在不同细胞系中的剂量-反应曲线,并使用共享色标和一致的格式。
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import numpy as np
import pandas as pd
# Simulated IC50 data for 3 drugs × 3 cell lines
np.random.seed(10)
drugs = ["DrugA", "DrugB", "DrugC"]
cell_lines = ["HCT116", "MCF7", "A549"]
doses = np.logspace(-2, 2, 7) # 0.01 to 100 µM
def hill(x, ec50, hill_n, top=100, bottom=0):
return bottom + (top - bottom) / (1 + (ec50 / x) ** hill_n)
ec50_table = {
("DrugA", "HCT116"): 0.5, ("DrugA", "MCF7"): 2.0, ("DrugA", "A549"): 8.0,
("DrugB", "HCT116"): 5.0, ("DrugB", "MCF7"): 0.8, ("DrugB", "A549"): 15.0,
("DrugC", "HCT116"): 12.0, ("DrugC", "MCF7"): 6.0, ("DrugC", "A549"): 1.2,
}
palette = px_colors = ["#EF553B", "#636EFA", "#00CC96", "#AB63FA", "#FFA15A",
"#19D3F3", "#FF6692", "#B6E880", "#FF97FF"]
fig = make_subplots(
rows=len(drugs), cols=len(cell_lines),
subplot_titles=[f"{d} / {c}" for d in drugs for c in cell_lines],
shared_xaxes=True, shared_yaxes=True,
vertical_spacing=0.08, horizontal_spacing=0.04,
)
for r, drug in enumerate(drugs, start=1):
for c, cell_line in enumerate(cell_lines, start=1):
ec50 = ec50_table[(drug, cell_line)]
x_fit = np.logspace(-2, 2, 200)
y_fit = hill(x_fit, ec50, hill_n=1.5)
# Noisy replicate data
y_data = np.array([hill(d, ec50, 1.5) + np.random.normal(0, 5) for d in doses])
color = palette[(r - 1) * len(cell_lines) + (c - 1)]
show_legend = (c == 1 and r == 1)
fig.add_trace(go.Scatter(
x=x_fit, y=y_fit, mode="lines",
line=dict(color=color, width=2),
name=f"{drug}/{cell_line}",
showlegend=False,
hovertemplate=f"{drug} in {cell_line}<br>Dose: %{{x:.2f}} µM<br>Viability: %{{y:.1f}}%<extra></extra>",
), row=r, col=c)
fig.add_trace(go.Scatter(
x=doses, y=np.clip(y_data, 0, 110), mode="markers",
marker=dict(color=color, size=7, opacity=0.8),
showlegend=False,
hovertemplate=f"Measured<br>Dose: %{{x:.2f}} µM<br>Viability: %{{y:.1f}}%<extra></extra>",
), row=r, col=c)
# IC50 annotation
fig.add_annotation(
x=np.log10(ec50), y=50,
text=f"IC50={ec50:.1f}µM",
font=dict(size=8), showarrow=False,
xref=f"x{(r-1)*len(cell_lines)+c if (r-1)*len(cell_lines)+c > 1 else ''}",
yref=f"y{(r-1)*len(cell_lines)+c if (r-1)*len(cell_lines)+c > 1 else ''}",
)
# Apply log scale to all x-axes
for i in range(1, len(drugs) * len(cell_lines) + 1):
axis_key = f"xaxis{i if i > 1 else ''}"
fig.layout[axis_key].update(type="log", title_text="Dose (µM)" if i > 6 else "")
for i in range(1, len(drugs) * len(cell_lines) + 1):
axis_key = f"yaxis{i if i > 1 else ''}"
fig.layout[axis_key].update(range=[-5, 115],
title_text="Viability (%)" if i in [1, 4, 7] else "")
fig.update_layout(
title="Dose-Response Dashboard — 3 Drugs × 3 Cell Lines",
height=700, width=900,
)
fig.write_html("dose_response_dashboard.html")
print("Saved: dose_response_dashboard.html")
print(f"Grid: {len(drugs)} drugs × {len(cell_lines)} cell lines = {len(drugs)*len(cell_lines)} panels")
关键参数
| 参数 | 模块 / 函数 | 默认值 | 范围 / 选项 | 效果 |
|---|---|---|---|---|
color | px.* | None | 列名 | 将 DataFrame 列映射为轨迹颜色;自动分配调色板 |
hover_data | px.* | {} | 字典或列名列表 | 在悬停提示框中显示额外的列 |
log_x / log_y | px.* | False | True、False | 对 x 轴或 y 轴应用 log10 刻度 |
facet_col / facet_row | px.* | None | 列名 | 按分类变量拆分为子图网格 |
color_continuous_scale | px.imshow、px.scatter | "plasma" | "RdBu_r"、"Viridis"、"Hot" 等 | 连续颜色映射所用的色图 |
color_continuous_midpoint | px.imshow | None | 任意数值 | 将发散色图的中心设为此值(z 分数请使用 0) |
barmode | px.histogram、px.bar | "relative" | "relative"、"overlay"、"group" | 多个条形轨迹的显示方式 |
opacity | go.Scatter、px.* | 1.0 | 0.0–1.0 | 点 / 条形的透明度 |
size / sizeref | go.Scatter | 6 / 自动 | 正数 | 标记大小;sizeref 用于统一不同轨迹间的大小 |
line_dash | fig.add_hline、go.Scatter | "solid" | "solid"、"dash"、"dot"、"dashdot" | 参考线和轨迹的线条样式 |
shared_xaxes / shared_yaxes | make_subplots | False | True、False、"rows"、"cols" | 关联各子图面板的坐标轴 |
scale | fig.write_image | 1 | 1–4 | PNG 导出的分辨率倍数(约 150 DPI 时请使用 2) |
最佳实践
-
DataFrame 数据优先使用
px,需要组合多种轨迹时再使用go。 80% 的绘图可使用px.scatter()及其同类函数。若需在同一图中使用不同类型的轨迹(例如散点 + 填充区域),或需要精细控制每条轨迹,则改用go。# Correct: px for simple grouped plots fig = px.box(df, x="treatment", y="expression", color="genotype") # Correct: go when px cannot express the structure fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=y_upper, fill="tonexty", ...)) fig.add_trace(go.Scatter(x=x, y=y_lower, ...)) -
科学图表务必设置
hovertemplate。 默认提示框会显示不带单位或基因名称的原始坐标。使用customdata编写自定义模板,可提供完整的生物学背景信息。fig.add_trace(go.Scatter( customdata=df[["gene", "padj"]].values, hovertemplate="<b>%{customdata[0]}</b><br>padj: %{customdata[1]:.2e}<extra></extra>", )) -
分享时导出 HTML,投稿时导出 PNG/SVG。
fig.write_html()会生成无需外部依赖的独立文件。使用write_image()时,请将scale设为2或更高,以达到满足印刷要求的分辨率。 -
不要在批处理脚本中使用
fig.show()。 在非交互环境(CI、HPC、cron 作业)中,fig.show()可能会打开浏览器窗口,也可能失败。请仅使用write_html()或write_image()。 -
z 分数数据使用发散调色板时,请设置
color_continuous_midpoint=0。 若不设置,中间颜色将默认为数据中点,而非零,从而错误呈现对称的倍数变化或相关性。fig = px.imshow(corr_matrix, color_continuous_scale="RdBu_r", color_continuous_midpoint=0, zmin=-1, zmax=1) -
出版用图请设置
plot_bgcolor="white"。 Plotly 默认使用浅灰色网格背景。在大多数科学场景中,白色背景配以浅色网格线更加清晰。fig.update_layout(plot_bgcolor="white") fig.update_xaxes(showgrid=True, gridcolor="lightgray") fig.update_yaxes(showgrid=True, gridcolor="lightgray")
常用方案
方案:使用下拉菜单切换条件
适用场景:在一张图中叠加多个条件,并通过下拉按钮整洁地显示或隐藏各条轨迹。
import plotly.graph_objects as go
import numpy as np
conditions = ["Untreated", "DrugA", "DrugB"]
colors = ["gray", "crimson", "steelblue"]
np.random.seed(1)
x = np.linspace(0, 24, 49)
fig = go.Figure()
for i, (cond, color) in enumerate(zip(conditions, colors)):
y = np.sin(x / 4 + i * 0.5) * (1 - i * 0.2) + np.random.normal(0, 0.05, len(x))
fig.add_trace(go.Scatter(
x=x, y=y, mode="lines+markers",
name=cond, line=dict(color=color, width=2),
visible=(i == 0), # only first trace visible initially
))
# One button per condition (shows only that trace)
buttons = []
for i, cond in enumerate(conditions):
visibility = [j == i for j in range(len(conditions))]
buttons.append(dict(label=cond, method="update",
args=[{"visible": visibility}, {"title": f"Gene Expression — {cond}"}]))
# "Show All" button
buttons.append(dict(label="Show All", method="update",
args=[{"visible": [True] * len(conditions)}, {"title": "Gene Expression — All Conditions"}]))
fig.update_layout(
updatemenus=[dict(type="dropdown", x=0.01, y=1.15, showactive=True, buttons=buttons)],
title="Gene Expression — Untreated",
xaxis_title="Time (h)", yaxis_title="Relative Expression",
)
fig.show()
方案:使用箭头标注特定命中项
适用场景:直接在图中标注离群值、药物命中项或显著基因,同时避免标注干扰其他点。
import plotly.graph_objects as go
import numpy as np
import pandas as pd
np.random.seed(33)
df = pd.DataFrame({
"x": np.random.normal(0, 1.5, 300),
"y": np.random.normal(0, 1.5, 300),
"gene": [f"G{i:03d}" for i in range(300)],
})
# Inject top hits
hits = pd.DataFrame({
"x": [3.2, -2.8, 2.5, -3.5],
"y": [4.1, 3.8, -3.2, -2.9],
"gene": ["BRCA1", "TP53", "EGFR", "KRAS"],
})
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df["x"], y=df["y"], mode="markers",
marker=dict(color="lightgray", size=5, opacity=0.6),
text=df["gene"],
hovertemplate="<b>%{text}</b><br>x: %{x:.2f}, y: %{y:.2f}<extra></extra>",
name="Background",
))
fig.add_trace(go.Scatter(
x=hits["x"], y=hits["y"], mode="markers",
marker=dict(color="crimson", size=10, symbol="diamond"),
text=hits["gene"],
hovertemplate="<b>%{text}</b> [HIT]<br>x: %{x:.2f}, y: %{y:.2f}<extra></extra>",
name="Hits",
))
for _, row in hits.iterrows():
fig.add_annotation(
x=row["x"], y=row["y"],
text=f"<b>{row['gene']}</b>",
showarrow=True, arrowhead=2, arrowwidth=1.5,
arrowcolor="crimson", font=dict(size=11, color="crimson"),
ax=25, ay=-30, # arrow offset in pixels
bgcolor="rgba(255,255,255,0.7)", bordercolor="crimson", borderwidth=1,
)
fig.update_layout(
title="Hit Identification with Arrow Annotations",
xaxis_title="Score A", yaxis_title="Score B",
plot_bgcolor="white",
)
fig.show()
故障排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
ValueError: kaleido is required for static image export | 未安装 kaleido | pip install kaleido;使用 import kaleido 验证 |
| Jupyter Notebook 中显示空白图表 | 未配置渲染器 | 运行 import plotly.io as pio; pio.renderers.default = "notebook",或将 JupyterLab 升级到 ≥3 |
fig.show() 打开空白浏览器标签页 | 图表中没有数据,或离线渲染器出现问题 | 检查 len(fig.data) > 0;使用 pio.renderers.default = "browser" |
| 悬停提示显示的值不正确 | hovertemplate 中的 customdata 索引不匹配 | 确认 customdata 列顺序与模板中的 %{customdata[N]} 索引一致 |
| 不同轨迹的颜色分配不一致 | 类别数量不同时,px 会重新排列调色板 | 使用 color_discrete_map={"Cat1": "#color1", ...} 显式固定颜色 |
write_image 生成的 PNG 模糊 | 默认 scale=1 对印刷来说太低 | 使用 fig.write_image("fig.png", scale=2) 获得 150 DPI,或使用 scale=4 获得 300 DPI |
设置 shared_xaxes=True 后,并非所有子图的 x 轴都采用对数刻度 | 共享坐标轴只会同步范围,不会同步类型 | 遍历 fig.layout 中所有 xaxis 键,并显式设置 type="log" |
| 在浏览器中渲染大型数据集较慢 | 单个散点轨迹中的独立点过多 | 对背景噪声点进行降采样;将带标签的命中点保留为单独且规模较小的轨迹 |
fig.update_layout 不会应用于子图坐标轴 | 多面板图表使用带索引的坐标轴(xaxis2、xaxis3) | 使用 fig.update_xaxes()(应用于所有坐标轴),或显式指定 fig.layout["xaxis2"] |
相关技能
- seaborn-statistical-plots — 用于统计聚合(置信区间、回归)、以最少代码生成适合发表的静态图表,以及需要 matplotlib 级别输出的场景
- matplotlib-scientific-plotting — 用于全面控制每个图表元素、自定义布局、嵌入文本渲染,以及准备符合期刊规范的图表
- pydeseq2-differential-expression — DESeq2 结果中的火山图输出是上述交互式火山图工作流的主要输入
- scanpy-scrna-seq — Scanpy 的 UMAP 嵌入可通过
px.scatter_3d以 3D 交互方式可视化
参考资料
- Plotly Python 文档 — 官方 API 参考、示例库和入门指南
- Plotly Express API 参考 — 完整的
px函数签名和参数 - Plotly Graph Objects 参考 — 完整的
go轨迹和布局属性参考 - Plotly GitHub 仓库 — 源代码、问题跟踪器和变更日志
- Kaleido 静态图像导出 —
write_image()PNG/SVG/PDF 导出所需的依赖项