# -*- coding: utf-8 -*-
"""gen_figures_partA_schematics.py —— 生成教学示意图 fig01–fig06。

说明：这些是"概念示意图"，用 matplotlib 手工绘制，不依赖真实数据。
运行：python code/figures/gen_figures_partA_schematics.py
"""
import os, sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, Circle, RegularPolygon, FancyArrowPatch, Rectangle, Ellipse, Polygon, Wedge
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fig_common import savefig, NICHE_COLORS, CT_COLORS, PALETTE10

# ============================================================ fig01 技术路线图
def fig01_roadmap():
    fig, ax = plt.subplots(figsize=(11, 8.2))
    ax.set_xlim(0, 11); ax.set_ylim(0, 12.6); ax.axis("off")

    # 标题
    ax.text(5.5, 12.1, "《空间生态位 × 细胞通讯重编程》课题技术路线", ha="center",
            fontsize=16, fontweight="bold", color="#2C3E50")

    phases = [
        ("① 基础铺垫（第 01–04 章）", "#3498DB", [
            ("01 肿瘤微环境与细胞通讯", "TME 组成 · 配体-受体 · 免疫抑制"),
            ("02 空间转录组技术原理", "scRNA-seq vs 10x Visium"),
            ("03 生信基础", "R / Python / Seurat / AnnData"),
            ("04 方法学总览", "生态位识别 · 通讯推断工具谱系"),
        ]),
        ("② 课题设计（第 05 章）", "#9B59B6", [
            ("创新点拆解", "生态位视角 + 通讯重编程 + 空间验证"),
            ("研究假设 H1–H3", "生态位间通讯存在系统性差异"),
        ]),
        ("③ 分析主线（第 06–13 章）", "#E67E22", [
            ("06–07 环境与数据", "conda / Seurat / 10x Visium 公开数据"),
            ("08 预处理与质控", "spot 过滤 · 归一化 · PCA · UMAP"),
            ("09 聚类与细胞类型注释", "marker 注释 · cell2location 去卷积"),
            ("10 空间生态位识别", "BayesSpace · Squidpy 邻域富集"),
            ("11 细胞通讯基础", "CellChat：配体-受体 · 通讯概率"),
            ("12 生态位内通讯", "按生态位分区独立分析"),
            ("13 通讯重编程", "生态位间差异 · COMMOT 空间验证"),
        ]),
        ("④ 写作与交付（第 14–16 章）", "#27AE60", [
            ("14 论文级图表", "空间图 · 网络图 · 热图 · Sankey"),
            ("15 结果解读与论文", "讲好'重编程'故事 · 投稿"),
            ("16 排错与检查清单", "交付前逐项检查"),
        ]),
    ]

    y = 11.2
    for title, color, items in phases:
        # 阶段条
        box = FancyBboxPatch((0.4, y - 0.62), 4.35, 1.06, boxstyle="round,pad=0.08",
                             linewidth=1.6, edgecolor=color, facecolor=color, alpha=0.92)
        ax.add_patch(box)
        ax.text(2.58, y - 0.09, title, ha="center", va="center", fontsize=12.5,
                fontweight="bold", color="white")
        # 条目框
        for i, (t, d) in enumerate(items):
            yy = y - 0.95 - i * 1.06
            FancyBboxPatch((0.25, yy - 0.42), 10.5, 0.92, boxstyle="round,pad=0.06",
                           linewidth=1.0, edgecolor="#BDC3C7", facecolor="#F8F9F9").set_clip_on(False)
            b = FancyBboxPatch((0.25, yy - 0.42), 10.5, 0.92, boxstyle="round,pad=0.06",
                               linewidth=1.0, edgecolor="#BDC3C7", facecolor="#F8F9F9")
            ax.add_patch(b)
            ax.text(1.6, yy + 0.05, t, ha="center", va="center", fontsize=10.5, fontweight="bold", color="#2C3E50")
            ax.text(5.9, yy + 0.05, d, ha="left", va="center", fontsize=9.5, color="#34495E")
        y = yy - 0.62

    ax.annotate("", xy=(5.5, 8.9), xytext=(5.5, 10.1),
                arrowprops=dict(arrowstyle="-|>", color="#95A5A6", lw=1.5))
    ax.annotate("", xy=(5.5, 5.9), xytext=(5.5, 6.9),
                arrowprops=dict(arrowstyle="-|>", color="#95A5A6", lw=1.5))
    ax.annotate("", xy=(5.5, 2.2), xytext=(5.5, 3.2),
                arrowprops=dict(arrowstyle="-|>", color="#95A5A6", lw=1.5))

    # 底部核心输出
    out = FancyBboxPatch((1.0, 0.25), 9.0, 1.15, boxstyle="round,pad=0.1",
                         linewidth=2, edgecolor="#C0392B", facecolor="#FDEDEC")
    ax.add_patch(out)
    ax.text(5.5, 0.82, "核心产出：不同空间生态位的细胞通讯网络图谱 + 微环境'通讯重编程'信号",
            ha="center", fontsize=11.5, fontweight="bold", color="#C0392B")

    return savefig(fig, "fig01_roadmap.png")

