高分Panel复现系列|转录因子网络太乱?用模块框把重点圈出来

这张图有一个很适合复用的表达方式:中间展示转录因子调控网络,两侧单独圈出重点通路基因。它不是常规的富集气泡图,而是把"谁调控谁"和"这些基因属于什么功能模块"放在同一张 panel 里。


图片来源

项目 内容
文章 A single-cell transcriptomic landscape characterizes the endocrine system aging in the mouse
期刊/年份 Protein & Cell, 2026
图号 Fig. 3C
DOI/链接 https://doi.org/10.1093/procel/pwaf074

原文利用小鼠内分泌系统单细胞图谱分析衰老相关通路,并在 Fig. 3C 中展示 thyroid follicular cells 中衰老相关 DEGs 的转录因子调控网络。图中重点突出两类通路:Inflammation 和 ER stress-UPR。


图片解读

这张图可以分成三层:

  1. 中间是基因调控网络,节点代表基因或转录因子,连线代表调控关系。
  2. 中心较大的红色节点是关键转录因子,例如 MaffAtf4Atf3FosJunJund 等。
  3. 左右两侧用椭圆框单独标注重点功能模块:左侧是 Inflammation,右侧是 ER stress-UPR。

复现时真正决定效果的是 网络布局、节点层级、标签位置和两侧功能模块框。如果只画普通网络图,重点基因会被大量背景节点淹没;所以这里要把背景节点弱化,把 hub gene 和模块基因突出出来。


输入数据

建议准备三个输入表。

节点表 input_network_nodes.csv

列名 含义
gene 基因名
group 节点类型,例如 hub、up、down、neutral
x 节点横坐标
y 节点纵坐标
label 是否显示标签;不显示可留空
size 节点大小
score 节点颜色映射值,例如 logFC 或调控强度

边表 input_network_edges.csv

列名 含义
from 起始基因
to 目标基因
edge_group 连线类型,例如 activation、repression、hub、module

模块基因表 input_module_genes.csv

列名 含义
module 功能模块名称
gene 模块内基因名
r 复制代码
library(dplyr)
library(readr)
library(ggplot2)
library(scales)

nodes <- read_csv("input_network_nodes.csv", show_col_types = FALSE)
edges <- read_csv("input_network_edges.csv", show_col_types = FALSE)
module_genes <- read_csv("input_module_genes.csv", show_col_types = FALSE)

需要示例数据的后台 添加小编 领取,调整好数据结构,以下代码可以直接复制粘贴运行。


第一步:把边表连接到节点坐标

网络图的核心是边表。fromto 只是基因名,真正画线时还需要把它们转换成起点和终点坐标。

r 复制代码
edge_dat <- edges |>
  left_join(nodes |> select(from = gene, x, y), by = "from") |>
  left_join(nodes |> select(to = gene, xend = x, yend = y), by = "to") |>
  mutate(edge_col = case_when(
    edge_group == "repression" ~ "#4e97b7",
    edge_group == "hub" ~ "#ce6d72",
    edge_group == "module" ~ "#b77a78",
    TRUE ~ "#e7b1b2"
  ))

第二步:准备两侧椭圆模块框

两侧功能模块不是图例,而是正文的一部分。这里用一组椭圆坐标来画边框。

r 复制代码
ellipse_df <- function(cx, cy, rx, ry, n = 240) {
  theta <- seq(0, 2 * pi, length.out = n)
  tibble(x = cx + rx * cos(theta), y = cy + ry * sin(theta))
}

left_ellipse <- ellipse_df(-2.48, 0.02, 0.58, 0.78)
right_ellipse <- ellipse_df(2.66, 0.02, 0.58, 0.84)

第三步:手动微调 hub gene 标签

网络中心的基因名很容易重叠,所以这里不建议完全依赖自动避让。对少量核心基因,手动给标签坐标反而更稳定。

r 复制代码
label_pos <- nodes |>
  filter(!is.na(label), group == "hub") |>
  mutate(
    label_x = case_when(
      gene == "Maff" ~ -0.18,
      gene == "Atf4" ~ -0.43,
      gene == "Atf3" ~ -0.36,
      gene == "Cebpb" ~ -0.33,
      gene == "Cebpd" ~ -0.10,
      gene == "Fos" ~ 0.30,
      gene == "Jun" ~ 0.66,
      gene == "Jund" ~ 0.88,
      gene == "Junb" ~ 0.35,
      TRUE ~ x
    ),
    label_y = case_when(
      gene == "Maff" ~ 0.61,
      gene == "Atf4" ~ 0.28,
      gene == "Atf3" ~ -0.12,
      gene == "Cebpb" ~ -0.55,
      gene == "Cebpd" ~ -0.73,
      gene == "Fos" ~ 0.23,
      gene == "Jun" ~ 0.34,
      gene == "Jund" ~ 0.11,
      gene == "Junb" ~ -0.46,
      TRUE ~ y
    )
  )

