app.tree
Functions for phylogenetic tree layout and analysis.
Provides the pipeline from parsed Newick tree to fully positioned tree with enriched leaf metadata:
parsed tree -> y-positioned -> x-positioned -> enriched leaves
Most functions in this namespace are pure and operate on immutable data. assign-y-coords and assign-node-ids accept a mutable atom to track traversal state; these functions are not referentially transparent unless callers treat the atom as part of the input/output state (e.g. by passing a fresh atom when purity is required).
Scale-related helpers (calculate-scale-unit, get-ticks) now live in app.scale.
See app.specs for function specs.
assign-leaf-names
(assign-leaf-names node)Precomputes a :leaf-names set on every node containing the names of all descendant leaves. For leaf nodes the set contains only that leaf’s name (when non-nil). For internal nodes it is the union of the children’s sets. This avoids the O(n²) cost of calling get-leaves per-node during rendering.
assign-node-ids
(assign-node-ids node)(assign-node-ids node next-id)Assigns a unique :id to every node in the tree for stable React keys.
Traverses the tree depth-first, assigning sequential integer IDs starting from 0. The ID counter is passed as an atom to maintain state across recursive calls.
Single-arity version returns the updated node with :id on every node. Two-arity version (internal) returns a tuple of [updated-node next-id-value].
assign-x-coords
(assign-x-coords node)(assign-x-coords node current-x is-root?)Assigns horizontal (x) coordinates by accumulating branch lengths.
Each node’s x-position is its parent’s x plus its own :branch-length. The root node’s branch length is ignored (pinned at x=0) so that the tree starts at the left edge.
Single-arity call is the public entry point; multi-arity is used internally for recursion.
assign-y-coords
(assign-y-coords node next-y)Assigns vertical (y) coordinates to every node in the tree.
Leaf nodes receive sequential integer y-values starting from the current value of the next-y atom. Internal nodes are positioned at the average y of their first and last child, producing a standard phylogenetic tree layout.
Returns a tuple of [updated-node next-y-value].
next-y is a mutable atom that tracks the next available y position. It is shared across the entire traversal to ensure leaves are evenly spaced.
count-tips
(count-tips node)Counts the number of leaf nodes (tips) in a tree.
A tip is any node with an empty :children vector. Internal nodes contribute the sum of their children’s tip counts.
distance-between
(distance-between root id-a id-b)Returns the phylogenetic distance (sum of branch lengths) between the nodes with ids id-a and id-b in root.
Distance is computed as (x-a - x-lca) + (x-b - x-lca), where :x is the cumulative branch-length sum from the root. Returns nil if either node is not found.
enrich-leaves
(enrich-leaves tips metadata-rows active-cols)Merges metadata from uploaded CSV/TSV rows onto positioned leaf nodes.
Looks up each leaf’s :name in metadata-rows using the first column of active-cols as the join key, and attaches the matching row under :metadata.
Returns the enriched tips vector (same length as input tips).
find-lca
(find-lca root id-a id-b)Returns the lowest common ancestor node of the nodes with ids id-a and id-b in root, or nil if either node is not found.
find-path-to-node
(find-path-to-node node target-id)Finds the path from root to target node. Returns vector of nodes root … target or nil if not found.
get-leaves
(get-leaves n)Collects all leaf nodes from a tree into a flat vector.
Traverses the tree depth-first and returns only nodes whose :children vector is empty. Preserves left-to-right order.
get-max-x
(get-max-x node)Returns the maximum x-coordinate across all nodes in a positioned tree.
Recursively traverses the tree comparing :x values. Used to determine the horizontal extent of the tree for scaling calculations.
ladderize
(ladderize tree)(ladderize tree direction)Ladderizes a tree by sorting children at each node by subtree size.
Direction can be :ascending (larger clades on top, the default) or :descending (larger clades on bottom).
This is a pure function - it doesn’t modify coordinates, just reorders the :children vectors.
leaves-in-rect
(leaves-in-rect tips {:keys [min-x max-x min-y max-y]} x-scale y-mult pad-x pad-y left-shift)Returns a set of tip names whose positioned coordinates fall inside a bounding rectangle.
Arguments: - tips - positioned leaf nodes (each with :x, :y, :name) - rect - map {:min-x :max-x :min-y :max-y} in SVG user-space - x-scale - horizontal scaling factor (pixels per branch-length unit) - y-mult - vertical scaling factor (pixels per tip) - pad-x - horizontal padding offset (px) - pad-y - vertical padding offset (px) - left-shift - additional horizontal shift (px)
Used by box-select / lasso selection in the viewer.
parse-and-position
(parse-and-position newick-str)Parses a Newick string and produces a fully positioned tree.
Pipeline: Newick string → parsed tree → position-tree.
This is the geometry-only stage — it depends solely on the Newick string and does not touch metadata. Memoize on newick-str to avoid re-parsing when only metadata changes.
Returns a map with: - :tree - root node with :x, :y, :id, and :leaf-names - :tips - flat vector of leaf nodes (no metadata yet) - :max-depth - maximum x-coordinate (for scale calculations)
position-tree
(position-tree parsed-tree)Assigns layout coordinates to a parsed tree map.
Pipeline: y-positioned → x-positioned → node-ids → leaf-names → collect leaves.
Accepts the output of newick->map or any equivalent tree map (e.g. from app.import.nextstrain/to-tree-map). Useful when the tree map is already available and Newick parsing can be skipped.
Returns a map with: - :tree - root node with :x, :y, :id, and :leaf-names - :tips - flat vector of leaf nodes (no metadata yet) - :max-depth - maximum x-coordinate (for scale calculations)
prepare-tree
(prepare-tree newick-str metadata-rows active-cols)Builds a fully positioned tree with enriched leaf metadata.
Combines parse-and-position (geometry) and enrich-leaves (metadata join) into a single call. Prefer the two-stage API in performance-sensitive paths so that Newick parsing can be memoized independently of metadata changes.
Returns a map with: - :tree - root node with :x, :y, :id, and :leaf-names on every node - :tips - flat vector of leaf nodes with :metadata merged - :max-depth - maximum x-coordinate (for scale calculations)
reroot-on-branch
(reroot-on-branch tree target-id)Re-roots the tree at the midpoint of the branch leading to the node with target-id.
This mimics FigTree’s “Reroot” behaviour: a new bifurcating root node is inserted at the midpoint of the selected branch, splitting it into two halves. One child of the new root is the subtree below the split point; the other child is the rest of the tree with reversed parent-child edges along the path from the old root to the split point.
The selected branch is identified by target-id — the :id of the child node at the end of the branch. The tree must have :id keys on every node (as produced by assign-node-ids or position-tree).
After rerooting: - All pairwise tip distances are preserved. - :id keys are stripped (callers should run position-tree to reassign layout coordinates and fresh IDs). - Unifurcations at the old root are collapsed.
Returns the new tree map, or nil if: - target-id is the root’s own ID (no incoming branch to split) - target-id is not found in the tree