高分Panel复现系列|单细胞比例变化图:从数据到配对折线 + 环形组成图

发布时间:2026/11/5 3:34:04
高分Panel复现系列|单细胞比例变化图:从数据到配对折线 + 环形组成图

单细胞文章里经常会看到这种“上方环形组成图 + 下方配对折线图”的组合图。它的优点是:既能展示每个大类细胞内部亚群组成,又能直观看出不同组织状态之间细胞比例的变化方向。


图片来源

项目内容
文章Single-cell landscape of intratumoral heterogeneity and tumor microenvironment remolding in pre-nodal metastases of breast cancer
期刊/年份Journal of Translational Medicine, 2024
图号Fig. 5g

这篇文章整合乳腺癌单细胞数据,比较了原发肿瘤和转移前淋巴结相关样本中的细胞组成差异。Fig. 5g 主要展示了 Fibroblast、Macrophage、Endothelial cell 和 B cell 内部亚群在两种状态之间的比例变化。


图片解读

这是一张典型的 细胞亚群组成变化图。

它可以拆成三层:

  1. 上方环形图:每个大类细胞各有两个环形图,分别对应Primary tumors和Pre-nodal metastases。
  2. 中间色块标题:标出当前 panel 属于哪一类细胞,例如 Fibroblast、Macrophage、Endothelial cell、B cell。
  3. 下方配对折线图:每一条线代表一个细胞亚群,连接该亚群在两种状态下的比例。

读图时重点看线的方向:

  • 线向上:说明该亚群在Pre-nodal metastases中比例升高;
  • 线向下:说明该亚群在Pre-nodal metastases中比例下降;
  • 线接近水平:说明两种状态下比例变化不明显。

这类图特别适合展示单细胞分析中的“细胞组成重塑”,尤其适合比较不同组织状态、治疗前后或疾病阶段之间的细胞比例变化。


输入数据

列名含义
cell_type大类细胞,例如 Fibroblast、Macrophage、Endothelial cell、B cell
subtype细胞亚群,例如 iCAF、SPP1+ Macrophage、Vein、Naive
sample_group分组信息,例如 Primary tumors、Pre-nodal metastases
cell_prop该细胞亚群在对应分组中的比例

数据结构可以理解为:

cell_typesubtypesample_groupcell_prop
FibroblastiCAFPrimary tumors0.330
FibroblastiCAFPre-nodal metastases0.320
MacrophageSPP1+ MacrophagePrimary tumors0.480
MacrophageSPP1+ MacrophagePre-nodal metastases0.395

其中最关键的是cell_prop。下方折线图直接使用它作为 y 轴,上方环形图则需要在每个cell_type + sample_group内部重新归一化,得到每个亚群在该大类细胞中的组成占比。

library(tidyverse)dat<-read_csv("input_data.csv",show_col_types=FALSE)

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

第一步:整理分组顺序和绘图标签

首先固定细胞大类的显示顺序,并把 x 轴分组改成换行标签,这样更接近原图排版。

library(tidyverse)library(patchwork)library(scales)dat<-read_csv("input_data.csv",show_col_types=FALSE)cell_order<-c("Fibroblast","Macrophage","Endothelial cell","B cell")dat<-dat|>mutate(cell_type=factor(cell_type,levels=cell_order),group_label=factor(sample_group,levels=c("Primary tumors","Pre-nodal metastases"),labels=c("Primary\ntumors","Pre-nodal\nmetastases")))|>group_by(cell_type,sample_group)|>mutate(donut_share=cell_prop/sum(cell_prop))|>ungroup()

这里有一个关键点:donut_share是环形图使用的比例,它是在每个大类细胞和每个分组内部计算的。


第二步:设置颜色和点形状

原图中不同细胞大类使用不同色系:

  • Fibroblast:绿色系;
  • Macrophage:橙棕色系;
  • Endothelial cell:红色系;
  • B cell:紫色系。

同时,不同大类细胞也使用不同点形状,便于在图例中区分。

pal<-c("iCAF"="#A9CF86","mCAF"="#73C8BE","vCAF"="#8DD0AD","SPP1+ Macrophage"="#EEDCCB","FOLR2+ Macrophage"="#EDB97C","IDO1+ Macrophage"="#B87935","CPA3+ Macrophage"="#F1BA43","S100A9+ Macrophage"="#BC8B2F","CXCL9+ Macrophage"="#8D5B2B","Vein"="#FB7373","Artery"="#D94355","Capillary"="#C66B6F","Lymphatic"="#C90024","Naive"="#C7B2D8","Memory"="#6F439F","Plasma cell"="#8E1F70")strip_col<-c("Fibroblast"="#69AD88","Macrophage"="#F0B86F","Endothelial cell"="#EF6D6A","B cell"="#A79CBB")shape_val<-c("Fibroblast"=22,"Macrophage"=23,"Endothelial cell"=24,"B cell"=25)

