高分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 内部亚群在两种状态之间的比例变化。
图片解读
这是一张典型的 细胞亚群组成变化图。
它可以拆成三层:
- 上方环形图:每个大类细胞各有两个环形图,分别对应
Primary tumors和Pre-nodal metastases。 - 中间色块标题:标出当前 panel 属于哪一类细胞,例如 Fibroblast、Macrophage、Endothelial cell、B cell。
- 下方配对折线图:每一条线代表一个细胞亚群,连接该亚群在两种状态下的比例。
读图时重点看线的方向:
- 线向上:说明该亚群在
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_type | subtype | sample_group | cell_prop |
|---|---|---|---|
| Fibroblast | iCAF | Primary tumors | 0.330 |
| Fibroblast | iCAF | Pre-nodal metastases | 0.320 |
| Macrophage | SPP1+ Macrophage | Primary tumors | 0.480 |
| Macrophage | SPP1+ Macrophage | Pre-nodal metastases | 0.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 由三部分上下拼接:
- 环形组成图;
- 彩色标题条;
- 配对折线图。
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/