Skip to content

Tree model

The model layer represents a tree as flat typed arrays — a structure-of-arrays design ported from figtree-web so that million-node trees stay cache-friendly and never allocate a number[] per node.

TreeBuffers is the central data structure. Child lists are not stored as arrays; instead each node points at its firstChild, and children are walked through a nextSibling linked list.

interface TreeBuffers {
count: number; // valid node slots, ≤ capacity
tipCount: number; // leaves (nodes with FLAG_TIP set)
root: number; // index of the root node
capacity: number; // max addressable nodeIdx (parsers pre-size)
parent: Int32Array;
firstChild: Int32Array;
nextSibling: Int32Array;
length: Float64Array; // branch length parent[i] → i; NaN when unspecified
flags: Uint8Array; // per-node bit flags — currently just FLAG_TIP
nameIdx: Int32Array; // index into `names`, or NO_NAME
names: StringPool; // interned labels
}

Sentinels and flags are exported alongside it:

ExportMeaning
FLAG_TIPBit set on leaf nodes (flags[i] & FLAG_TIP).
NO_NODE (-1)Missing parent / child / sibling pointer.
NO_NAME (-1)nameIdx value for an unlabeled node.
StringPoolInterns labels so identical tip names are stored once.

allocTreeBuffers(capacity, names?) returns an empty buffer sized for capacity nodes; parsers bump count as they write slots. Passing a shared StringPool lets several trees intern identical labels together.

import { allocTreeBuffers, StringPool } from "insomni-phylo";
const pool = new StringPool();
const buffers = allocTreeBuffers(1024, pool);

Three small helpers read the buffers. children is a generator over the nextSibling chain and is safe to break out of early.

import { children, isTip, nodeName } from "insomni-phylo";
for (const c of children(tree, tree.root)) {
if (isTip(tree, c)) console.log("leaf:", nodeName(tree, c));
}
FunctionSignatureNotes
nodeName(b, i) => string | nullInterned label, or null if unlabeled.
isTip(b, i) => booleanTrue when FLAG_TIP is set.
children(b, i) => Generator<number>Yields direct child node ids.

ladderize reorders sibling chains by descendant-tip count, in place. Only firstChild / nextSibling pointers change — node indices, parents, lengths, and any table keyed by nodeIdx stay aligned. It is a stable sort.

import { ladderize } from "insomni-phylo";
ladderize(tree, "increasing"); // smallest clade first (FigTree default)
ladderize(tree, "decreasing"); // reverse

LadderizeDirection is "increasing" | "decreasing".

Per-node metadata parsed from [&k=v] / NHX / NEXUS lives in an AnnotationTable — columnar dense storage with one cell per node.

import { AnnotationTable } from "insomni-phylo";
const table = new AnnotationTable();
table.set(3, "posterior", 0.97, "0.97"); // value + optional verbatim raw text
table.get(3, "posterior"); // 0.97
table.getRaw(3, "posterior"); // "0.97"

Each AnnotationColumn carries values, rawValues (the verbatim source slice), and an observed type ("number" | "string" | "boolean" | "list" | "mixed"). The rawValues channel is what makes byte-exact round-trips possible — see FigTree compatibility.

Method / functionPurpose
set(i, key, value, raw?)Write a cell; densifies columns lazily.
get(i, key) / getRaw(i, key)Read the value / verbatim source.
column(key) / has(key) / keys()Column access and enumeration.
extractNumericColumn(table, key)Dense Float64Array (NaN for missing).
extractNumericColumnSized(table, key, n)Same, padded to n nodes.
getNumericStats(values){ min, max, count } or null if all-NaN.
getNumericPercentiles(values, fractions)Linear-interpolated percentiles.
extractHilights(table)Parsed !hilight clade highlight colors.

AnnotationValue is number | string | boolean | AnnotationValue[].

buildAttributeCatalog runs one pass over an AnnotationTable and summarizes every column into an AttributeInfo — the data behind “Colour by / Size by” dropdowns. Keys starting with ! (FigTree “hot” annotations) are hidden unless includeHot is set.

import { buildAttributeCatalog } from "insomni-phylo";
const catalog = buildAttributeCatalog(table, { buffers: tree });
// → AttributeInfo[] sorted by key

Each AttributeInfo reports its kind (AttributeKind: "number", "string", "boolean", "range", "list", "mixed", "temporal"), the non-null count, numeric min/max, a capped distinct set (limit DISTINCT_LIMIT = 64), plus auto-mapping hints — suggestedScale, suggestedPalette, isDiverging, isSkewed, p5/p95, histogramBins, and (when buffers is supplied) a phyloSignal clustering diagnostic.

joinByTip merges a row table onto the tree by matching a row field against tip names (ggtree’s %<+%). It is pure and returns a fresh AnnotationTable plus warning diagnostics for unmatched or duplicate keys. It takes a TipAxis (produced by a layout) for name → tip-id resolution.

import { joinByTip, layoutRectilinear } from "insomni-phylo";
const axis = layoutRectilinear(tree).tipAxis;
const { table, diagnostics } = joinByTip(
[
{ id: "A", country: "DE" },
{ id: "B", country: "FR" },
],
axis,
{ key: "id" },
);

groupClade returns a fresh categorical AnnotationColumn labeling nodes inside a clade vs. outside (ggtree’s groupClade / groupOTU). The clade is specified either by nodeId or by the MRCA of named tips.

import { groupClade } from "insomni-phylo";
const col = groupClade(
tree,
{ mrcaOf: ["A", "B"] },
{
label: "ingroup",
otherLabel: "outgroup", // pass null to leave outside nodes empty
},
);