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).
Rectilinear — layoutRectilinear
Section titled “Rectilinear — layoutRectilinear”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 nodelayout.ny[i]; // unit-space y per node (tip rows are integers 0..tipCount-1)RectLayout extends LayoutBase with:
| Field | Meaning |
|---|---|
nx / ny: Float32Array | Per-node unit-space coordinates. |
tipAxis: TipAxis | Tip ordering + position lookups (see below). |
subtreeMinY / subtreeMaxY: Float32Array | Per-node subtree y-extent. |
rowIndex: BranchRowIndex | null | Row-bucket spatial index for hit-testing (built later). |
The tip axis
Section titled “The tip axis”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}Dendrogram — layoutDendrogram
Section titled “Dendrogram — layoutDendrogram”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 referencelayout.tipAxis; // same TipAxis as RectLayoutDendrogramLayout 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.
| Type | Fields |
|---|---|
UnrootedLayoutOptions | startAngle? (radians; default -π/2). |
EqualDaylightOptions | maxIterations? (20), convergence? (0.01 rad). |
UnrootedLayout | LayoutBase + 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:
| Value | Effect |
|---|---|
"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. |