phylogenetics/tree-visualization/SKILL.md
Draw and export phylogenetic trees with Bio.Phylo plus matplotlib, and route rich figures to ggtree, ETE4, or iTOL. Covers why a tree figure is an argument not a neutral picture, the cladogram-vs-phylogram-vs-chronogram choice that hides or reveals rate and time, how ladderization manufactures a false arrow of progress, why an unlabeled support number always flatters the result (bootstrap vs posterior vs SH-aLRT vs UFBoot are different scales), why Bio.Phylo silently drops BEAST HPD bars so annotated Bayesian trees must go through treeio plus ggtree, and the tip-count and raster-vs-vector thresholds for legible publication figures. Use when drawing a tree, choosing a layout, coloring branches, showing or labeling support, exporting vector figures, or deciding a drawing tool. Routes annotation-preserving reads to tree-io, and rooting and ladderizing to tree-manipulation.
npx skillsauth add GPTomics/bioSkills bio-phylo-tree-visualizationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference examples tested with: BioPython 1.83+, matplotlib 3.8+. Rich-figure alternatives: ggtree 3.12+ / treeio 1.28+ / ggtreeExtra 1.14+ (Bioconductor), ete4 4.x (Python), iTOL v6 (web), FigTree (desktop).
Before using code patterns, verify installed versions match. If versions differ:
pip show biopython then help(Phylo.draw) to check signaturespackageVersion('ggtree') then ?ggtree to verify layout and geom argumentsIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
Bio.Phylo draw() renders rectangular layouts only and parses no BEAST/MrBayes annotation block; circular/fan/unrooted layouts and HPD bars require ggtree, ETE4, or iTOL. The ETE3->ETE4 migration changed APIs; introspect rather than assume ETE3 signatures.
"Draw and export my tree figure" -> Render a topology under a chosen layout and branch-length encoding, decide which support to show and how to label it, and export in a format that survives print.
Phylo.draw(tree, axes=ax) (Bio.Phylo + matplotlib)Scope: drawing, styling, annotating, and exporting tree figures, and choosing the drawing tool. Reading/converting files and preserving [&...] annotations before plotting -> tree-io. Rooting, ladderizing, pruning, collapsing low-support nodes -> tree-manipulation. ggplot2 grammar, themes, and ggsave underlying ggtree -> data-visualization/ggplot2-fundamentals. Composing a tree plus aligned panels into one figure -> data-visualization/multipanel-figures.
A phylogenetic tree figure is a rhetorical and modeling artifact, not a neutral picture of the data. A Newick or Nexus file encodes a topology, optionally branch lengths, optionally one node label; it does NOT encode which child is drawn on top, whether the horizontal axis means evolutionary distance or just nesting depth, where the root sits, the aspect ratio, or which of several support measures a bare "95" refers to. Every one of those is supplied by the person drawing the tree, so the figure is the data filtered through a dozen interpretive choices the reader cannot see. Four argument-level decisions dominate, and the same file can be drawn to tell mutually contradictory stories that are all technically correct renderings of one topology:
The skill's job is to make these choices conscious and DECLARE them in the caption, not to default into them. Default ladderization plus cladogram-ish rendering plus unlabeled nodes is the path of least resistance, and it is an argument the user did not intend to make.
The central decision is which drawing tool to reach for, and it is coupled to the I/O decision: a tool that flattens the tree to topology plus one label cannot draw BEAST HPD bars or posteriors it cannot represent. The plotting path through treeio + ggtree is the same I/O choice made in tree-io.
| Tool (lang) | Citation | Layouts and annotation power | When to choose |
|-------------|----------|------------------------------|----------------|
| Bio.Phylo + matplotlib (Py) | Talevich 2012 | rectangular phylogram/cladogram only; one label per node via label_func/branch_labels; no circular/unrooted, no BEAST-block parse | a fast scripted look inside a Python pipeline; headless/CI rendering of simple trees; NOT publication figures with metadata or HPD bars |
| ggtree + treeio + ggtreeExtra (R) | Yu 2017; Wang 2020; Xu 2021 | rectangular, slanted, circular, fan, radial/unrooted; %<+% attaches a data.frame, geom_tiplab/geom_nodelab/geom_cladelab, gheatmap, geom_facet (Cartesian) or geom_fruit rings (circular); treeio reads BEAST/MrBayes/IQ-TREE so HPD bars and posterior plot directly | the publication standard whenever a figure integrates metadata, rich/dual support, HPD bars, or aligned panels; the only clean path for BEAST HPD bars and posterior |
| ETE4 (Py; ETE3 paper) | Huerta-Cepas 2016 | rectangular and circular; programmatic per-node NodeStyle/faces and layout functions; integrated NCBI taxonomy | styling thousands of nodes by rule, taxonomy-driven annotation, or rendering many trees headlessly in Python |
| iTOL v6 (web) | Letunic 2024 | rectangular, circular, unrooted, very large trees; tab-delimited dataset templates for color strips, heatmaps, bars, binary symbols, clade collapse; SVG/PDF/PNG/EPS export | large trees and template-based metadata with no code; collaborative web-driven polished figures |
| FigTree (desktop GUI) | Rambaut (software) | rectangular, polar, radial; reads BEAST NEXUS node annotations to display HPD bars and posterior on screen; manual rotate/collapse/color | interactive inspection of a single BEAST/MrBayes tree before scripting the final figure; not reproducible, use for exploration not the pipeline |
Decision rule: large tree or no-code web annotation -> iTOL v6. Publication figure with metadata, dual support, HPD bars, or aligned heatmaps -> ggtree + treeio (+ ggtreeExtra for circular rings). Programmatic styling of thousands of nodes or NCBI-taxonomy annotation -> ETE4. Fast topology look from a Python script -> Bio.Phylo. Interactive rooting/inspection of one BEAST tree -> FigTree, then move the reproducible figure to ggtree. The recurring mistake is defaulting to Bio.Phylo because the pipeline is Python, then discovering it cannot draw a circular layout, place a heatmap, or show the BEAST HPD bars that are the whole point.
| Layout | Reveals | Hides or distorts | Good for | Bad for | |--------|---------|-------------------|----------|---------| | Rectangular phylogram | branch lengths read directly off a linear axis; honest distance comparison | tip labels overplot past a few hundred tips; uses vertical space | the default when length matters and tip count is moderate (<~150) | very large trees | | Slanted / triangular | compactness; quick topology read | the diagonal implies a ladder/progression; imprecise length reading | quick schematic topology views | precise branch-length comparison | | Circular / fan | hundreds to thousands of tips in one panel; metadata rings | radial distortion -- the same length subtends a larger arc near the rim, so distance is perceptually compressed near the root; near-root structure cramped | big trees where the message is broad structure plus aligned metadata | quantitative branch-length comparison | | Unrooted / radial | honestly shows NO assumed root or time direction; overall shape and long-branch outliers | no time direction; clade membership harder to trace; clusters over-read as clades | exploratory views, "no root committed" | directional stories; any "basal/early-diverging" claim | | Chronogram (time-tree) | node timing on a time axis | requires a clock model and calibrations the reader must trust; without HPD bars timing looks falsely precise | dated BEAST/treePL/MCMCtree trees where timing is the message | any tree lacking a clock; never draw without age uncertainty bars |
Circular is still a ROOTED tree bent into a ring (center = root = past); unrooted/radial explicitly refuses a root. Presenting an unrooted radial tree and narrating "X is basal" is a contradiction.
Quick text and ASCII inspection, no figure needed:
from Bio import Phylo
tree = Phylo.read('tree.nwk', 'newick')
print(tree) # indented text summary
Phylo.draw_ascii(tree) # ASCII-art diagram, useful in a terminal or log
Draw to a vector file (always pass an axes and do_show=False for headless/scripted use):
from Bio import Phylo
import matplotlib.pyplot as plt
tree = Phylo.read('tree.nwk', 'newick')
tree.ladderize() # legibility only; ordering carries NO phylogenetic meaning -- say so in the caption
fig, ax = plt.subplots(figsize=(10, 8))
Phylo.draw(tree, axes=ax, do_show=False)
ax.set_title('Phylogenetic tree (phylogram, branch length = subs/site)')
fig.savefig('tree.pdf', bbox_inches='tight') # vector: text and lines stay sharp at any size
plt.close(fig)
Label tips, and show support with its measure named (never a bare integer):
def tip_only(clade):
return clade.name if clade.is_terminal() else ''
def support_label(clade):
# the measure MUST be stated in the legend/caption; here values are bootstrap percentages
if not clade.is_terminal() and clade.confidence is not None:
return f'{clade.confidence:.0f}'
return ''
fig, ax = plt.subplots(figsize=(12, 10))
Phylo.draw(tree, axes=ax, do_show=False, label_func=tip_only, branch_labels=support_label)
ax.set_title('Bootstrap support shown at internal nodes')
fig.savefig('supported_tree.svg', bbox_inches='tight')
plt.close(fig)
Color branches by group (convert to phyloXML for native color support):
from Bio.Phylo.PhyloXML import BranchColor
xtree = tree.as_phyloxml() # phyloXML carries branch color through draw()
for clade in xtree.find_clades():
if clade.name and clade.name.startswith('Homo'):
clade.color = BranchColor.from_name('red')
fig, ax = plt.subplots(figsize=(10, 8))
Phylo.draw(xtree, axes=ax, do_show=False)
fig.savefig('colored_tree.pdf', bbox_inches='tight')
plt.close(fig)
Scale the panel to tip count so labels stay legible, and drop the axis frame:
n_tips = len(tree.get_terminals())
height = max(8, n_tips * 0.25) # ~0.25 in/tip keeps ~6-8 pt labels from colliding
fig, ax = plt.subplots(figsize=(10, height))
Phylo.draw(tree, axes=ax, do_show=False)
ax.axis('off')
fig.savefig('scaled_tree.pdf', bbox_inches='tight')
plt.close(fig)
For circular/fan/unrooted layouts, metadata heatmaps, dual support, or BEAST HPD bars, Bio.Phylo cannot help -- route to ggtree + treeio (R), ETE4, or iTOL.
Trigger: Rendering a constraint tree, a supertree, or a tree whose lengths are non-comparable with the horizontal axis proportional to length.
Mechanism: The phylogram geometry asserts a quantitative claim about evolutionary distance that the lengths do not support.
Symptom: A topology-only or arbitrary-length tree appears to make precise distance statements.
Fix: Draw as a cladogram and SAY so (branch.length='none' in ggtree); or, if lengths are real, keep them and state the unit (subs/site vs time) plus a scale bar.
Trigger: A node shows "98" with no legend, or two measures are printed without saying which is which. Mechanism: The reader defaults to assuming bootstrap, but it may be a posterior (much weaker for the same number), an SH-aLRT (cutoff 80), a UFBoot (cutoff 95, not the BP-70 scale), or a TBE. Symptom: A weakly resolved bush reads as a confident comb because the displayed number is over-read. Fix: Always state the measure(s) and their order (e.g. "SH-aLRT/UFBoot" at each node); collapse nodes below threshold into polytomies rather than drawing fake resolution, since bootstrap, posterior, SH-aLRT, and UFBoot sit on different scales.
Trigger: A rectangular layout past a few hundred tips. Mechanism: Horizontal labels collide into an unreadable black band; authors then shrink the font to illegibility or silently drop labels. Symptom: Tip labels are unreadable or missing. Fix: Rotate labels, switch to circular/fan with radial labels, collapse uninformative clades, annotate by colored strips/rings (ggtreeExtra, iTOL datasets) instead of text, or move to iTOL which is built for large trees.
Trigger: Rotating nodes to make a paraphyletic or polyphyletic group look contiguous (or a clade look scattered). Mechanism: Node rotation is information-free with respect to the tree, but the eye reads contiguity as a clade and ordering as direction. Symptom: A group that is not monophyletic appears unified, or a directional narrative is implied. Fix: Color by group and let the topology speak; never narrate ordering as meaning; state "tips ladderized for legibility, ordering carries no phylogenetic meaning."
Trigger: Drawing a BEAST/MrBayes time-tree with point-estimate node ages and no HPD bars.
Mechanism: The 95% HPD intervals on node heights ARE the result; omitting them asserts false precision.
Symptom: Node ages look known to the day; a reviewer asks where the credible intervals went.
Fix: Draw the HPD bars via treeio read.beast() -> ggtree geom_range('height_0.95_HPD') (introspect the column name; it varies by source program and treeio version), or enable node bars in FigTree; never draw an annotated Bayesian tree with Bio.Phylo, which drops the annotations silently.
Trigger: Saving a tree as a 150-dpi PNG for a paper. Mechanism: Raster pixelates thin branches and small tip labels at print size. Symptom: Fuzzy labels and pixelated branches; journal rejection or blurry print. Fix: Export SVG/PDF/EPS (vector) so text and lines stay sharp at any scale; only if forced to rasterize, do so at final size with >=600 dpi for line art.
These are operational defaults for a standard portrait panel with ~6-8 pt labels; adapt to font, page, and journal specs.
| Quantity | Threshold | Rationale / source | |----------|-----------|--------------------| | Tips, rectangular horizontal labels comfortable | <=~50 | labels do not collide at legible font | | Tips, start rotating/shrinking labels | ~50-150 | label height x tip count approaches panel height | | Tips, switch to circular/fan or collapse clades | ~150-500 | rectangular labels collide; radial labels recover space | | Tips, per-tip text impractical; use strips/rings | >~500-1000 | annotate by color/groups (iTOL, ggtreeExtra); iTOL is built for large trees | | Publication export | vector (SVG/PDF/EPS) | text and lines stay sharp at any scale; the default | | Forced raster, line-art/tree | >=600 dpi (many journals 600-1200) | ~300 dpi is the photo floor but too coarse for thin branches and small labels | | Scale bar on any phylogram | mandatory (subs/site or a time axis) | without it the reader cannot recover true distances under aspect-ratio distortion | | Bootstrap strong | >=95 (>=70 moderate) | Hillis-Bull heuristic; state the measure (Felsenstein 1985) | | UFBoot strong | >=95 (not the BP-70 scale) | recalibrated; IQ-TREE joint rule SH-aLRT>=80 AND UFBoot>=95 | | SH-aLRT strong | >=80 | IQ-TREE recommendation | | Posterior probability strong | >=0.95, but WEAKER than bootstrap 95 | PP systematically higher for the same data; do not equate |
Lock the branch-length scale; do not let the figure engine non-uniformly stretch a phylogram to fill a fixed panel, which distorts the lengths the figure exists to communicate.
| Error / symptom | Cause | Solution |
|-----------------|-------|----------|
| BEAST HPD bars absent from a Python figure | drew an annotated tree with Bio.Phylo | route through treeio read.beast + ggtree geom_range |
| Figure not saving / blank | do_show=True opens a window instead of writing | pass do_show=False, then fig.savefig(...) |
| Branch colors not appearing | plain Newick tree has no color slot | convert with tree.as_phyloxml() and set clade.color |
| Labels overlap into a black band | too many tips for rectangular layout | increase panel height, rotate labels, or switch to circular/iTOL |
| "Basal" claim on a radial tree | narrated an unrooted layout as if rooted | root explicitly (tree-manipulation) and show the root before any directional claim |
| Support number misread | bare integer with no measure stated | label the measure(s) and order; collapse sub-threshold nodes to polytomies |
| Phylo.draw has no circular option | Bio.Phylo is rectangular-only | use ggtree layout='circular', ETE4, or iTOL |
Talevich E, Invergo BM, Cock PJA, Chapman BA. 2012. Bio.Phylo: a unified toolkit for processing, analyzing and visualizing phylogenetic trees in Biopython. BMC Bioinformatics 13:209. Yu G, Smith DK, Zhu H, Guan Y, Lam TT-Y. 2017. ggtree: an R package for visualization and annotation of phylogenetic trees with their covariates and other associated data. Methods in Ecology and Evolution 8(1):28-36. Yu G. 2020. Using ggtree to visualize data on tree-like structures. Current Protocols in Bioinformatics 69(1):e96. Wang L-G, Lam TT-Y, Xu S, Dai Z, Zhou L, Feng T, Guo P, Dunn CW, Jones BR, Bradley T, Zhu H, Guan Y, Jiang Y, Yu G. 2020. treeio: an R package for phylogenetic tree input and output with richly annotated and associated data. Molecular Biology and Evolution 37(2):599-603. Xu S, Dai Z, Guo P, Fu X, Liu S, Zhou L, Tang W, Feng T, Chen M, Zhan L, Wu T, Hu E, Jiang Y, Bo X, Yu G. 2021. ggtreeExtra: compact visualization of richly annotated phylogenetic data. Molecular Biology and Evolution 38(9):4039-4042. Huerta-Cepas J, Serra F, Bork P. 2016. ETE 3: reconstruction, analysis, and visualization of phylogenomic data. Molecular Biology and Evolution 33(6):1635-1638. Letunic I, Bork P. 2024. Interactive Tree of Life (iTOL) v6: recent updates to the phylogenetic tree display and annotation tool. Nucleic Acids Research 52(W1):W78-W82. Felsenstein J. 1985. Confidence limits on phylogenies: an approach using the bootstrap. Evolution 39(4):783-791.
tools
End-to-end CLIP-seq pipeline from FASTQ to ENCODE-compliant binding sites, single-nucleotide crosslink maps, annotation, motifs, and (optionally) differential binding. Use when running the full Yeo lab eCLIP / iCLIP / iCLIP2 / iCLIP3 / irCLIP / PAR-CLIP analysis with SMInput control, protocol-specific UMI extraction, ENCODE STAR parameters, CLIPper or Skipper peak calling with stringent log2 FC and -log10 p thresholds, IDR rescue and self-consistency QC, and downstream motif registration with mCross or PEKA.
development
Detect, date, and contextualize whole-genome duplication (WGD / paleopolyploidy) events using wgd v2 (Chen et al 2024), KsRates (Sensalari 2022 substitution-rate-corrected Ks dating), DupGen_finder (Qiao 2019), MAPS (Li 2018 phylogenomic), POInT (Conant 2008 ordered-block), SLEDGe (2024 ML-based), Whale.jl (Bayesian DL+WGD), and synteny-anchored paranome construction. Use when identifying ancient polyploidy from Ks distributions and synteny block analysis, positioning WGD events relative to speciation, distinguishing tandem from segmental from WGD duplications, dating the 2R/3R vertebrate / fish / salmonid WGDs, building paranome and Ks-age mixture models, applying KsRates substitution-rate correction across lineages, or testing alternative biased-fractionation / dosage-balance models post-WGD.
tools
Build whole-genome alignments using Progressive Cactus (Armstrong 2020 reference-free clade-level WGA), Minigraph-Cactus (Hickey 2024 pangenome-aware), LASTZ chain/net (UCSC pipeline), MUMmer4 (Marçais 2018 pairwise), minimap2 -x asm5/10/20 (Li 2018 fast pairwise), AnchorWave (Song 2022 WGD-aware), and Mauve / progressiveMauve (bacterial). Operates the HAL toolkit (Hickey 2013) for downstream extraction including halSynteny, halLiftover, halBranchMutations, and hal2maf. Use when constructing multi-species alignments for comparative-annotation projection (TOGA), synteny detection, conservation analyses (phyloP / PhastCons), or pangenome graph construction; selecting between reference-free (Cactus) and reference-anchored (LASTZ chains/nets) approaches; tuning sensitivity for closely vs distantly related genomes; or producing HAL files for genome-wide downstream tools.
development
Detect syntenic blocks and structural rearrangements between genomes using MCScanX (Wang 2012), JCVI/MCScan (Tang 2008 Python), GENESPACE (Lovell 2022) for orthology-anchored riparian visualization, SyRI for structural variation, AnchorWave for sequence-level synteny, i-ADHoRe 3.0 for highly diverged species, SynNet for synteny networks, and ntSynt for multi-genome macrosynteny. Use when identifying collinear gene blocks across species, distinguishing macrosynteny from microsynteny, detecting inversions/translocations/duplications, anchoring orthology in WGD lineages, producing publication riparian plots, computing synteny block age via Ks (cross-references whole-genome-duplication), or running synteny-aware ortholog inference in polyploids.