第四步:叠加网络、节点、标签和模块框

背景边和普通节点要轻,hub gene 要重,两侧模块框则用不同颜色区分。

r 复制代码
p <- ggplot() +
  geom_curve(
    data = edge_dat,
    aes(x = x, y = y, xend = xend, yend = yend, color = edge_col),
    curvature = 0.08,
    linewidth = 0.16,
    alpha = 0.24
  ) +
  scale_color_identity() +
  geom_point(
    data = nodes |> filter(group %in% c("up", "down", "neutral")),
    aes(x, y, fill = score, size = size),
    shape = 21,
    color = "white",
    stroke = 0.10,
    alpha = 0.72
  ) +
  geom_point(
    data = nodes |> filter(group == "hub"),
    aes(x, y, size = size),
    shape = 21,
    fill = "#b81423",
    color = "#9b101b",
    stroke = 0.45,
    alpha = 0.96
  ) +
  geom_path(
    data = left_ellipse,
    aes(x, y),
    color = "#bd7136",
    linewidth = 1.0
  ) +
  geom_path(
    data = right_ellipse,
    aes(x, y),
    color = "#4b9a78",
    linewidth = 1.0
  ) +
  annotate(
    "text",
    x = -2.48,
    y = 1.05,
    label = "Inflammation",
    family = "serif",
    fontface = "bold",
    size = 4.6,
    color = "#222222"
  ) +
  annotate(
    "text",
    x = 2.66,
    y = 1.10,
    label = "ER stress-UPR",
    family = "serif",
    fontface = "bold",
    size = 4.5,
    color = "#222222"
  ) +
  annotate(
    "text",
    x = -2.48,
    y = 0.02,
    label = paste(
      module_genes |> filter(module == "Inflammation") |> pull(gene),
      collapse = "\n"
    ),
    family = "serif",
    fontface = "italic",
    size = 4.3,
    lineheight = 0.92,
    color = "#555555"
  ) +
  annotate(
    "text",
    x = 2.66,
    y = 0.02,
    label = paste(
      module_genes |> filter(module == "ER stress-UPR") |> pull(gene),
      collapse = "\n"
    ),
    family = "serif",
    fontface = "italic",
    size = 4.1,
    lineheight = 0.92,
    color = "#555555"
  ) +
  geom_text(
    data = label_pos,
    aes(label_x, label_y, label = label),
    family = "serif",
    fontface = "italic",
    size = 3.7,
    color = "#5a3c32"
  ) +
  scale_fill_gradient2(
    low = "#2b83ba",
    mid = "#f2eeee",
    high = "#c82333",
    midpoint = 0,
    guide = "none"
  ) +
  scale_size_identity() +
  coord_fixed(
    xlim = c(-3.35, 3.35),
    ylim = c(-1.45, 1.45),
    clip = "off"
  ) +
  theme_void(base_family = "serif") +
  theme(plot.margin = margin(12, 16, 10, 16))

第五步:导出结果

这种横向网络图建议宽一点,避免两侧模块和中心网络挤在一起。

r 复制代码
ggsave(
  "gene_network_module.png",
  p,
  width = 7.6,
  height = 3.55,
  dpi = 450,
  bg = "white"
)

ggsave(
  "gene_network_module.pdf",
  p,
  width = 7.6,
  height = 3.55,
  bg = "white"
)

完整代码

r 复制代码
library(dplyr)
library(readr)
library(ggplot2)
library(scales)

nodes <- read_csv("input_network_nodes.csv", show_col_types = FALSE)
edges <- read_csv("input_network_edges.csv", show_col_types = FALSE)
module_genes <- read_csv("input_module_genes.csv", show_col_types = FALSE)

edge_dat <- edges |>
  left_join(nodes |> select(from = gene, x, y), by = "from") |>
  left_join(nodes |> select(to = gene, xend = x, yend = y), by = "to") |>
  mutate(edge_col = case_when(
    edge_group == "repression" ~ "#4e97b7",
    edge_group == "hub" ~ "#ce6d72",
    edge_group == "module" ~ "#b77a78",
    TRUE ~ "#e7b1b2"
  ))

ellipse_df <- function(cx, cy, rx, ry, n = 240) {
  theta <- seq(0, 2 * pi, length.out = n)
  tibble(x = cx + rx * cos(theta), y = cy + ry * sin(theta))
}

left_ellipse <- ellipse_df(-2.48, 0.02, 0.58, 0.78)
right_ellipse <- ellipse_df(2.66, 0.02, 0.58, 0.84)