第三步:画上方环形组成图

环形图本质上是一个极坐标柱状图。这里用geom_col()先画堆叠柱,再用coord_polar()转成环形。

make_donut<-function(d){ggplot(d,aes(x=2,y=donut_share,fill=subtype))+geom_col(width=1,color="white",linewidth=0.5)+coord_polar(theta="y")+xlim(0.6,2.5)+facet_wrap(~group_label,nrow=1)+scale_fill_manual(values=pal)+guides(fill="none")+theme_void()+theme(strip.text=element_blank())}

这里的xlim(0.6, 2.5)用来制造中间空洞,让饼图变成环形图。


第四步:画中间的细胞类型标题条

原图每个 panel 中间都有一个彩色标题条,用来标注当前细胞类型。

make_strip<-function(ct){ggplot()+annotate("rect",xmin=0,xmax=1,ymin=0,ymax=1,fill=strip_col[[ct]])+annotate("text",x=0.5,y=0.5,label=ct,size=4.5)+coord_cartesian(xlim=c(0,1),ylim=c(0,1),expand=FALSE)+theme_void()}

这个部分不依赖真实数据,只是为了复现原图的 panel 标题风格。


第五步:画下方配对折线图

下方折线图是整张图的核心。每个细胞亚群在两个分组中各有一个点,中间用线连接。

make_line<-function(d,ct,show_y=FALSE){y_range<-range(d$cell_prop,na.rm=TRUE)pad<-diff(y_range)*0.12+0.015ggplot(d,aes(group_label,cell_prop,group=subtype,color=subtype))+geom_line(linewidth=1.0,alpha=0.85)+geom_point(aes(fill=subtype),shape=shape_val[[ct]],size=5.5,alpha=0.50,stroke=0.25)+scale_color_manual(values=pal,name=NULL)+scale_fill_manual(values=pal,guide="none")+scale_y_continuous(limits=c(max(-0.05,y_range[1]-pad),y_range[2]+pad),breaks=pretty_breaks(4))+guides(color=guide_legend(ncol=4,byrow=TRUE))+labs(x=NULL,y=if(show_y)"Cell proportion"elseNULL)+theme_classic(base_size=12)+theme(axis.text.x=element_text(angle=60,hjust=1,vjust=1),axis.line=element_blank(),panel.border=element_rect(color="black",fill=NA,linewidth=0.6),legend.position="bottom")}

这里有一个容易报错的小细节。y 轴标题建议写成:

labs(x=NULL,y=if(show_y)"Cell proportion"elseNULL)

不要写成:

labs(x=NULL,y=ifelse(show_y,"Cell proportion",NULL))

因为ifelse()不适合返回NULL,有时会触发 replacement length zero 相关报错。


第六步:组合单个细胞类型 panel

每个细胞类型 panel 由三部分上下拼接:

  1. 环形组成图;
  2. 彩色标题条;
  3. 配对折线图。
plot_one_cell<-function(ct,show_y=FALSE){d<-filter(dat,cell_type==ct)make_donut(d)/make_strip(ct)/make_line(d,ct,show_y)+plot_layout(heights=c(0.18,0.08,0.74))}

patchwork的/表示上下拼接,plot_layout(heights = ...)用来控制三层图的高度比例。


第七步:横向拼接并导出图片

最后把四个细胞大类横向拼接,并统一收集图例。

fig<-wrap_plots(plot_one_cell("Fibroblast",TRUE),plot_one_cell("Macrophage"),plot_one_cell("Endothelial cell"),plot_one_cell("B cell"),nrow=1)+plot_layout(guides="collect")+plot_annotation(title="g")&theme(legend.position="bottom",legend.box="vertical",legend.key.width=unit(0.9,"cm"),legend.text=element_text(size=8.5),plot.title=element_text(size=24,face="bold"))ggsave("paired_line_donut_panel_g.png",fig,width=12,height=8.5,dpi=320,bg="white")ggsave("paired_line_donut_panel_g.pdf",fig,width=12,height=8.5,bg="white")

