Glowing phylogenetic tree diagram with annotated branches and coloured tips

A phylogeny is a claim about evolutionary history, and a figure is how that claim gets read. The same tree can look like a confident result or an obvious artefact depending on where you rooted it, whether support values are visible, and whether the tips are in a meaningful order. None of those are cosmetic choices.

This tutorial covers the two tools that cover most real needs: ggtree in R for scripted, reproducible publication figures, and iTOL for interactive annotation that a collaborator can explore in a browser.

Read the tree, then check it before drawing anything

if (!requireNamespace("BiocManager", quietly = TRUE)) install.packages("BiocManager")
BiocManager::install(c("ggtree", "treeio", "ggtreeExtra"))
install.packages(c("ape", "phytools", "ggplot2", "dplyr"))

library(ggtree)
library(treeio)
library(ape)
library(ggplot2)
library(dplyr)

tree <- read.tree("trees/amp_16s.treefile")
tree

Print the object before plotting. Three facts come out immediately: the number of tips, whether it is rooted or unrooted, and whether branch lengths exist. If Nnode is missing or all branch lengths are zero, you are holding a topology rather than a tree, and any figure will be a cladogram — which is fine, as long as you know that.

is.rooted(tree)      # FALSE means you must root before interpreting
is.binary(tree)      # FALSE means multifurcations (polytomies) exist
range(tree$edge.length)   # check for zero or absurd branch lengths

Rooting is an analytical decision, not a formatting one

An unrooted tree has no evolutionary direction: the root can be placed anywhere without changing the likelihood. Where you place it changes what the figure claims about ancestry, so the choice needs a reason.

MethodWhen to use itRisk
OutgroupA defensible outgroup existsWrong outgroup silently distorts the whole tree
MidpointNo outgroup, roughly clock-like dataAssumes equal rates; can mislead on rate-variable data
Rooted by the inference toolIQ-TREE or BEAST already rooted itCheck it did what you assume
# Midpoint rooting — fast, and wrong when rates vary wildly
tree_mid <- midpoint(tree)   # from phytools

# Outgroup rooting — preferred when an outgroup is justified
tree_out <- root(tree, outgroup = "Outgroup_taxon", resolve.root = TRUE)

# Check what changed
is.rooted(tree_out)
ggtree(tree_out) + geom_tiplab(size = 2.5) + geom_treescale()

If the tree came from IQ-TREE with -o, or from a Bayesian run, it may already be rooted. Rooting it again with midpoint() can silently overturn a deliberate choice — always confirm before applying a second rooting step.

A first figure that is already publication-shaped

p <- ggtree(tree_out, layout = "rectangular", linewidth = 0.4) %<+% metadata +
  geom_tiplab(aes(color = phylum), size = 2.6, align = TRUE, linesize = 0.2) +
  geom_treescale(x = 0, y = 0, width = 0.02, fontsize = 3) +
  scale_color_brewer(palette = "Set2") +
  theme(legend.position = "right")

ggsave("figures/tree_base.pdf", p, width = 7, height = 9)

%<+% attaches a metadata table by matching row names to tip labels. That one operator is why ggtree is worth learning: once metadata is attached, every aesthetic — colour, shape, size — can be mapped from your data rather than edited by hand.

Tip labels are the biggest source of ugly figures

# Extract the labels and check them before blaming the plot
tree_out$tip.label[1:10]
# Rename only for display; keep the original tree intact
tree_disp <- tree_out
tree_disp$tip.label <- sub("^([^_]+_[^_]+).*", "\\1", tree_disp$tip.label)

ggtree(tree_disp) + geom_tiplab(size = 3)

For trees past about 80 tips, drop labels and map identity to colour or a heatmap strip instead. A figure with 300 legible labels does not exist; a figure with 300 coloured tips and a legend does.

Showing support values honestly

Bootstrap or posterior values are the evidence for each split. Omitting them turns a topology into an assertion; showing all of them at every node produces an unreadable figure. Show the ones that matter, and say what threshold you used.

# Node labels, filtered to what is worth showing
ggtree(tree_out) +
  geom_text2(aes(subset = !isTip & as.numeric(label) > 70,
                 label = round(as.numeric(label))),
             size = 2.2, nudge_x = -0.004, hjust = 1) +
  geom_tiplab(size = 2.4)

Note geom_text2 rather than geom_text: the 2 variants operate on nodes as well as tips. ge… variants like geom_nodepoint and geom_rootpoint follow the same convention, and using the tip-only version is a common reason node annotations appear in the wrong place.

A support value below roughly 70 bootstrap (or 0.95 posterior) should be shown as a collapsed node or flagged in the caption, not left to look like a resolved split. The tree does not contain the resolution; the figure should not imply that it does.

Circular layouts, groups and heatmaps

# Circular layout with a highlighted clade
ggtree(tree_out, layout = "circular") +
  geom_tiplab(size = 1.8, offset = 0.02) +
  geom_hilight(node = 42, fill = "steelblue", alpha = 0.2) +
  geom_cladelab(node = 42, label = "Clade A", align = TRUE, offset = 0.15)

