mcpField
P
plotly-interactive-plots

plotly-interactive-plots

github.com/jaechang-hits数据与数据库38471 次安装6 天前更新
代码仓库

关于 plotly-interactive-plots

Plotly Interactive Plots 是使用 Plotly Express 和 graph_objects 创建交互式网页图表的 Python 技能,适用于数据分析人员和研究者。它涵盖数据探索、自定义布局,以及 HTML 和静态图像导出。

主要功能

  1. 01使用 px 创建面向 DataFrame 的图表
  2. 02使用 go 精细控制轨迹和布局
  3. 03支持悬停、缩放、平移和点击
  4. 04支持 3D、动画及专用图表
  5. 05创建子图并导出 HTML 或图像

适用场景

  1. 01通过悬停详情探索科学数据
  2. 02构建多面板剂量反应仪表板
  3. 03以 HTML 分享交互式图表
  4. 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")

关键参数

参数模块 / 函数默认值范围 / 选项效果
colorpx.*None列名将 DataFrame 列映射为轨迹颜色;自动分配调色板
hover_datapx.*{}字典或列名列表在悬停提示框中显示额外的列
log_x / log_ypx.*FalseTrue、False对 x 轴或 y 轴应用 log10 刻度
facet_col / facet_rowpx.*None列名按分类变量拆分为子图网格
color_continuous_scalepx.imshow、px.scatter"plasma""RdBu_r"、"Viridis"、"Hot" 等连续颜色映射所用的色图
color_continuous_midpointpx.imshowNone任意数值将发散色图的中心设为此值(z 分数请使用 0)
barmodepx.histogram、px.bar"relative""relative"、"overlay"、"group"多个条形轨迹的显示方式
opacitygo.Scatter、px.*1.00.0–1.0点 / 条形的透明度
size / sizerefgo.Scatter6 / 自动正数标记大小;sizeref 用于统一不同轨迹间的大小
line_dashfig.add_hline、go.Scatter"solid""solid"、"dash"、"dot"、"dashdot"参考线和轨迹的线条样式
shared_xaxes / shared_yaxesmake_subplotsFalseTrue、False、"rows"、"cols"关联各子图面板的坐标轴
scalefig.write_image11–4PNG 导出的分辨率倍数(约 150 DPI 时请使用 2)

最佳实践

  1. 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, ...))
    
  2. 科学图表务必设置 hovertemplate。 默认提示框会显示不带单位或基因名称的原始坐标。使用 customdata 编写自定义模板,可提供完整的生物学背景信息。

    fig.add_trace(go.Scatter(
        customdata=df[["gene", "padj"]].values,
        hovertemplate="<b>%{customdata[0]}</b><br>padj: %{customdata[1]:.2e}<extra></extra>",
    ))
    
  3. 分享时导出 HTML,投稿时导出 PNG/SVG。 fig.write_html() 会生成无需外部依赖的独立文件。使用 write_image() 时,请将 scale 设为 2 或更高,以达到满足印刷要求的分辨率。

  4. 不要在批处理脚本中使用 fig.show()。 在非交互环境(CI、HPC、cron 作业)中,fig.show() 可能会打开浏览器窗口,也可能失败。请仅使用 write_html() 或 write_image()。

  5. z 分数数据使用发散调色板时,请设置 color_continuous_midpoint=0。 若不设置,中间颜色将默认为数据中点,而非零,从而错误呈现对称的倍数变化或相关性。

    fig = px.imshow(corr_matrix, color_continuous_scale="RdBu_r",
                    color_continuous_midpoint=0, zmin=-1, zmax=1)
    
  6. 出版用图请设置 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未安装 kaleidopip 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 交互方式可视化

参考资料

评论

支持 Markdown