Phylogenetic tree visualization and formatting with ggtree (R) or iTOL (web). Use when rendering a phylogenetic tree as a figure, choosing tree layout, coloring branches or labels by taxonomy, collapsing clades, displaying support values, or adding overlays to a tree. Do NOT load for tree inference (use protein-phylogeny skill) or domain annotation (future separate skill).
Conventions for rendering phylogenetic trees using ggtree (R/Bioconductor) or iTOL (Interactive Tree of Life, web-based).
Ask the user which backend to use based on their needs:
| Backend | Best for | Output | Language |
|---|---|---|---|
| ggtree | Publication figures, full programmatic control, offline use | PDF/PNG/SVG | R (.qmd script) |
| iTOL | Interactive exploration, quick iteration, web sharing, UI tweaking | Web + PDF/SVG/PNG exports | R (.qmd annotations) + Python (.qmd upload) |
| Feature | ggtree | iTOL |
|---|
| Interactive exploration | No | Yes (web UI) |
| Label alignment control | Full (programmatic) | Limited (UI toggle only, not via API) |
| Collapse triangle labels | Manual geom_text() | Built-in LABELS for internal nodes |
| Circular label positioning | Complex (manual angle computation) | Automatic |
| Branch length display | Yes (phylogram/cladogram toggle) | Yes (via UI) |
| Offline/reproducible | Fully offline | Requires iTOL API + internet |
| Two-script workflow | No (single .qmd) | Yes (R .qmd annotations + Python .qmd upload) |
Before rendering, validate the tree file. Tree-formatting scripts should include a validation step early on.
ape::read.tree() (R) or ete3.Tree() (Python)ape::is.rooted())ape::is.binary())| characters (breaks iTOL), spaces, unusual characters#| label: validate-tree
library(ape)
tree_path <- here("data/phylogenetics/tree.treefile")
stopifnot("Tree file not found" = file.exists(tree_path))
tree <- read.tree(tree_path)
cat("Tips:", Ntip(tree), "\n")
cat("Rooted:", is.rooted(tree), "\n")
cat("Binary:", is.binary(tree), "\n")
# Zero-length branches
if (!is.null(tree$edge.length)) {
n_zero <- sum(tree$edge.length == 0)
if (n_zero > 0) cat("WARNING:", n_zero, "zero-length branches\n")
}
# Tip label issues (pipe breaks iTOL)
has_pipe <- grepl("\\|", tree$tip.label)
if (any(has_pipe)) {
cat("WARNING:", sum(has_pipe), "tips contain '|' — will break iTOL annotations\n")
}
#| label: validate-tree
from ete3 import Tree
tree_path = PROJECT_ROOT / "data/phylogenetics/tree.treefile"
assert tree_path.exists(), f"Tree file not found: {tree_path}"
tree = Tree(str(tree_path))
tips = tree.get_leaf_names()
print(f"Tips: {len(tips)}")
# Check for pipe characters
pipe_tips = [t for t in tips if "|" in t]
if pipe_tips:
print(f"WARNING: {len(pipe_tips)} tips contain '|' — must relabel before iTOL")
Help the user select the right visualization. Ask about purpose and tree size, then recommend from the options below.
| Type | Best for | Tips | Key features |
|---|---|---|---|
| Collapsed rectangular phylogram | Large family trees; showing branch-length variation and gene family structure | 250-2000+ | Collapsed pure clades, branch lengths, selective labels |
| Collapsed rectangular cladogram | Large family trees; topology focus, cleaner labels | 250-2000+ | Same as phylogram but no branch lengths, narrower page |
| Collapsed circular | Large trees; compact overview showing overall structure | 250-2000+ | Circular layout, collapsed clades, optional selective labels |
| Simple rectangular phylogram | Small-medium trees where all tips are readable | < 250 | All tips labeled, no collapsing needed |
| Unrooted | Networks, showing relationships without root assumption | Any | No directionality implied |
Gather these decisions before writing any code:
collapse_groups parameter controls which
taxonomic groups are eligible. Common choices:
c("Bilateria") — only collapse bilaterians (keeps sponges/cnidarians expanded)c("Bilateria", "Protostomia", "Deuterostomia") — collapse specific groupsNULL — all groups eligible for collapsing"Bilateria (36 tips: LAMA1, LAMA2, LAMB1)". This ensures key gene family
members remain visible even when the clade is collapsed.ITOL_PROJECT env var
or hardcode in the upload script.All ggtree templates are Quarto .qmd documents following the project's data
science conventions (YAML frontmatter with status field, git hash, BUILD_INFO.txt).
Reference template: ~/.claude/skills/tree-formatting/templates/ggtree/collapsed_rectangular.qmd
This template is a complete, runnable .qmd with all tuned style parameters. Copy it
into the project's scripts/ directory and adapt the sections marked PROJECT-SPECIFIC:
collapse_groups parameter (which taxonomic groups to collapse)The template handles: tree loading, midpoint rooting, pure-clade collapsing by taxonomic group, branch coloring by taxonomy, all visible tips labeled, model species gene names on collapsed triangle labels, formula-based page sizing, and PDF output.
Key features:
INCHES_PER_TIP = 0.12, height = max(8, n_visible * INCHES_PER_TIP)collapse_groups parameter — controls which taxonomic groups are eligible for
collapsing (e.g., c("Bilateria") to only collapse bilaterians, or NULL for all)"Group (N tips: GENE1, GENE2, ...)" so key gene family members remain visiblemax(pre_data$x[tip_ids]) (triangle tip),
not at internal node x (triangle base)Reference template: ~/.claude/skills/tree-formatting/templates/ggtree/collapsed_circular.qmd
Same structure as rectangular — adapt PROJECT-SPECIFIC sections. Produces:
Critical circular gotcha: Labels must be positioned BEFORE collapse() is called.
The template handles this by computing angles from y-position (y / max_y * 360),
flipping text on the left half of the circle, and using geom_text() with explicit
angle/hjust values instead of geom_tiplab2().
For simple rectangular or unrooted trees, no template exists yet. Build from ggtree