Circular layouts buy space at the cost of readability — distances become harder to compare visually. Use them when you have many tips and the message is about grouping rather than branch length, and say so in the caption.

Attaching a trait heatmap

library(ggtreeExtra)
p2 <- ggtree(tree_out) + geom_tiplab(size = 2)

p2 + geom_fruit(data = meta_numeric, geom = geom_tile,
                mapping = aes(y = tip, x = trait, fill = value),
                offset = 0.08) +
  scale_fill_viridis_c()

The heatmap columns should map to tip labels in the same order as the tree's tips. A silent reordering produces a figure where the colours do not correspond to the branches, which is worse than having no heatmap at all. Assert the match explicitly before plotting.

Exporting figures that survive a journal

# Vector for line art; PDF is safest for journals
ggsave("figures/tree.pdf", p2, width = 8, height = 10, device = cairo_pdf)

# High-resolution raster only if the journal insists
ggsave("figures/tree.tiff", p2, width = 8, height = 10, dpi = 300, compression = "lzw")

Save the R script and the exact tree file with the figure. A tree figure that cannot be regenerated from a stored tree is a picture, not a result.

When iTOL is the better choice

iTOL excels at interactive exploration and at metadata annotation that a collaborator wants to toggle. Upload a Newick file, then attach annotation datasets in its plain-text format.

# DATASET_STYLE: colour branches and tips by group
DATASET_STYLE
SEPARATOR TAB
DATASET_LABEL	Group colours
COLOR	#1f77b4
DATA
branch	clade_1	label	#1f77b4	normal	1
branch	clade_2	label	#d62728	normal	1
# DATASET_COLORSTRIP: a categorical strip beside the tree
DATASET_COLORSTRIP
SEPARATOR TAB
DATASET_LABEL	Phylum
COLOR_BRANCHES	0
DATA
SAMPLE_1	#1f77b4	Firmicutes
SAMPLE_2	#ff7f0e	Bacteroidetes

The advantage over ggtree is that a reviewer can hover, expand collapsed clades and download a vector export themselves. The disadvantage is that annotation lives in the web session: export both the figure and the dataset files, or the figure is no longer reproducible.

NeedUse
Scripted, version-controlled, batched figuresggtree
Many metadata layers, interactive explorationiTOL
Figure embedded in a manuscript pipelineggtree + ggsave
Collaborator needs to poke at the treeiTOL export link plus dataset files

Why this matters beyond the figure

Every choice in this tutorial — rooting, what support to show, whether to collapse unresolved nodes — is an interpretive statement about the data. A tree figure is the part of a phylogenetics study most people will actually read, which makes it the part most worth getting right. If you want to work through the inference side, from alignment to model selection to bootstrap interpretation, Ampersand Academy covers that sequence in detail.

Practice

  1. Read a tree, check whether it is rooted and binary, and report the branch length range.
  2. Produce the same figure with midpoint and outgroup rooting and write one sentence on how the interpretation differs.
  3. Filter support labels to values above 70 and explain why showing all of them is worse.
  4. Add a two-column metadata heatmap and verify the tip order matches the tree.

ggtree or iTOL for a publication figure?

ggtree if the figure must be regenerated from code as part of an analysis pipeline, because the script is the record. iTOL if you need rapid interactive annotation and many metadata layers, or want collaborators to explore the tree. Reproducible publication work usually ends up in ggtree.

How should I root my tree?

Use a justified outgroup when one exists, since that encodes an explicit assumption about ancestry. Use midpoint only when the data are roughly clock-like and no outgroup is available, and state the choice. Never re-root a tree that your inference tool already rooted without knowing why.

Why are my bootstrap values missing from the plot?

Either the tree file does not contain them, which happens when a Newick file was exported without support labels, or you plotted with tip-only geoms. Check the raw Newick text for numbers on internal branches, then use geom_text2 so node labels are rendered.

How do I show only high-support nodes?

Filter inside the aesthetic with subset, for example subset = !isTip & as.numeric(label) > 70. Values below your threshold should be collapsed or mentioned in the caption rather than drawn as though the split were resolved.

Can ggtree handle a tree with thousands of tips?

It can, but the figure becomes unreadable long before that. Beyond roughly 200 tips, drop tip labels, encode identity and traits with colour, and consider a circular layout. Rendering time grows too, so test on a subset first.

How do I annotate tips with metadata in ggtree?

Build a data frame whose row names match the tree tip labels exactly, then attach it with the %<+% operator. Values become available as aesthetics in the plot, so you can map colour, shape or size directly from your metadata.

What file formats should I export?

PDF for vector line art, since journals prefer scalable formats, and TIFF at 300 dpi only when a raster is required. Avoid PNG for trees: thin branch lines and small support labels degrade badly at screen resolution.

Last updated on · Written by