Skip to content

Layout algorithms

A layout turns a TreeBuffers into per-node coordinate arrays. All layouts emit coordinates in unit space — the renderer multiplies by the chart’s scales at draw time. Every layout result satisfies the shared LayoutBase shape:

interface LayoutBase {
count: number;
root: number;
bounds: { minX: number; maxX: number; minY: number; maxY: number };
leaves: Uint32Array; // tip node ids
hasBranchLengths: boolean; // false ⇒ cladogram fallback was used
}

LayoutFn<T> is (b: TreeBuffers) => T. When no node carries a finite branch length, every layout falls back to a cladogram (unit-length / topological-depth branches).

The classic phylogram layout: x is the branch-length sum from the root, y is the integer tip-row index. This is the workhorse the renderer uses for both the rectangular and circular (polar-projected) views.

import { layoutRectilinear } from "insomni-phylo";
const layout = layoutRectilinear(tree);
layout.nx[i]; // unit-space x per node
layout.ny[i]; // unit-space y per node (tip rows are integers 0..tipCount-1)

RectLayout extends LayoutBase with:

FieldMeaning
nx / ny: Float32ArrayPer-node unit-space coordinates.
tipAxis: TipAxisTip ordering + position lookups (see below).
subtreeMinY / subtreeMaxY: Float32ArrayPer-node subtree y-extent.
rowIndex: BranchRowIndex | nullRow-bucket spatial index for hit-testing (built later).

RectLayout.tipAxis (type TipAxis) is the layout-agnostic view of the tip ordering. It is what joinByTip consumes for name → id resolution:

interface TipAxis {
count: number;
order: Uint32Array; // tip ids top → bottom
y(tipId: number): number; // position in the layout's native unit
tipAt(pos: number): number; // inverse, or -1 if none nearby
name(tipId: number): string | null;
byName(name: string): number; // tip id, or -1
}

layoutDendrogram wraps layoutRectilinear and carries the source TreeBuffers alongside the coordinate arrays, so a consumer can pass a single object. Only direction: "right" (root on the left, tips on the right) is implemented today.

import { layoutDendrogram } from "insomni-phylo";
const layout = layoutDendrogram(tree, { direction: "right" });
layout.buffers; // source tree, by reference
layout.tipAxis; // same TipAxis as RectLayout

DendrogramLayout adds buffers, nx, ny, tipAxis, and direction (DendrogramDirection = "right") to LayoutBase.

Unrooted — layoutUnrootedEqualAngle & equalDaylight

Section titled “Unrooted — layoutUnrootedEqualAngle & equalDaylight”

Unrooted layouts place every node directly in Cartesian space — there is no depth axis and no linear tip order, so UnrootedLayout.tipAxis is null.

layoutUnrootedEqualAngle implements Felsenstein’s equal-angle method: each internal node gets an angular sector divided among its children by leaf count. It is O(N).

import { equalDaylight, layoutUnrootedEqualAngle } from "insomni-phylo";
const base = layoutUnrootedEqualAngle(tree, { startAngle: -Math.PI / 2 });
const relaxed = equalDaylight(base, { maxIterations: 20, convergence: 0.01 });

equalDaylight refines an equal-angle layout by iterative daylight relaxation, spreading subtrees so the angular gaps (“daylight”) between them even out. It operates on copies — the input layout is not mutated — and reports daylightIterations on the result.

TypeFields
UnrootedLayoutOptionsstartAngle? (radians; default -π/2).
EqualDaylightOptionsmaxIterations? (20), convergence? (0.01 rad).
UnrootedLayoutLayoutBase + buffers, nx, ny, algorithm ("equal-angle" / "equal-daylight"), daylightIterations, tipAxis: null.

Branch-length transforms — applyTreeTransform

Section titled “Branch-length transforms — applyTreeTransform”

applyTreeTransform rewrites a RectLayout’s nx values in place to apply a FigTree-style transform; ny and the leaf order are untouched. It returns the same layout instance.

import { applyTreeTransform, layoutRectilinear } from "insomni-phylo";
const layout = layoutRectilinear(tree);
applyTreeTransform(layout, tree, "cladogram");

TreeTransform mirrors FigTree’s trees.transformType:

ValueEffect
"proportional"x is the branch-length sum from root (native, no-op).
"cladogram"Every tip aligned at the same x; internal nodes at tipDepth − maxLeafDepth(i).
"equal"Every branch length 1, so x = ancestorCount.