Embeddable React component for visualizing phylogenetic trees with pluggable data zones.
<TBrowse> renders a tree alongside a row-aligned set of zones (Tree, Labels, MSA, custom plug-ins). Tree topology, taxonomy, MSA, gene metadata, and per-node annotations are independent host inputs — TBrowse does no fetching of its own. View state (selection, collapse, prune, swap, zone widths, MSA viewport, search) is a single serializable object that round-trips through URL state via the controlled viewState / onViewStateChange props.
2.0.0 is a complete rewrite of the original component on a fresh React 18 / TypeScript / Zustand stack. The 1.x line on npm (tbrowse@1.0.x) is the previous nwb / React 16 / Redux-Toolkit implementation, preserved at commit b017a74 for archival reference. The 2.0 API is not backwards-compatible with 1.x.
The rewrite is functionally complete: chassis, four built-in zones (Tree / Labels / MSA / Neighborhood), Ensembl REST and Gramene adapters, animated transitions, branch-length compression, gene-family coloring, protein-domain annotations, splice-junction marks, and a nodeOfInterest pivot. See CHANGELOG.md for the migration summary.
import { TBrowse, treeZone, labelsZone, msaZone, type Tree, type MSA, type Taxonomy } from 'tbrowse';
const tree: Tree = { rootId: 'n0', nodes: { /* ... */ } };
const taxonomy: Taxonomy = { /* taxId → { scientificName, commonName, ... } */ };
const msa: MSA = { alphabet: 'protein', length: 60, sequences: { /* geneId → string */ } };
<div style={{ width: 900, height: 600 }}>
<TBrowse
tree={tree}
taxonomy={taxonomy}
msa={msa}
zones={[treeZone, labelsZone, msaZone]}
/>
</div>The bundled Vite playground (npm run dev) has a "Load Ensembl tree" button that fetches a real gene tree (BRCA2 — ENSG00000139618), runs it through fromEnsemblGeneTree, and renders it pivoted to put human BRCA2 at the top. Demonstrates the entire stack against live data.
import { fromEnsemblGeneTree, computePivotState, TBrowse } from 'tbrowse';
const json = await fetch(
'https://rest.ensembl.org/genetree/member/id/homo_sapiens/ENSG00000139618?aligned=1&sequence=protein',
{ headers: { Accept: 'application/json' } },
).then((r) => r.json());
const { tree, taxonomy, msa, geneMetadata } = fromEnsemblGeneTree(json);rest.ensembl.org intermittently sheds load with 5xx errors, which is fatal to a fan-out that issues one request per leaf. The playground therefore ships a mirror picker listing the current endpoint plus every live per-release archive host. Archive hosts pin one Ensembl release and run on separate infrastructure, so they stay responsive when the main endpoint does not. The default is rest.ensembl.org (the current release), so the playground shows current data; switch to an archive when the main endpoint is struggling.
One caveat if you hard-code a host yourself: the gene-tree member route changed shape mid-history.
| Release | Route |
|---|---|
| ≤ 109 | /genetree/member/id/{gene} |
| 110–111 | either form |
| ≥ 112 | /genetree/member/id/{species}/{gene} |
Sending the wrong form returns a 404, so the playground's URL builder switches on the selected release. Note that alignment width and leaf count differ between releases (BRCA2 is 174 leaves / 5471 columns on r112, 176 / 7138 on r105), so switching mirrors invalidates anything derived from the tree — the playground clears cached domains and gene structures on a switch.
All playground fetches go through a shared fetchWithRetry helper that retries 429 and 5xx with exponential backoff plus jitter, honouring Retry-After when present. The per-leaf fan-outs record individual failures and carry on rather than aborting the batch.
npm install
npm run dev # Vite dev server (examples/playground)
npm run typecheck # tsc --noEmit
npm test # vitest
npm run build # library build to dist/interface TBrowseProps {
tree: Tree; // required
taxonomy?: Taxonomy;
msa?: MSA;
geneMetadata?: GeneMetadata;
nodeAnnotations?: NodeAnnotation[];
labelProviders?: LabelProvider[];
zones: ZoneDefinition[]; // built-ins + plugins
nodeOfInterest?: string; // pivot the tree to this gene/node at mount
// Controlled / uncontrolled view state
viewState?: ViewState;
initialViewState?: Partial<ViewState>;
onViewStateChange?: (next: ViewState) => void;
theme?: 'light' | 'dark';
className?: string;
}Tree.nodes is flat: { id, parentId, distance, isLeaf, taxonomyId?, geneId?, eventType?, bootstrap? }. MSA.sequences is keyed by gene id. The Ensembl adapter (fromEnsemblGeneTree) walks the nested REST response into this normalized shape.
The single serializable object that captures everything URL-shareable.
interface ViewState {
selectedNodeId: NodeId | null;
collapsedNodeIds: NodeId[]; // internal nodes shown as triangles
prunedNodeIds: NodeId[]; // subtrees hidden + replaced with stub glyphs
swappedNodeIds: NodeId[]; // internal nodes whose children render in reversed order
zones: { id: string; width: number; visible: boolean }[];
zoneStates: Record<string, unknown>; // per-zone state slot (built-ins use well-known keys)
search: { field: string; query: string } | null;
}All chassis interactions (resize, reorder, collapse via tooltip, prune via tooltip, swap, MSA pan/zoom, label-field picker, show/hide) write to this object via onViewStateChange. Persist it to a URL fragment for shareable views.
| Zone | Id | Default width |
|---|---|---|
treeZone |
tree |
280px |
labelsZone |
labels |
220px |
msaZone |
msa |
360px |
The Tree zone supports collapse/expand, prune/regrow, swap children, click-tooltip with contextual actions, internal-node event glyphs (speciation circle / duplication square), proportional-height collapsed-subtree triangles, and animated transitions. Labels supports a configurable field picker (gene id, gene name, gene description, scientific name, common name) plus optional async LabelProvider plug-ins. MSA renders a canvas alignment with a coverage minimap (drag-to-pan), wheel-driven pan + ctrl/shift-zoom, and four built-in color schemes (Plain, Clustal, Hydrophobicity, Nucleotide).
Custom zones implement ZoneDefinition:
interface ZoneDefinition<S = unknown> {
id: string;
displayName: string;
Header: ComponentType<ZoneRenderProps<S>>;
Body: ComponentType<ZoneRenderProps<S>>;
defaultWidth: number;
minWidth: number;
defaultZoneState: S;
isAvailable: (data: HostData) => boolean;
}Both Header and Body receive the same ZoneRenderProps: visibleRows (already collapsed/pruned/swapped), virtualization rowRange, hover/select callbacks, the host's data, and per-zone state plumbing.
| Export | What |
|---|---|
computeVisibleRows |
Pure derivation: (tree, collapsed, pruned, swapped) → VisibleRow[]. |
computeTreeLayout |
Pure phylogram layout against a column width. |
computePivotState |
Resolve a gene id / node id and return collapse + swap sets that pivot the tree. |
fromEnsemblGeneTree |
Convert Ensembl REST gene-tree JSON to TBrowse inputs. |
createStubZone |
Build a placeholder zone for development. |
ESM + CJS, types via vite-plugin-dts. Peer-deps on react / react-dom only; runtime dep on zustand. Current bundle ~14 kB gzipped (CJS).
src/
TBrowse.tsx, types.ts, store.tsx, treeIndex.ts
visibleRows.ts chassis row derivation
pivot.ts nodeOfInterest helper
layout/ grid + virtualization + animation
zones/
tree/ SVG branches, glyphs, triangles, tooltip
labels/ field picker, label-provider cache
msa/ canvas paint, minimap, color schemes
adapters/
ensembl.ts Ensembl REST → TBrowse inputs
*.test.ts vitest unit tests
examples/playground/ Vite dev app (kitchen-sink demo)
MIT.