c07_volcano 高通量组学 (Omics) Nature Cell Lancet

High-Throughput Volcano Plot with Collision-Free Gene Labeling

转录组/蛋白质组差异表达高通量火山图,内置 adjustText 防重叠算法与显著性双阈值截断线。

🔬 矢量预览 (Vector Preview) 600 DPI Ready
High-Throughput Volcano Plot with Collision-Free Gene Labeling

📊 统计学特性 (Statistical Features)

  • log2(Fold-Change)
  • -log10(p-value / FDR)
  • 双阈值四象限分类
  • 显著差异基因计数统计

🛡️ QC 质检要点 (QC Highlights)

  • 核心候选基因标签 0 重叠碰撞
  • 阈值虚线标注清晰
  • 点半透明防止海量数据完全掩盖
  • 高分辨率矢量导出
🎨 顶刊色彩提取器 (Palette Extractor)Nature

Crisp sans-serif typography, clean borderless spines, high-contrast palette with soft muted secondary accents.

🛡️ 无障碍评分:AAA (Deuteranopia & Protanopia Compliant)

📋 Data Contract 数据契约规范 Strict Schema

输入数据必须完全符合下列列名与数据类型规范,方可通过自动化数据前检 (Pre-flight Validation)。

📥 下载规范示例 CSV 数据
字段名称 (Column) 数据类型 (Type) 约束性 语义说明与值域约束
gene_symbol string 必需 (Required) Gene or probe identifier (e.g., TP53, EGFR)
log2FC continuous 必需 (Required) log2(Fold Change) effect size
pvalue continuous 必需 (Required) Raw or nominal p-value (min: 0, max: 1)
padj continuous 可选 (Optional) FDR adjusted p-value / q-value
is_highlight boolean 可选 (Optional) Flag to force text label placement
组内最小样本量: n ≥ 10
最大允许缺失率: 5%

🐍 独立可复现 Python 绘图源码 Stand-alone Script

完全可复现的 Python 脚本,支持 CLI 参数 `--data`, `--style`, `--output-dir` 与模块化 `render()` 调用。

#!/usr/bin/env python3
"""FigureCraft Chart Engine: Volcano Plot with Collision-Free Gene Labeling (c07_volcano)."""

import os
import sys
import argparse
from pathlib import Path
from typing import List, Dict, Optional
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from adjustText import adjust_text

try:
    from engines.styles import apply_style, get_palette
except ImportError:
    sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
    from engines.styles import apply_style, get_palette

CHART_ID = "c07_volcano"


def render(
    data_path: Optional[str] = None,
    style: str = "nature",
    output_dir: str = "output",
    formats: Optional[List[str]] = None,
    dpi: int = 600,
) -> Dict[str, str]:
    if formats is None:
        formats = ["svg", "pdf", "png", "tiff"]
    if data_path is None:
        data_path = str(Path(__file__).resolve().parents[1] / "data" / f"{CHART_ID}.csv")

    apply_style(style)
    palette = get_palette(style)
    df = pd.read_csv(data_path)
    df = df.dropna(subset=["gene_symbol", "log2FC", "pvalue"])

    # Transform coordinates
    df["neg_log10_p"] = -np.log10(np.clip(df["pvalue"], 1e-300, 1.0))
    fc_thresh = 1.0
    p_thresh = -np.log10(0.05)

    # Classify regulation
    df["regulation"] = "NS"
    df.loc[(df["log2FC"] >= fc_thresh) & (df["neg_log10_p"] >= p_thresh), "regulation"] = "Up"
    df.loc[(df["log2FC"] <= -fc_thresh) & (df["neg_log10_p"] >= p_thresh), "regulation"] = "Down"

    color_up = palette[0] if len(palette) > 0 else "#E64B35"
    color_down = palette[1] if len(palette) > 1 else "#4DBBD5"
    color_ns = "#9CA3AF"

    fig, ax = plt.subplots(figsize=(6.5, 5.0))

    # Scatter points by category
    for reg, col, label, alpha, z in [
        ("NS", color_ns, "Non-significant", 0.45, 2),
        ("Down", color_down, "Down-regulated", 0.85, 3),
        ("Up", color_up, "Up-regulated", 0.85, 4),
    ]:
        sub = df[df["regulation"] == reg]
        ax.scatter(
            sub["log2FC"],
            sub["neg_log10_p"],
            c=col,
            label=label,
            alpha=alpha,
            s=30 if reg != "NS" else 20,
            edgecolors="#FFFFFF" if reg != "NS" else "none",
            linewidth=0.5,
            zorder=z,
        )

    # Threshold guidelines
    ax.axvline(x=fc_thresh, color="#6B7280", linestyle="--", linewidth=0.75, zorder=1)
    ax.axvline(x=-fc_thresh, color="#6B7280", linestyle="--", linewidth=0.75, zorder=1)
    ax.axhline(y=p_thresh, color="#6B7280", linestyle="--", linewidth=0.75, zorder=1)

    # Label top / highlighted significant genes using adjustText
    to_label = df[df["regulation"] != "NS"]
    if "is_highlight" in df.columns:
        # Filter where is_highlight is true or top 10 most extreme
        highlight_mask = df["is_highlight"].astype(str).str.upper() == "TRUE"
        to_label = df[highlight_mask]

    if len(to_label) > 15:
        to_label = to_label.sort_values("neg_log10_p", ascending=False).head(15)

    texts = []
    for _, row in to_label.iterrows():
        t = ax.text(
            row["log2FC"],
            row["neg_log10_p"],
            row["gene_symbol"],
            fontsize=7.5,
            fontweight="semibold",
            color="#111827",
        )
        texts.append(t)

    if texts:
        adjust_text(
            texts,
            ax=ax,
            arrowprops=dict(arrowstyle="-", color="#4B5563", lw=0.6),
            expand=(1.2, 1.4),
        )

    ax.set_xlabel("$\log_2$(Fold Change)")
    ax.set_ylabel("$-\log_{10}$($P$-value)")
    ax.set_title("High-Throughput Differential Expression Volcano Profile")
    ax.legend(loc="upper right", frameon=False, fontsize=7.5)

    os.makedirs(output_dir, exist_ok=True)
    generated_files = {}
    for fmt in formats:
        out_path = os.path.join(output_dir, f"{CHART_ID}_{style}.{fmt}")
        fig.savefig(out_path, format=fmt, dpi=dpi, bbox_inches="tight")
        generated_files[fmt] = str(Path(out_path).resolve())

    plt.close(fig)
    return generated_files


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Render Volcano Plot")
    parser.add_argument("--data", type=str, default=None, help="Path to input CSV dataset")
    parser.add_argument("--style", type=str, default="nature", choices=["nature", "cell", "lancet", "ieee", "economist"], help="Journal style pack")
    parser.add_argument("--output-dir", type=str, default="output", help="Output directory")
    parser.add_argument("--formats", type=str, default="svg,pdf,png,tiff", help="Comma-separated export formats")
    parser.add_argument("--dpi", type=int, default=600, help="Raster DPI")
    args = parser.parse_args()

    fmt_list = [f.strip() for f in args.formats.split(",") if f.strip()]
    results = render(args.data, style=args.style, output_dir=args.output_dir, formats=fmt_list, dpi=args.dpi)
    for fmt, path in results.items():
        print(f"Generated {fmt.upper()}: {path}")