label_pos <- nodes |>
  filter(!is.na(label), group == "hub") |>
  mutate(
    label_x = case_when(
      gene == "Maff" ~ -0.18,
      gene == "Atf4" ~ -0.43,
      gene == "Atf3" ~ -0.36,
      gene == "Cebpb" ~ -0.33,
      gene == "Cebpd" ~ -0.10,
      gene == "Fos" ~ 0.30,
      gene == "Jun" ~ 0.66,
      gene == "Jund" ~ 0.88,
      gene == "Junb" ~ 0.35,
      TRUE ~ x
    ),
    label_y = case_when(
      gene == "Maff" ~ 0.61,
      gene == "Atf4" ~ 0.28,
      gene == "Atf3" ~ -0.12,
      gene == "Cebpb" ~ -0.55,
      gene == "Cebpd" ~ -0.73,
      gene == "Fos" ~ 0.23,
      gene == "Jun" ~ 0.34,
      gene == "Jund" ~ 0.11,
      gene == "Junb" ~ -0.46,
      TRUE ~ y
    )
  )

p <- ggplot() +
  geom_curve(
    data = edge_dat,
    aes(x = x, y = y, xend = xend, yend = yend, color = edge_col),
    curvature = 0.08,
    linewidth = 0.16,
    alpha = 0.24
  ) +
  scale_color_identity() +
  geom_point(
    data = nodes |> filter(group %in% c("up", "down", "neutral")),
    aes(x, y, fill = score, size = size),
    shape = 21,
    color = "white",
    stroke = 0.10,
    alpha = 0.72
  ) +
  geom_point(
    data = nodes |> filter(group == "hub"),
    aes(x, y, size = size),
    shape = 21,
    fill = "#b81423",
    color = "#9b101b",
    stroke = 0.45,
    alpha = 0.96
  ) +
  geom_path(
    data = left_ellipse,
    aes(x, y),
    color = "#bd7136",
    linewidth = 1.0
  ) +
  geom_path(
    data = right_ellipse,
    aes(x, y),
    color = "#4b9a78",
    linewidth = 1.0
  ) +
  annotate(
    "text",
    x = -2.48,
    y = 1.05,
    label = "Inflammation",
    family = "serif",
    fontface = "bold",
    size = 4.6,
    color = "#222222"
  ) +
  annotate(
    "text",
    x = 2.66,
    y = 1.10,
    label = "ER stress-UPR",
    family = "serif",
    fontface = "bold",
    size = 4.5,
    color = "#222222"
  ) +
  annotate(
    "text",
    x = -2.48,
    y = 0.02,
    label = paste(
      module_genes |> filter(module == "Inflammation") |> pull(gene),
      collapse = "\n"
    ),
    family = "serif",
    fontface = "italic",
    size = 4.3,
    lineheight = 0.92,
    color = "#555555"
  ) +
  annotate(
    "text",
    x = 2.66,
    y = 0.02,
    label = paste(
      module_genes |> filter(module == "ER stress-UPR") |> pull(gene),
      collapse = "\n"
    ),
    family = "serif",
    fontface = "italic",
    size = 4.1,
    lineheight = 0.92,
    color = "#555555"
  ) +
  geom_text(
    data = label_pos,
    aes(label_x, label_y, label = label),
    family = "serif",
    fontface = "italic",
    size = 3.7,
    color = "#5a3c32"
  ) +
  scale_fill_gradient2(
    low = "#2b83ba",
    mid = "#f2eeee",
    high = "#c82333",
    midpoint = 0,
    guide = "none"
  ) +
  scale_size_identity() +
  coord_fixed(
    xlim = c(-3.35, 3.35),
    ylim = c(-1.45, 1.45),
    clip = "off"
  ) +
  theme_void(base_family = "serif") +
  theme(plot.margin = margin(12, 16, 10, 16))

ggsave(
  "gene_network_module.png",
  p,
  width = 7.6,
  height = 3.55,
  dpi = 450,
  bg = "white"
)

ggsave(
  "gene_network_module.pdf",
  p,
  width = 7.6,
  height = 3.55,
  bg = "white"
)

复现结果


参考链接

相关推荐
oradh6 小时前
Oracle TX 锁 Mode 4(Share)问题排查总结
数据库·oracle·tx 锁 mode 4·oracle tx 锁
yi.Ist7 小时前
数据定义语言-DDL操作
数据库·学习·mysql·oracle·大海豚
2601_963282778 小时前
工程项目、政企采购为什么优先选择对讲机批量采购?
数据库
Doraemomo8 小时前
SQLite数据库
数据库·sqlite
云贝教育-郑老师9 小时前
MySQL 的“黑匣子“:Performance Schema 把数据库内部变成一张可查的表
数据库·mysql
这个DBA有点耶11 小时前
同城双活落地的三座山:网络延迟、脑裂预防、反向同步
数据库·架构·dba
青春之我_XP11 小时前
MySQL 常用日期函数 实战指南
数据库·sql·mysql·数据分析·数据库开发·日期函数
Nturmoils11 小时前
sys_dump 备了库,角色和权限别漏在外面
数据库
接着奏乐接着舞。11 小时前
【2026】73道Redis 常见面试题与参考答案
数据库·redis·后端·缓存