第一次用ggtree画系统发育树时,我对着屏幕愣了半天——图是出来了,但那些歪歪扭扭的标签挤成一团,根节点和分支怎么看怎么别扭,跟期刊上那种干净利落的进化树完全是两码事。后来我才明白,工具本身从来不是瓶颈,对绘图逻辑的理解才是。ggtree作为R语言里最主流的进化树可视化工具,越用越能体会到它那套“树即数据、图形即图层”的设计有多省心,这也是我决定把ggtree系列经验整理出来的原因。
这一篇先解决最核心的高频需求:怎么把一棵树文件变成一张能看的图,再一步步从基础绘图走到论文级编排。内容包括安装与依赖处理、不同软件输出文件的读入、布局与分支长度参数、标签/节点/分组上色这些最常用操作,以及我在实际项目中撞过的坑。适合刚接触系统发育分析、有一定R基础但面对ggtree一堆图层函数发懵的研究生,也适合想从MEGA/iTOL转到R里做批量可视化的人。
1. 为什么ggtree能成为“事实标准”:先搞懂它的设计思路
1.1 传统绘图函数和图层化思路的根本差异
很多人在用ggtree之前,最先接触的是ape包里的plot.phylo()。ape当然也能画树,两条命令直接出图,简单粗暴。但问题在于:当你需要给某个clade加个高亮背景、给不同分支按分组上色、把末端标签换成点并映射颜色、把节点支持率标到内部节点旁边,甚至想在同一张图里再拼一个热图或柱状图时,plot.phylo的应对方案基本上是“再调一堆内部参数”,或者干脆自己拿低级绘图函数points()、text()、rect()在图上叠加。
而ggtree遵循的是《Grammar of Graphics》那套语法——先有数据,再有映射,最后叠加图层。树仍然是一棵树,但在ggtree眼里,它可以被组织成一张“树形数据表”,每个节点、每条分支都成为一行观测,整棵树的拓扑、分支长度、节点标签、末端标签都成了数据框里的列。这样一来,你就能像用ggplot2处理普通表格一样去处理树:aes()映射、scale_*调色、theme()改样式、facet_*分面,底层全是同一套规则。
我个人的感受是:第一次上手ggtree觉得函数多、文档绕,是因为脑子里还带着“树是一个整体图形”的旧模型;一旦切换到“树是一张可以操作的数据表”,很多函数就变得非常直觉——geom_tiplab()就是在数据表的tip行上画文本,geom_hilight()就是选中某个节点的所有后代行填一个矩形,仅此而已。
1.2 树在ggtree里被“翻译”成了什么
ggtree内部通过fortify()把phylo对象或treedata对象“翻译”成一种叫tbl_tree的data.frame。翻译过来之后的表格中,每一行对应树上的一个节点(包括内部节点和末端tip),常用列包括:
parent:父节点编号node:当前节点编号branch.length:当前节点到父节点的分支长度label:节点标签。tip行是物种/样本名,内部节点行可能是bootstrap值或后验概率isTip:是否为末端节点x/y:经过布局计算后的坐标group等自定义列:如果你用%<+%操作符绑定了外部数据,这里会出现新列
理解了这张“隐形表”,你自己就能解释很多现象:为什么aes(color=group)能直接给不同分组上色?因为表里确实有group这一列。为什么geom_tiplab(aes(label=label))可以自定义显示文本?因为label就是一列。为什么按node高亮一个clade那么容易?因为每个节点的所有后代都能通过parent和node递归查出来。
很多人在网上问“ggtree里能不能像ggplot2那样把树的某一部分数据取出来处理”,答案是可以,但它不是tree$edge,而是:
r复制library(tidytree)
as_tibble(tree)
这行代码会把树转成标准tibble,每一行就是一个节点。这之后你想做筛选、合并、分组统计,全都走dplyr那套管道就够了。这也是ggtree生态比传统进化树绘图工具高一个维度的根本原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从零开始跑通第一张图:安装、读树与基础参数
2.1 安装与依赖环境
ggtree是Bioconductor包,不建议从CRAN装,官方推荐的安装方式是:
r复制if (!requireNamespace("BiocManager", quietly = TRUE))
install.packages("BiocManager")
BiocManager::install("ggtree")
安装时它会自动把treeio、tidytree、ggplot2、ape这些核心依赖一起拉下来。如果你之前装过旧版,建议在安装后检查版本:
r复制packageVersion("ggtree")
ggtree的更新节奏和Bioconductor版本绑定,比如R 4.3对应Bioc 3.18,R 4.4对应Bioc 3.19。
这里特别提醒一步:在Linux服务器上如果编译报错,常见原因不是ggtree本身,而是缺少系统依赖。比如报libxml2相关错误时,先装系统库再重试:
bash复制sudo apt install libxml2-dev libssl-dev libcurl4-openssl-dev
Windows用户一般直接装就能过,但如果你用RStudio的图形设备画图时出现中文字体变方块,或导出PDF时字体重叠,问题不在ggtree而在字体配置,后面的踩坑章节会专门说。
2.2 不同来源的树文件怎么读
做进化分析的人手里,树文件的来源五花八门:可能是IQ-TREE输出的.treefile,可能是BEAST输出的.tre(里面对每个节点都有后验概率和95% HPD区间),可能是MrBayes输出的.con.tre,也可能是RAxML输出的.bestTree。ggtree生态里有一个专门负责读文件的包叫treeio,读进来之后得到的是treedata对象,比phylo多存了节点注释信息。
常用读法:
r复制library(treeio)
library(ggtree)
# Newick格式:最常见,IQ-TREE、RAxML等都能导出
tree <- read.tree("result.treefile")
# NEXUS格式
tree_nexus <- read.nexus("result.nex")
# BEAST输出
beast_tree <- read.beast("beast_output.tre")
# MrBayes输出(一般读consensus树)
mb_tree <- read.mrbayes("result.con.tre")
# 从代码里直接输入一颗简单树
mini_tree <- read.tree(text = "((A:0.2,B:0.3):0.5,(C:0.4,D:0.1):0.2);")
read.tree(text=...)是在本地测试绘图语法时非常方便的办法。我不建议你一开始就直接读自己的大数据集调试参数,先用一颗几片叶子的小树把图层逻辑跑通,再上完整数据,效率高很多。
read.beast和read.mrbayes读出来的treedata对象里,分支上的节点信息都在@data槽位里。如果你用了ape函数去操作它,大概率会报“不能处理S4对象”之类的错误,这时候需要先用ape::as.phylo()转成phylo,再用as.treedata()转回来。这个习惯会影响后面很多操作,养成它不吃亏。
2.3 第一个绘图命令与布局参数
拿到树对象后,最简绘图只有一行:
r复制ggtree(tree)
默认输出是矩形布局,左到右分支,默认去掉背景网格。如果这时候直接ggsave保存,图能看但绝对谈不上好看。先别急着堆图层,把布局参数搞明白更重要。
layout参数定义了整棵树的形状:
| layout | 适合场景 | 说明 |
|---|---|---|
rectangular |
默认矩形树 | 适合分支长度需要直观对比的场景,最常用 |
slanted |
斜线树 | 视觉上更紧凑,适合分支较密的树 |
circular |
圆形树 | 适合tip数量较多的展示型图,分支长度视觉上会失真 |
fan |
扇形树 | circular的局部展开,适合大树的一部分 |
unrooted |
无根树 | 适合展示拓扑关系而非分支长度 |
time |
时间树 | 需要传入时间轴信息 |
举例:
r复制p <- ggtree(tree, layout = "fan", size = 0.8)
另外有一个看起来不起眼但非常实用的参数:branch.length。默认情况下它会用树文件里的分支长度来布局,但有些场景下你想只看拓扑、不想让分支长短影响视觉效果(比如比对外观差异极大的数据集),可以设成branch.length = "none",这时所有末端会等距排列,图形看起来整齐许多。
r复制p_topo <- ggtree(tree, branch.length = "none")
这个参数是我最常用的“救场”参数。有些Beast树的时间标尺跨度极大,根到tip的距离比某几个短分支长几十倍,直接画出来短分支全挤在右边一条线里,根本看不出拓扑。切成“none”之后,拓扑结构一清二楚。
3. 图层堆叠才是精髓:手把手调出能放进论文的图
3.1 标签、节点点、分支三类几何对象
ggtree的绘图逻辑可以简化成三个层次:先画树骨架,再往上面叠几何对象,最后调整主题和坐标。树骨架本身已经由ggtree()完成,接下来最常叠加的是这三类对象。
第一类:末端标签。
r复制p + geom_tiplab(size = 3, offset = 0.02, color = "black")
offset控制标签和末端点的距离,这个值不固定,取决于你树的总跨度。比如分支长度单位是0到1,那offset=0.02就够;如果单位是0到100,offset可能要设成2或5。我一般先画一版看距离,再慢慢微调。hjust控制标签的对齐方向,如果你想做树尖对齐的“缩进式”标签,可以设hjust = 0或负值。
第二类:末端点。
r复制p + geom_tippoint(size = 2, shape = 21, fill = "white", color = "black")
如果只想让每个tip都有一颗小圆点,geom_tippoint()就够了。这里最香的是它完全支持aes()映射,比如按分组给不同tip填不同颜色:
r复制p + geom_tippoint(aes(color = group), size = 3)
只要你的数据里有group这一列(后面会讲怎么绑上去),这一步就能直接做出分组着色。
第三类:内部节点标签。这是系统发育树最常见的需求之一——展示bootstrap值或贝叶斯后验概率。注意geom_nodelabel默认显示的是所有内部节点的label列,而label列里的内容来源取决于你读的是什么文件:Newick文件里的内部节点支持率会被读进来,但有些软件输出时内部节点根本没有标签,读了也是NA。
r复制p + geom_nodelabel(aes(label = label), size = 3, hjust = 0)
有种常见做法是把支持率小于某个阈值的节点标出来,比如只显示bootstrap≥70的节点。利用tbl_tree的“树即数据”特性,可以先过滤再映射:
r复制p + geom_nodelabel(aes(label = ifelse(as.numeric(label) < 70, "", label)), size = 3)
说白了,label列就在数据表里,你想怎么改都行。
3.2 用颜色映射分组信息
进化树里最常出现的可视化需求之一,就是区分不同宿主、不同地理来源、不同谱系。原理非常简单:把分组变量变成树数据里的一列,然后在aes()里映射颜色。
第一步:准备好分组注释表格。典型结构是两列,一列是tip名称(和树里的tip名完全一致),一列是分组名。
r复制group_info <- data.frame(
tip = c("A1", "A2", "B1", "B2"),
group = c("Lineage1", "Lineage1", "Lineage2", "Lineage2")
)
第二步:用%<+%操作符把这张表绑定到树对象上,注意是%<+%,不是ggplot2的%+%。
r复制p <- ggtree(tree) %<+% group_info +
geom_tippoint(aes(color = group), size = 3) +
scale_color_manual(values = c("Lineage1" = "#E64B35", "Lineage2" = "#4DBBD5"))
这样绑完之后,group就成了一张藏在树数据里的列,所有图层都能引用。
如果你还想让末端标签也按分组上色:
r复制p + geom_tiplab(aes(color = group), size = 3)
3.3 节点高亮与支系标记
高亮特定clade是投稿前展示“我们关注的进化支系”的常规操作,常用geom_hilight()和geom_cladelabel()搭配。
geom_hilight需要提供目标clade的node号,常见有两种方式确定node号:
方式一:先用ggtree(tree) + geom_text(aes(label = node))把所有节点编号显示出来,再肉眼查找。适合节点少的树。
方式二:用tidytree::MRCA()查找某几个tip的最近共同祖先节点。
r复制library(tidytree)
node_h <- MRCA(tree, c("A1", "A2"))
p + geom_hilight(node = node_h, fill = "steelblue", alpha = 0.3)
alpha控制在0.2到0.4之间比较合适,太高会盖住分支线。geom_cladelabel则是给这个支系在树外画一条竖线和文字标签:
r复制p + geom_cladelabel(node = node_h, label = "Focus clade",
align = TRUE, offset = 0.05, color = "steelblue")
3.4 主题、坐标范围与导出参数速查
图形画完,真正影响“论文感”的往往是主题和导出设置。ggtree自带的theme_tree()会把背景、网格全部清掉;theme_tree2()在保留树结构的同时会显示坐标轴刻度,适合展示分支长度或时间轴时用。
r复制p + theme_tree()
p + theme_tree2()
很多时候图右侧标签被截断,根本原因是ggplot2默认坐标范围没留够,直接xlim()扩展即可:
r复制p + xlim(0, max_x + 0.5)
或者用ggplot2的expand参数微调。但要注意,xlim扩展不会影响geom_tiplab的坐标计算,扩展距离要根据分支长度量级自己试。
导出图片我用最多的配置:
r复制ggsave("tree_final.pdf", p, width = 8, height = 6, dpi = 300, limitsize = FALSE)
PDF格式适合投稿前的矢量编辑,PNG格式适合快速预览。如果你导出后感觉图被拉伸变形,记得先调整width和height的比值;tip多的树天然需要窄而高的画布,tip少的树适合宽而扁的画布。这个比例调对了,图的气质一下就不一样。
4. 真实踩坑记录:节点编号、中文显示、布局裁切
4.1 定位节点编号的正确套路
geom_hilight()、geom_cladelabel()、geom_range()这些函数都需要填node参数,而“我到底该填哪个数字”是新手提问区保留节目。最容易踩的坑是:树经过ggtree()重排布局后,你看到的tip顺序和tree$tip.label里的顺序未必一致。如果照着原始编号标clade,高亮区域很容易飘到完全无关的支系上。
我现在的固定做法是:不管树多大,先用一组“调试图层”把节点号和tip名同时显示出来,确认之后再删掉调试代码。
r复制ggtree(tree) +
geom_tiplab(aes(label = paste0(label, "_", node)), size = 3) +
geom_nodelab(aes(label = node), size = 3, color = "red")
这样每个tip会显示“名字_节点号”,内部节点会显示红色节点号,基本不会认错。对于几十个tip以内的树,这一招非常直观。
节点很多时就要用MRCA()按tip集合找祖先节点,前面已经给过例子。再补充一个用法:如果你想高亮“某个节点以下所有后代”,但不确定这个节点编号,可以先用tidytree::offspring()函数把后代tip全部列出来,再往下传:
r复制library(tidytree)
tips_in_clade <- offspring(tree, node = node_h, type = "tips")
这也算是“树即数据”思想带给我们的冗余安全性。
4.2 中文字体变方块和乱码的根治
R在Linux环境下的字体问题有多烦,不用我多说了。常见场景:终端里跑R脚本输出PDF,图的标题和tip标签里如果有中文,导出的PDF里全是一个个小方块。这不是ggtree的锅,是R的默认字体配置里不含可用中文字体。
跨平台最省心的方案是showtext包:
r复制library(showtext)
showtext_auto()
showtext_auto()开启后,R绘图时会把系统中的字体自动嵌入到图形里,不需要手动par(family=...)。在Windows、macOS、Linux上都能统一产出好看的中文标签。
如果不想引入额外包,Windows下也可以用:
r复制windowsFonts(Arial = windowsFont("Arial"))
但这个方法只对Windows有效,可复现性差。发出去的脚本别人拿到不一定能跑出同样的效果,而showtext可以保证不同系统间结果一致。特别是在RStudio里画图时,开了showtext_auto()后中文预览和导出PDF就能保持一致,非常省事。
4.3 图被截断、标签消失的排查思路
“为什么我的图右边标签被切掉了?”——这个问题我至少回答过十次。九成原因是ggplot2的坐标范围只覆盖到树本身的X范围,而geom_tiplab()默认把标签往外画,标签长度超过了坐标上限,于是被切掉了。
排查优先级:
- 先
xlim()扩坐标范围,看标签是否恢复。如果是,说明只是坐标不够。 - 如果标签还是丢,检查
geom_tiplab(aes(label=sample_id))里的sample_id是否是NA。很多树文件里的tip标签是空的或不是字符型,比如读进来的label是Factor,需要先as.character()。 - 如果只画了一部分tip,检查你的树对象里是否真的有这么多tip,比如
Ntip(tree)显示15,但图里只有10条线,那大概率是读树时格式出了问题,而不是绘图层的锅。
还有一个隐蔽问题:读入的树本身含有为0的分支长度。branch.length=0在矩形布局里会让两条分支重叠,视觉上像“少了一条线”。排查这种问题,先设branch.length = "none"看拓扑是否完整。如果拓扑完整但加上分支长度后部分分支重叠,那就是数据本身有零长分支,需要在分析阶段处理。
4.4 常见报错的对照与修复
| 报错信息 | 出现原因 | 处理方法 |
|---|---|---|
object of type 'S4' is not subsettable |
输入的是treedata对象,用了$取列 |
改用@data,或先ape::as.phylo()转phylo再操作 |
Do not know how to deal with objects of class ... |
输入对象不是phylo/treedata |
检查读树函数是否成功,确认对象类型是phylo |
node not found |
传给geom_hilight/geom_cladelabel的节点号不存在 |
用ggtree(tree) + geom_text(aes(label=node))复核节点号 |
all(!is.na(labels))类报错 |
tip.label里有NA或空字符串 | 清洗数据,保证所有tip都有名称 |
| 图形预览时中文字体方块 | R找不到合适的中文字体 | 调用showtext包或配置系统字体 |
其中S4对象那个报错是最容易让人懵的。因为read.beast()读进来的是treedata对象,很多人习惯性地写tree$node或者tree$edge,结果直接报错。处理办法记住一句口诀:phylo用$,treedata用@。
5. 关联外部数据与进阶布局:让树真正“表达信息”
5.1 用 %<+% 把外部数据绑定到树
树本身只有拓扑和分支长度,但一张论文图往往需要同时展示“样本来自哪里”“表达量高低”“是否处于某个状态”等信息。ggtree生态用%<+%操作符解决这个问题,它能把一个以tip名为第一列的data.frame整张绑到树数据上,之后所有图层都能引用其中的列。
举个例子,你在研究不同分离株的耐药表型:
r复制meta <- data.frame(
sample_id = c("S001", "S002", "S003"),
resistance = c("R", "S", "R"),
region = c("Asia", "Europe", "Africa")
)
p <- ggtree(tree) %<+% meta +
geom_tippoint(aes(color = resistance), size = 3) +
geom_tiplab(aes(label = region), size = 3, offset = 0.05)
绑定之后,resistance和region就像原生列一样可以被aes()引用。这是ggtree最强大的地方:树形的骨架,加上任意元数据的双层表达。
如果你觉得一张图信息量不够,还可以用facet_plot()在树旁边展开一个额外的面板,比如在每个tip后面画一个表达量的条形图。
r复制p + facet_plot("expression", data = expr_df, geom = geom_segment,
mapping = aes(x = 0, xend = expression, y = y, yend = y))
5.2 多棵树对比与分面展示
有些分析需要并排比较多棵树(比如不同基因构建的系统发育树),做法不复杂:把多棵树合并成一个multiPhylo对象,再用facet_wrap分面。
r复制library(ape)
trees <- c(phy1, phy2, phy3)
class(trees) <- "multiPhylo"
ggtree(trees) + facet_wrap(~.id, ncol = 1) + geom_tiplab(size = 3)
注意multiPhylo要求所有树用同一批tip,否则分面后标签会乱。
对于单棵树内部想按某个分组把不同clade隔开显示,可以用scaleClade()调整某个clade的缩放比例,或者用ggtree::rotate()旋转子树的连接方向,具体效果取决于你的拓扑结构。这里先不展开,后续写“ggtree进阶”时我再单独讲。
5.3 圆形树、扇形树与热图的搭配
当样本量变大(比如超过50个tip),矩形树右侧的标签会密密麻麻,这时很多人的第一选择是layout="circular"或layout="fan"。
r复制ggtree(tree, layout = "circular") +
geom_tiplab(size = 2, offset = 0.02)
圆形树有个视觉陷阱:离圆心越远的分支长度在视觉上被拉伸得越厉害,所以如果你的树分支长度跨度很大,用circular会传递错误的定量信息。这时候扇形树(fan)稍微好一点,但同样要注意标注“分支长度比例尺”。
如果要在树的周围加一层热图,最常用的配套是ggtree内部的gheatmap()。用法也简单,只需传入矩阵格式表达数据即可。
r复制p <- ggtree(tree, layout = "fan")
gheatmap(p, expr_matrix, offset = 0.1, width = 0.3,
colnames_angle = 90, colnames_offset_y = 4)
需要注意的是,gheatmap和facet_plot在数据规模较大时都要留意性能和排版重叠问题。实测下来,超过200个tip时,热图列名很容易糊成一团,要么旋转角度,要么干脆不显示列名,图注里说明。
6. 从一棵树到一张能提交的figure:我的完整实操习惯
6.1 我的固定工作流
这几年的实际项目中,我总结了一个很稳定的ggtree工作流,每一步都不花哨,但能保证最后出的图基本不用返工。
第一步,先读入树并做一个极简检查。不用任何美化,直接ggtree(tree) + geom_tiplab(),确认拓扑是否正确、tip数量是否对、是否有明显异常分支。
第二步,绑定注释信息并画一版“全要素图”。把分组合并、tip点、节点支持率、clade高亮全部堆上去,不管好不好看,先把所有信息都画出来看一遍。
第三步,删掉不需要的要素,确定视觉主体。投稿图不是信息越全越好,一个figure只需要讲一个核心结论。如果树的重点是大谱系分类,那tip点颜色就比节点支持率重要;如果是展示分支可信度,那节点高亮和bootstrap值优先。
第四步,调整画布比例和导出参数。tip数量决定了画布高宽比,树的分支长度跨度决定了xlim扩展量。最终导出时我习惯width=8, height=max(4, Ntip*0.18),再根据实际效果微调。
6.2 让图可复现的几个细节
一个在实验室内部反复传阅的脚本,最重要的素质是可复现。我踩过的坑包括:不同R版本下ggtree默认字体变化导致中文标签错位;不同人的系统里showtext包没安装导致PDF字体不一样;数据文件路径是相对路径但有人把脚本拷到别处就找不到文件。
我现在的做法很简单:脚本开头固定设置随机种子、固定showtext字体、用here::here()定位数据路径,并且在脚本最后加一行sessionInfo(),把R包版本记录下来。这些前置工作看着和绘图无关,但三个月后当你需要重出插图时,就会感谢当初多写的这三行。
6.3 根据数据规模调整绘图策略
树的大小不同,适合的布局和参数区间完全不同。我是这样分类的:
- 20个tip以内:矩形布局,tip标签直接展示,节点编号用数字默认可读。
- 20到100个tip:推荐扇形或矩形,标签字号控制在2-3,加
geom_tippoint帮助区分。 - 100个tip以上:必须用
layout="fan"或circular,标签字号降到2以下,同时降低geom_tiplab透明度或只标部分关键tip,否则整张图就是一堆黑色文字糊在一起。
最近接手过一个约800个tip的数据集,我的做法是先在ggtree里把树画出来做整体概览,然后挑出核心clade用ggtree::scaleClade()单独放大,这样既保留了整体背景,又能看清局部细节。对于大体量树,别指望一张图解决所有问题,拆分展示才是正路。
至于我个人最后想分享的实操体会:ggtree的真正优势不是某个函数多好用,而是它的“数据表”思维让你敢于对树做任何自定义操作。当你能把树当成一张普通表格看待时,所有ggplot2的花活都能用到树上——这在传统绘图工具里几乎不可能。建议所有刚接触ggtree的人,先花二十分钟跑通一张基础图,再用as_tibble()看一眼树背后的数据结构,之后的路会顺畅许多。下一篇我会继续写树形数据的合并、裁剪和内部节点注释,这些在真实项目中比想象中更常用。