# ============================================================ fig02 scRNA vs 空间转录组
def fig02_sc_vs_spatial():
    rng = np.random.default_rng(7)
    fig, axes = plt.subplots(1, 2, figsize=(11.5, 4.8))

    # 左：scRNA-seq
    ax = axes[0]
    ax.set_xlim(0, 10); ax.set_ylim(0, 10); ax.axis("off")
    ax.text(5, 9.6, "单细胞测序（scRNA-seq）", ha="center", fontsize=13, fontweight="bold", color="#2C3E50")
    # 组织块 -> 解离
    t1 = Rectangle((0.6, 3.0), 2.6, 3.4, facecolor="#F5CBA7", edgecolor="#CA6F1E", lw=1.5)
    ax.add_patch(t1); ax.text(1.9, 4.7, "组织", ha="center", fontsize=10)
    ax.annotate("", xy=(4.2, 4.7), xytext=(3.4, 4.7), arrowprops=dict(arrowstyle="-|>", color="#7F8C8D", lw=2))
    # 液滴
    for i in range(8):
        c = Circle((4.6 + (i % 4) * 0.95, 2.6 + (i // 4) * 2.6), 0.36,
                   facecolor=PALETTE10[i % 10], alpha=0.85, edgecolor="white")
        ax.add_patch(c)
    ax.text(6.2, 5.5, "解离成单细胞\n→ 微滴包裹", ha="center", fontsize=9.5, color="#34495E")
    ax.annotate("", xy=(8.3, 4.7), xytext=(7.3, 4.7), arrowprops=dict(arrowstyle="-|>", color="#7F8F8D", lw=2))
    # 丢失位置
    xs = rng.normal(6.9, 1.0, 40); ys = rng.normal(3.0, 1.0, 40)
    cols = [PALETTE10[rng.integers(0, 10)] for _ in range(40)]
    ax.scatter(xs, ys, c=cols, s=26, alpha=0.8, edgecolors="white", linewidths=0.3)
    ax.text(8.4, 1.2, "UMAP 聚类\n（位置信息丢失）", ha="center", fontsize=9.5, color="#34495E")
    ax.text(5, 0.6, "× 无法回答：细胞在哪里、和谁相邻", ha="center", fontsize=10.5, color="#C0392B", fontweight="bold")

    # 右：空间转录组
    ax = axes[1]
    ax.set_xlim(0, 10); ax.set_ylim(0, 10); ax.axis("off")
    ax.text(5, 9.6, "空间转录组（10x Visium）", ha="center", fontsize=13, fontweight="bold", color="#2C3E50")
    # 组织切片放在载玻片
    tissue = Ellipse((5, 5.4), 7.2, 5.6, facecolor="#F5CBA7", edgecolor="#CA6F1E", lw=1.5, alpha=0.9)
    ax.add_patch(tissue)
    # 六边形 spot 网格（仅组织内一部分示意）
    sp = 0.62
    for i in range(-5, 6):
        for j in range(-4, 5):
            x = 5 + i * sp + (j % 2) * sp / 2
            y = 5.4 + j * sp * 0.866
            d = np.sqrt(((x - 5) / 3.6) ** 2 + ((y - 5.4) / 2.8) ** 2)
            if d < 0.98:
                col = NICHE_COLORS["肿瘤核心"] if d < 0.4 else (NICHE_COLORS["侵袭前沿"] if d < 0.62 else "#AED6F1")
                hexg = RegularPolygon((x, y), numVertices=6, radius=0.30, orientation=np.pi / 6,
                                      facecolor=col, edgecolor="white", lw=0.4, alpha=0.95)
                ax.add_patch(hexg)
    ax.annotate("", xy=(5, 2.1), xytext=(5, 2.9), arrowprops=dict(arrowstyle="-|>", color="#7F8C8D", lw=2))
    ax.text(5.9, 2.05, "每个 spot 捕获其覆盖组织内的 mRNA\n（保留位置 + 表达）", fontsize=9.5, ha="left", color="#34495E")
    ax.text(5, 0.6, "√ 可回答：细胞在哪里、与谁相邻、如何通讯", ha="center", fontsize=10.5, color="#27AE60", fontweight="bold")

    return savefig(fig, "fig02_sc_vs_spatial.png")

# ============================================================ fig03 Visium 原理
def fig03_visium_principle():
    fig = plt.figure(figsize=(11.5, 6.6))
    gs = fig.add_gridspec(2, 3, hspace=0.35, wspace=0.25)

    # (a) 捕获区
    ax = fig.add_subplot(gs[0, 0]); ax.set_aspect("equal"); ax.axis("off")
    ax.set_xlim(0, 10); ax.set_ylim(0, 10)
    ax.text(5, 9.4, "(a) 捕获区：6.5×6.5 mm", ha="center", fontsize=11, fontweight="bold")
    rect = Rectangle((0.8, 0.8), 8.4, 8.4, facecolor="#FDF2E9", edgecolor="#B9770E", lw=1.8)
    ax.add_patch(rect)
    sp = 0.78
    for i in range(11):
        for j in range(11):
            x = 1.6 + i * sp + (j % 2) * sp / 2
            y = 1.6 + j * sp * 0.866
            if 0.8 < x < 9.2 and 0.8 < y < 9.2:
                hexg = RegularPolygon((x, y), 6, radius=0.36, orientation=np.pi / 6,
                                      facecolor="#D4AC0D" if (i + j) % 3 else "#E8C873",
                                      edgecolor="white", lw=0.5, alpha=0.9)
                ax.add_patch(hexg)
    ax.text(5, 0.4, "约 5000 个 spot（直径 ≈ 55 μm，中心距 100 μm）", ha="center", fontsize=9)

    # (b) spot 放大：条形码探针
    ax = fig.add_subplot(gs[0, 1]); ax.axis("off"); ax.set_xlim(0, 10); ax.set_ylim(0, 10)
    ax.text(5, 9.4, "(b) 单个 spot 的捕获探针", ha="center", fontsize=11, fontweight="bold")
    for i in range(4):
        for j in range(5):
            c = Circle((1.5 + j * 1.9, 3.6 + i * 1.6), 0.55, facecolor="#D6EAF8",
                       edgecolor="#2E86C1", lw=1.0)
            ax.add_patch(c)
            # 探针柄
            ax.plot([1.5 + j * 1.9, 1.5 + j * 1.9], [4.4 + i * 1.6, 5.3 + i * 1.6],
                    color="#2E86C1", lw=1.0)
    ax.text(5, 6.0, "每个探针 = 空间条形码 + UMI + poly-T", ha="center", fontsize=9.5, color="#34495E")
    ax.text(5, 5.2, "（空间条形码标记 spot 的位置身份）", ha="center", fontsize=9, color="#7F8C8D")
    ax.text(5, 1.6, "同一 spot 内所有探针共享同一条空间条形码", ha="center", fontsize=9.5, color="#C0392B")

    # (c) 组织切片 + mRNA 捕获
    ax = fig.add_subplot(gs[0, 2]); ax.axis("off"); ax.set_xlim(0, 10); ax.set_ylim(0, 10)
    ax.text(5, 9.4, "(c) 组织切片贴片 → mRNA 原位捕获", ha="center", fontsize=11, fontweight="bold")
    tissue = Ellipse((5, 5.4), 8.2, 6.4, facecolor="#F9E79F", edgecolor="#9A7D0A", lw=1.5, alpha=0.95)
    ax.add_patch(tissue)
    # 组织细胞核示意
    rng = np.random.default_rng(3)
    for _ in range(46):
        x = rng.normal(5, 2.6); y = rng.normal(5.4, 2.0)
        if ((x - 5) / 4.1) ** 2 + ((y - 5.4) / 3.2) ** 2 < 1:
            ax.scatter(x, y, s=8, c="#784212", alpha=0.8)
    # mRNA 释放箭头
    for (xx, yy) in [(2.4, 4.2), (5, 6.6), (7.4, 3.6)]:
        ax.annotate("", xy=(xx, yy - 0.55), xytext=(xx, yy + 0.4),
                    arrowprops=dict(arrowstyle="-|>", color="#E74C3C", lw=1.4))
    ax.text(8.0, 6.8, "mRNA\n被探针捕获", fontsize=9, ha="center", color="#E74C3C")
    ax.text(5, 1.2, "透化使 mRNA 垂直扩散到探针，原位 '复印' 组织表达图谱", ha="center", fontsize=9, color="#34495E")

    # (d) 测序 → 表达矩阵
    ax = fig.add_subplot(gs[1, :]); ax.axis("off"); ax.set_xlim(0, 13); ax.set_ylim(0, 7.6)
    ax.text(6.5, 7.2, "(d) 测序 → 按空间条形码拆分 → spot × 基因表达矩阵", ha="center", fontsize=11.5, fontweight="bold")
    # 左：spot 网格 -> 右：矩阵
    rng = np.random.default_rng(5)
    for i in range(6):
        for j in range(6):
            x = 1.0 + i * 0.62; y = 5.3 - j * 0.62
            hexg = RegularPolygon((x, y), 6, radius=0.3, orientation=np.pi / 6,
                                  facecolor=PALETTE10[(i + j) % 10], edgecolor="white", lw=0.4, alpha=0.95)
            ax.add_patch(hexg)
    ax.annotate("", xy=(5.4, 4.6), xytext=(4.6, 4.6), arrowprops=dict(arrowstyle="-|>", color="#7F8C8D", lw=2))
    ax.text(3.4, 0.5, "spot 位置（空间坐标）", ha="center", fontsize=9, color="#34495E")
    # 矩阵
    m = rng.integers(0, 60, (7, 8)).astype(float)
    for i in range(7):
        m[i, i % 8] = rng.integers(60, 120)
    axm = fig.add_axes([0.48, 0.06, 0.44, 0.42]); axm.axis("off")
    axm.imshow(m, cmap="YlOrRd", aspect="auto")
    for i in range(7):
        axm.text(-0.7, i, f"spot_{i+1}", ha="right", va="center", fontsize=7.5)
    for j in range(8):
        axm.text(j, -0.9, f"gene{j+1}", ha="center", va="bottom", fontsize=7.5, rotation=30)
    axm.text(3.5, 7.0, "表达计数矩阵", ha="center", fontsize=9.5, color="#34495E")
    return savefig(fig, "fig03_visium_principle.png")

# ============================================================ fig04 TME 组成
def fig04_tme_composition():
    fig, ax = plt.subplots(figsize=(10.5, 6.6))
    ax.set_xlim(0, 10.5); ax.set_ylim(0, 7); ax.axis("off")
    ax.text(5.25, 6.6, "肿瘤微环境（TME）的主要细胞与基质组分", ha="center",
            fontsize=15, fontweight="bold", color="#2C3E50")

    # ECM 背景
    ax.set_facecolor("#FBF5EF")
    rng = np.random.default_rng(11)
    for _ in range(60):
        x = rng.uniform(0.3, 10.2); y = rng.uniform(0.5, 6.2)
        ax.plot([x, x + rng.uniform(0.2, 0.6)], [y, y + rng.uniform(0.05, 0.2)],
                color="#D5B48C", lw=0.7, alpha=0.6)
    ax.text(0.4, 6.15, "ECM（胶原、纤连蛋白等）", fontsize=9, color="#8B6F47")

    # 中心：肿瘤细胞团
    for (cx, cy, r) in [(3.4, 3.6, 0.85), (4.3, 3.2, 0.95), (5.1, 4.0, 0.9), (4.2, 4.4, 0.8)]:
        ax.add_patch(Circle((cx, cy), r, facecolor=CT_COLORS["肿瘤细胞"], alpha=0.88, edgecolor="white", lw=1.5))
    ax.text(4.2, 2.35, "肿瘤细胞（癌细胞）", ha="center", fontsize=11, fontweight="bold", color=CT_COLORS["肿瘤细胞"])

    # 周围细胞
    cells = [
        (8.0, 5.2, "CAF", "癌症相关成纤维细胞"),
        (8.6, 2.4, "CD8 T", "杀伤性 T 细胞"),
        (7.2, 0.8, "巨噬细胞", "肿瘤相关巨噬细胞"),
        (1.0, 1.0, "B 细胞", "B 细胞"),
        (0.9, 4.9, "内皮细胞", "血管内皮细胞"),
        (9.7, 3.9, "NK", "自然杀伤细胞"),
    ]
    for x, y, key, name in cells:
        ax.add_patch(Circle((x, y), 0.62, facecolor=CT_COLORS[key], alpha=0.9, edgecolor="white", lw=1.5))
        ax.text(x, y + 0.02, key, ha="center", va="center", fontsize=9.5, color="white", fontweight="bold")
        ax.text(x, y - 1.05, name, ha="center", fontsize=9, color="#34495E")

    # 交互箭头（细胞因子/趋化因子）
    for (x1, y1, x2, y2, label) in [
        (5.2, 3.9, 7.4, 5.0, "CXCL12"),
        (4.8, 3.3, 8.3, 2.6, "TGF-β"),
        (4.4, 4.2, 7.0, 1.2, "PD-L1/PD-1"),
        (3.6, 3.0, 1.7, 1.3, "VEGF"),
        (5.6, 3.6, 1.6, 4.6, "趋化因子"),
        (4.0, 3.4, 9.3, 3.9, "MIF"),
    ]:
        ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                    arrowprops=dict(arrowstyle="-|>", color="#E67E22", lw=1.6, alpha=0.85,
                                    connectionstyle="arc3,rad=0.18"))
        mx, my = (x1 + x2) / 2 + 0.25, (y1 + y2) / 2 + 0.28
        ax.text(mx, my, label, fontsize=8.5, color="#B9770E", ha="center",
                bbox=dict(boxstyle="round,pad=0.15", fc="white", ec="#E67E22", lw=0.6))

    ax.text(5.25, 0.12, "肿瘤通过分泌因子与接触互作，持续'重编程'周围细胞的行为（免疫抑制、促血管生成、促纤维化）",
            ha="center", fontsize=10, color="#7B241C", style="italic")
    return savefig(fig, "fig04_tme_composition.png")

# ============================================================ fig05 配体-受体
def fig05_ligand_receptor():
    fig, ax = plt.subplots(figsize=(11, 5.4))
    ax.set_xlim(0, 11); ax.set_ylim(0, 5.4); ax.axis("off")
    ax.text(5.5, 5.05, "细胞通讯的三种基本模式（配体 → 受体）", ha="center",
            fontsize=14, fontweight="bold", color="#2C3E50")

    def cell(x, y, r, label, fc):
        ax.add_patch(Circle((x, y), r, facecolor=fc, alpha=0.9, edgecolor="white", lw=2))
        ax.text(x, y, label, ha="center", va="center", fontsize=10, color="white", fontweight="bold")

    # --- 旁分泌 ---
    cell(1.6, 2.7, 1.0, "分泌细胞", "#2980B9")
    cell(4.6, 2.7, 1.0, "靶细胞", "#8E44AD")
    ax.add_patch(Circle((2.4, 2.7), 0.16, color="#E74C3C"))
    ax.add_patch(Circle((2.9, 2.9), 0.13, color="#E74C3C"))
    ax.add_patch(Circle((3.4, 2.6), 0.15, color="#E74C3C"))
    for xx in [2.55, 3.05, 3.55]:
        ax.annotate("", xy=(xx + 0.15, 2.7), xytext=(xx - 0.1, 2.7),
                    arrowprops=dict(arrowstyle="-|>", color="#E74C3C", lw=1.4))
    ax.text(3.05, 3.6, "分泌型配体\n（如 CXCL12）", ha="center", fontsize=9, color="#C0392B")
    ax.text(4.6, 1.25, "受体（如 CXCR4）", ha="center", fontsize=9, color="#6C3483")
    ax.add_patch(Wedge((4.6, 2.7), 1.25, -60, 60, facecolor="#F5EEF8", edgecolor="#8E44AD", lw=1.2, alpha=0.8))
    ax.text(1.6, 0.75, "旁分泌：配体释放到胞外\n作用于邻近细胞", ha="center", fontsize=9.5, color="#34495E")

    # --- 近分泌 ---
    cell(6.6, 2.7, 1.0, "信号细胞", "#C0392B")
    cell(9.4, 2.7, 1.0, "靶细胞", "#16A085")
    ax.plot([7.6, 8.4], [3.15, 3.15], color="#E74C3C", lw=3, solid_capstyle="round")
    ax.plot([7.6, 8.4], [2.25, 2.25], color="#7D3C98", lw=3, solid_capstyle="round")
    ax.annotate("", xy=(8.35, 3.15), xytext=(7.7, 3.15), arrowprops=dict(arrowstyle="-|>", color="#E74C3C", lw=1.5))
    ax.text(8.0, 3.65, "膜结合配体\n（如 PD-L1）", ha="center", fontsize=8.5, color="#C0392B")
    ax.text(8.0, 1.75, "受体（如 PD-1）", ha="center", fontsize=8.5, color="#117864")
    ax.text(8.0, 0.75, "近分泌：细胞直接接触\n（免疫检查点即此模式）", ha="center", fontsize=9.5, color="#34495E")

    # --- 自分泌 ---
    ax.add_patch(Rectangle((10.05, 4.3), 0.85, 0.75, facecolor="none", edgecolor="#95A5A6", lw=1))
    ax.text(10.5, 4.0, "自分泌：细胞分泌的因子\n作用于自身", fontsize=8.5, ha="center", color="#34495E")
    return savefig(fig, "fig05_ligand_receptor.png")

# ============================================================ fig06 空间生态位概念
def fig06_niche_concept():
    fig = plt.figure(figsize=(12, 6.2))
    gs = fig.add_gridspec(1, 2, width_ratios=[1.5, 1], wspace=0.08)
    ax = fig.add_subplot(gs[0, 0]); ax.set_aspect("equal"); ax.axis("off")
    ax.set_xlim(0, 11); ax.set_ylim(0, 10)
    ax.text(5.5, 9.5, "组织切片上的四个典型空间生态位（spatial niche）", ha="center",
            fontsize=13.5, fontweight="bold", color="#2C3E50")

    rng = np.random.default_rng(21)
    # 组织轮廓：不规则团块
    th = np.linspace(0, 2 * np.pi, 200)
    rx, ry = 4.9, 4.0
    xx = 5.5 + rx * np.cos(th) * (1 + 0.10 * np.sin(3 * th))
    yy = 5.0 + ry * np.sin(th) * (1 + 0.12 * np.cos(2 * th))
    ax.fill(xx, yy, color="#EAF2F8", edgecolor="#5D6D7E", lw=1.2, zorder=0)

    # 生成 spot 并分类
    spots = []
    for _ in range(2200):
        x = rng.uniform(0.8, 10.2); y = rng.uniform(1.2, 8.8)
        # 判断是否在组织内
        inside = False
        for x0, y0 in zip(xx, yy):
            if (x - x0) ** 2 + (y - y0) ** 2 < 0.35:
                inside = True; break
        if not inside:
            continue
        d = np.sqrt((x - 5.5) ** 2 + (y - 5.0) ** 2)
        niche = "基质区"
        if d < 1.55:
            niche = "肿瘤核心"
        elif d < 2.6:
            niche = "侵袭前沿"
        # 免疫补丁（椭圆）
        for (px, py, pa, pb, rot) in [(2.6, 4.6, 1.2, 0.8, 0.4), (7.8, 3.6, 1.3, 0.9, -0.5),
                                      (6.6, 6.6, 1.0, 0.7, 0.9), (3.4, 6.8, 0.9, 0.7, -0.8)]:
            dx = (x - px) * np.cos(-rot) - (y - py) * np.sin(-rot)
            dy = (x - px) * np.sin(-rot) + (y - py) * np.cos(-rot)
            if (dx / pa) ** 2 + (dy / pb) ** 2 < 1 and niche != "肿瘤核心":
                niche = "免疫浸润区"
        spots.append((x, y, niche))

    for x, y, niche in spots:
        ax.scatter(x, y, s=9, c=NICHE_COLORS[niche], edgecolors="none", alpha=0.9, zorder=2)
    # 分区描边
    c1 = Circle((5.5, 5.0), 1.55, fill=False, edgecolor="#922B21", lw=2, ls="--")
    c2 = Circle((5.5, 5.0), 2.6, fill=False, edgecolor="#CA6F1E", lw=2, ls="--")
    ax.add_patch(c1); ax.add_patch(c2)
    ax.annotate("", xy=(8.2, 6.4), xytext=(8.1, 6.5),
                arrowprops=dict(arrowstyle="->", color="#922B21"))
    ax.text(8.75, 6.9, "肿瘤核心", fontsize=10, color="#922B21", fontweight="bold")
    ax.text(8.7, 3.2, "侵袭前沿", fontsize=10, color="#CA6F1E", fontweight="bold")
    ax.text(1.0, 2.4, "免疫浸润区", fontsize=10, color="#1E8449", fontweight="bold")
    ax.text(9.3, 8.2, "基质区", fontsize=10, color="#1F618D", fontweight="bold")

    # 右侧：组成饼图
    ax2 = fig.add_subplot(gs[0, 1]); ax2.axis("off")
    ax2.set_xlim(0, 10); ax2.set_ylim(0, 10)
    ax2.text(5, 9.4, "各生态位的细胞组成（示意）", ha="center", fontsize=12.5, fontweight="bold", color="#2C3E50")
    comps = {
        "肿瘤核心": [0.62, 0.10, 0.05, 0.08, 0.03, 0.12],
        "侵袭前沿": [0.40, 0.22, 0.12, 0.12, 0.06, 0.08],
        "免疫浸润区": [0.18, 0.12, 0.30, 0.20, 0.15, 0.05],
        "基质区": [0.10, 0.35, 0.10, 0.12, 0.08, 0.25],
    }
    cts = ["肿瘤细胞", "CAF", "CD8 T", "巨噬细胞", "B 细胞", "内皮细胞"]
    for i, (name, comp) in enumerate(comps.items()):
        axp = fig.add_axes([0.56 + (i % 2) * 0.21, 0.47 - (i // 2) * 0.24, 0.17, 0.17])
        axp.pie(comp, colors=[CT_COLORS[c] for c in cts], startangle=90,
                wedgeprops=dict(edgecolor="white", lw=1))
        axp.set_title(name, fontsize=10, color=NICHE_COLORS[name], fontweight="bold")
    # 图例
    handles = [plt.Line2D([0], [0], marker="o", color="w", markerfacecolor=CT_COLORS[c], markersize=9, label=c)
               for c in cts]
    fig.legend(handles=handles, loc="lower center", ncol=6, frameon=False, fontsize=9, bbox_to_anchor=(0.5, 0.02))
    return savefig(fig, "fig06_niche_concept.png")

if __name__ == "__main__":
    fig01_roadmap()
    fig02_sc_vs_spatial()
    fig03_visium_principle()
    fig04_tme_composition()
    fig05_ligand_receptor()
    fig06_niche_concept()
    print("Part A done.")