复现结果


完整代码

library(tidyverse)library(patchwork)library(scales)dat<-read_csv("input_data.csv",show_col_types=FALSE)cell_order<-c("Fibroblast","Macrophage","Endothelial cell","B cell")dat<-dat|>mutate(cell_type=factor(cell_type,levels=cell_order),group_label=factor(sample_group,levels=c("Primary tumors","Pre-nodal metastases"),labels=c("Primary\ntumors","Pre-nodal\nmetastases")))|>group_by(cell_type,sample_group)|>mutate(donut_share=cell_prop/sum(cell_prop))|>ungroup()pal<-c("iCAF"="#A9CF86","mCAF"="#73C8BE","vCAF"="#8DD0AD","SPP1+ Macrophage"="#EEDCCB","FOLR2+ Macrophage"="#EDB97C","IDO1+ Macrophage"="#B87935","CPA3+ Macrophage"="#F1BA43","S100A9+ Macrophage"="#BC8B2F","CXCL9+ Macrophage"="#8D5B2B","Vein"="#FB7373","Artery"="#D94355","Capillary"="#C66B6F","Lymphatic"="#C90024","Naive"="#C7B2D8","Memory"="#6F439F","Plasma cell"="#8E1F70")strip_col<-c("Fibroblast"="#69AD88","Macrophage"="#F0B86F","Endothelial cell"="#EF6D6A","B cell"="#A79CBB")shape_val<-c("Fibroblast"=22,"Macrophage"=23,"Endothelial cell"=24,"B cell"=25)make_donut<-function(d){ggplot(d,aes(x=2,y=donut_share,fill=subtype))+geom_col(width=1,color="white",linewidth=0.5)+coord_polar(theta="y")+xlim(0.6,2.5)+facet_wrap(~group_label,nrow=1)+scale_fill_manual(values=pal)+guides(fill="none")+theme_void()+theme(strip.text=element_blank())}make_strip<-function(ct){ggplot()+annotate("rect",xmin=0,xmax=1,ymin=0,ymax=1,fill=strip_col[[ct]])+annotate("text",x=0.5,y=0.5,label=ct,size=4.5)+coord_cartesian(xlim=c(0,1),ylim=c(0,1),expand=FALSE)+theme_void()}make_line<-function(d,ct,show_y=FALSE){y_range<-range(d$cell_prop,na.rm=TRUE)pad<-diff(y_range)*0.12+0.015ggplot(d,aes(group_label,cell_prop,group=subtype,color=subtype))+geom_line(linewidth=1.0,alpha=0.85)+geom_point(aes(fill=subtype),shape=shape_val[[ct]],size=5.5,alpha=0.50,stroke=0.25)+scale_color_manual(values=pal,name=NULL)+scale_fill_manual(values=pal,guide="none")+scale_y_continuous(limits=c(max(-0.05,y_range[1]-pad),y_range[2]+pad),breaks=pretty_breaks(4))+guides(color=guide_legend(ncol=4,byrow=TRUE))+labs(x=NULL,y=if(show_y)"Cell proportion"elseNULL)+theme_classic(base_size=12)+theme(axis.text.x=element_text(angle=60,hjust=1,vjust=1),axis.line=element_blank(),panel.border=element_rect(color="black",fill=NA,linewidth=0.6),legend.position="bottom")}plot_one_cell<-function(ct,show_y=FALSE){d<-filter(dat,cell_type==ct)make_donut(d)/make_strip(ct)/make_line(d,ct,show_y)+plot_layout(heights=c(0.18,0.08,0.74))}fig<-wrap_plots(plot_one_cell("Fibroblast",TRUE),plot_one_cell("Macrophage"),plot_one_cell("Endothelial cell"),plot_one_cell("B cell"),nrow=1)+plot_layout(guides="collect")+plot_annotation(title="g")&theme(legend.position="bottom",legend.box="vertical",legend.key.width=unit(0.9,"cm"),legend.text=element_text(size=8.5),plot.title=element_text(size=24,face="bold"))ggsave("paired_line_donut_panel_g.png",fig,width=12,height=8.5,dpi=320,bg="white")ggsave("paired_line_donut_panel_g.pdf",fig,width=12,height=8.5,bg="white")

参考链接

  • DOI: https://doi.org/10.1186/s12967-024-05625-6
  • 文章链接: https://pmc.ncbi.nlm.nih.gov/articles/PMC11363495/