@euphrasiologist/lwphylo 1.1.15 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +0,0 @@
1
- import radialData from "./radialData.js"
2
- import getRadii from "./getRadii.js"
3
- import getArcs from "./getArcs.js"
4
-
5
- /**
6
- * Simple wrapper for radial layout:
7
- * - data: per-node { angle, r, x, y, ... }
8
- * - radii: per-edge radial spokes (parent.r → child.r)
9
- * - arcs: per-internal-node arcs spanning its children at parent radius
10
- */
11
- export default function radialLayout(node) {
12
- const data = {};
13
- data.data = radialData(node);
14
- data.radii = getRadii(node);
15
- data.arcs = getArcs(data.data);
16
- return data;
17
- }
18
-
@@ -1,10 +0,0 @@
1
- /**
2
- * Legacy shim: normalize angle to [0, 2π).
3
- * The new radial code does not need geometric reflections;
4
- * it uses circular spans and atan2.
5
- */
6
- export default function reflectAngle(rad /*, dir */) {
7
- const TAU = Math.PI * 2;
8
- return ((rad % TAU) + TAU) % TAU;
9
- }
10
-
@@ -1,47 +0,0 @@
1
- import getHorizontal from "./getHorizontal.js";
2
-
3
- /**
4
- * Build per-child vertical segments for a rectangular tree:
5
- * For each non-root node (child), draw a vertical from (parent.x, child.y) to (parent.x, parent.y).
6
- * This yields exactly one vertical per edge (child->parent), making highlighting trivial.
7
- *
8
- * Returns an array of:
9
- * {
10
- * parentId: number,
11
- * childId: number,
12
- * x: number, // x of the parent junction
13
- * y0: number, // min(child.y, parent.y)
14
- * y1: number, // max(child.y, parent.y)
15
- * }
16
- */
17
- export default function getChildVerticals(node) {
18
- const data = getHorizontal(node); // has parentId, thisId, x0,x1,y0=y1
19
-
20
- // Build a quick index to access parent's y by id
21
- const byId = new Map(data.map(d => [d.thisId, d]));
22
-
23
- const childVerticals = [];
24
-
25
- for (const d of data) {
26
- if (d.parentId == null) continue;
27
- const parent = byId.get(d.parentId);
28
- if (!parent) continue;
29
-
30
- const x = d.x0; // child’s vertical sits at parent.x == child.x0
31
- const yc = d.y0; // child y
32
- const yp = parent.y0; // parent y
33
- const y0 = Math.min(yc, yp);
34
- const y1 = Math.max(yc, yp);
35
-
36
- childVerticals.push({
37
- parentId: d.parentId,
38
- childId: d.thisId,
39
- x,
40
- y0,
41
- y1
42
- });
43
- }
44
-
45
- return childVerticals;
46
- }
47
-
@@ -1,57 +0,0 @@
1
- import mean from "../utils/mean.js"
2
- import fortify from "../utils/fortify.js"
3
-
4
- /**
5
- * Rectangle layout: compute per-node x0,x1 and y0=y1
6
- * - Tip y is assigned by input order (preserves ladderize/order)
7
- * - Internal node y is mean of child y's
8
- * - x1 accumulates branch lengths from root
9
- */
10
-
11
- export default function getHorizontal(node) {
12
- const pd = fortify(node);
13
-
14
- // Fast lookup from id -> pd index
15
- const idIndex = new Map(pd.map((d, i) => [d.thisId, i]));
16
-
17
- // 1) Leaf order from the INPUT TREE (respects your child order / ladderize)
18
- const leafIds = [];
19
- (function dfs(n) {
20
- if (!n.children || n.children.length === 0) { leafIds.push(n.id); return; }
21
- n.children.forEach(dfs);
22
- })(node);
23
-
24
- // Map each leaf id to a vertical slot (1..N)
25
- const tipSlot = new Map(leafIds.map((id, i) => [id, i + 1]));
26
-
27
- // 2) Set Y for tips directly from that order; internal node Y via children mean
28
- (function setY(n) {
29
- const i = idIndex.get(n.id);
30
- if (!n.children || n.children.length === 0) {
31
- const y = tipSlot.get(n.id);
32
- pd[i].y0 = y; pd[i].y1 = y;
33
- return y;
34
- }
35
- const ys = n.children.map(setY);
36
- const y = mean(ys);
37
- pd[i].y0 = y; pd[i].y1 = y;
38
- return y;
39
- })(node);
40
-
41
- // 3) Set X by accumulating branch lengths down the tree
42
- (function setX(n, xParent) {
43
- const i = idIndex.get(n.id);
44
- const bl = pd[i].branchLength ?? 0;
45
- const x0 = xParent ?? 0;
46
- const x1 = x0 + bl;
47
- pd[i].x0 = x0; pd[i].x1 = x1;
48
- if (n.children && n.children.length) n.children.forEach(c => setX(c, x1));
49
- })(node, 0);
50
-
51
- // Clean up: remove fields not needed downstream without triggering no-unused-vars
52
- return pd.map((row) => {
53
- const { y: _y, x: _x, angle: _angle, ...item } = row;
54
- return item;
55
- });
56
- }
57
-
@@ -1,36 +0,0 @@
1
- import getHorizontal from "./getHorizontal.js"
2
-
3
- export default function getVertical(node) {
4
- const data = getHorizontal(node);
5
-
6
- // Group rows by parentId (children that share a parent)
7
- const byParent = new Map();
8
- for (const row of data) {
9
- if (row.parentId == null) continue;
10
- const a = byParent.get(row.parentId);
11
- if (a) a.push(row); else byParent.set(row.parentId, [row]);
12
- }
13
-
14
- const verticals = [];
15
- for (const [parentId, kids] of byParent.entries()) {
16
- if (!kids.length) continue;
17
- // Works for binary and multifurcations:
18
- const yvals = kids.map(d => d.y0);
19
- const y0 = Math.min(...yvals);
20
- const y1 = Math.max(...yvals);
21
- // All children share the same junction x (their x0)
22
- const x = kids[0].x0;
23
-
24
- verticals.push({
25
- parentId,
26
- x0: x,
27
- x1: x,
28
- y0,
29
- y1,
30
- heights: y1 - y0
31
- });
32
- }
33
-
34
- return verticals;
35
- }
36
-
@@ -1,36 +0,0 @@
1
- import getHorizontal from "./getHorizontal.js"
2
- import getVertical from "./getVertical.js"
3
- import getChildVerticals from "./getChildVerticals.js"
4
-
5
- /**
6
- * Rectangle layout wrapper.
7
- * Returns:
8
- * - data: per-node rows (x0,x1,y0=y1,...)
9
- * - vertical_lines: single spanning vertical per parent (baseline draw)
10
- * - child_vertical_lines: one vertical per edge (for highlighting)
11
- * - horizontal_lines: per-edge child horizontals (x0->x1 at y), with labels & tip flags
12
- */
13
- export default function rectangleLayout(node) {
14
- const data = getHorizontal(node); // per-node
15
- const vertical_lines = getVertical(node); // parent spans
16
- const child_vertical_lines = getChildVerticals(node); // per-edge verticals
17
-
18
- // IMPORTANT: include y0 & y1, and carry isTip/labels for the renderer
19
- const byId = new Map(data.map(d => [d.thisId, d]));
20
- const horizontal_lines = data
21
- .filter(d => d.parentId != null)
22
- .map(d => ({
23
- parentId: d.parentId,
24
- childId: d.thisId,
25
- thisId: d.thisId,
26
- thisLabel: d.thisLabel,
27
- isTip: d.isTip,
28
- x0: d.x0,
29
- x1: d.x1,
30
- y0: d.y0,
31
- y1: d.y0
32
- }));
33
-
34
- return { data, vertical_lines, child_vertical_lines, horizontal_lines };
35
- }
36
-
@@ -1,60 +0,0 @@
1
- import numTips from "../utils/numTips.js"
2
-
3
- /**
4
- * Equal-angle layout for unrooted trees.
5
- * - Precomputes ntips in O(n) to avoid repeated subtree counts
6
- * - Uses angles in "π units" (0..2) to match existing API
7
- * - Populates x,y positions from branchLength and angle
8
- */
9
-
10
- function annotateTipCounts(root) {
11
- (function post(n) {
12
- if (!n.children || n.children.length === 0) {
13
- n.ntips = 1; return 1;
14
- }
15
- let sum = 0;
16
- for (const c of n.children) sum += post(c);
17
- n.ntips = sum;
18
- return sum;
19
- })(root);
20
- return root;
21
- }
22
-
23
- function equalAngleLayout(node) {
24
- if (node.parent === null) {
25
- annotateTipCounts(node);
26
- node.start = 0.; // guarantees no arcs overlap 0
27
- node.end = 2.; // *π
28
- node.angle = 0.; // irrelevant at root
29
- node.ntips = numTips(node); // safe (already computed), left for compatibility
30
- node.x = 0;
31
- node.y = 0;
32
- }
33
-
34
- let lastStart = node.start;
35
-
36
- for (let i = 0; i < node.children.length; i++) {
37
- const child = node.children[i];
38
- const arc = (node.end - node.start) * (child.ntips / node.ntips);
39
-
40
- child.start = lastStart;
41
- child.end = lastStart + arc;
42
-
43
- // bisect the arc in π-units
44
- child.angle = child.start + (child.end - child.start) / 2.;
45
- lastStart = child.end;
46
-
47
- // map to coordinates (convert π-units to radians by multiplying by Math.PI)
48
- const theta = child.angle * Math.PI;
49
- const bl = (child.branchLength ?? 0);
50
- child.x = node.x + bl * Math.sin(theta);
51
- child.y = node.y + bl * Math.cos(theta);
52
-
53
- equalAngleLayout(child);
54
- }
55
-
56
- return node;
57
- }
58
-
59
- export default equalAngleLayout
60
-
@@ -1,18 +0,0 @@
1
- import edges from "../utils/edges.js";
2
- import fortify from "../utils/fortify.js";
3
- import equalAngleLayout from "./equalAngleLayout.js";
4
-
5
- /**
6
- * Simple wrapper function for equalAngleLayout()
7
- */
8
-
9
- export default function (node) {
10
- var data = {};
11
- // use the Felsenstein equal angle layout algorithm
12
- var eq = fortify(equalAngleLayout(node));
13
- data.data = eq;
14
- // make the edges dataset
15
- data.edges = edges(eq);
16
-
17
- return data;
18
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * Convert parsed Newick tree from fortify() into data frame of edges
3
- * this is akin to a "phylo" object in R, where thisID and parentId
4
- * are the $edge slot. I think.
5
- */
6
-
7
- export default function edges(df, rectangular = false) {
8
- const rows = [...df].sort((a, b) => a.thisId - b.thisId);
9
- const byId = new Map(rows.map((r) => [r.thisId, r]));
10
- const result = [];
11
-
12
- for (const row of rows) {
13
- if (row.parentId == null) continue;
14
- const parent = byId.get(row.parentId);
15
- if (!parent) continue;
16
-
17
- if (rectangular) {
18
- result.push({ x1: row.x, y1: row.y, id1: row.thisId, x2: parent.x, y2: row.y, id2: undefined });
19
- result.push({ x1: parent.x, y1: row.y, id1: undefined, x2: parent.x, y2: parent.y, id2: row.parentId });
20
- } else {
21
- result.push({ x1: row.x, y1: row.y, id1: row.thisId, x2: parent.x, y2: parent.y, id2: row.parentId });
22
- }
23
- }
24
- return result;
25
- }
26
-
@@ -1,49 +0,0 @@
1
- import { preorder } from "./preorder.js"
2
-
3
- /**
4
- * Convert parsed Newick tree from readTree() into data
5
- * frame.
6
- * this is akin to a "phylo" object in R.
7
- */
8
-
9
- export default function (tree, sort = true) {
10
- var df = [];
11
-
12
- for (const node of preorder(tree)) {
13
- if (node.parent === null) {
14
- df.push({
15
- 'parentId': null,
16
- 'parentLabel': null,
17
- 'thisId': node.id,
18
- 'thisLabel': node.label,
19
- 'children': node.children.map(x => x.id),
20
- 'branchLength': 0.,
21
- 'isTip': false,
22
- 'x': node.x,
23
- 'y': node.y,
24
- 'angle': node.angle
25
- })
26
- }
27
- else {
28
- df.push({
29
- 'parentId': node.parent.id,
30
- 'parentLabel': node.parent.label,
31
- 'thisId': node.id,
32
- 'thisLabel': node.label,
33
- 'children': node.children.map(x => x.id),
34
- 'branchLength': node.branchLength,
35
- 'isTip': (node.children.length == 0),
36
- 'x': node.x,
37
- 'y': node.y,
38
- 'angle': node.angle
39
- })
40
- }
41
- }
42
-
43
- if (sort) {
44
- df = df.sort(function (a, b) {
45
- return a.thisId - b.thisId;
46
- })
47
- }
48
- return (df);
49
- }
@@ -1,64 +0,0 @@
1
- /**
2
- * Build quick indexes for a fortified data frame.
3
- */
4
- export function indexTree(df) {
5
- const byId = new Map(df.map(r => [r.thisId, r]));
6
- const children = new Map(df.map(r => [r.thisId, []]));
7
- let root = null;
8
- for (const r of df) {
9
- if (r.parentId == null) { root = r.thisId; continue; }
10
- children.get(r.parentId).push(r.thisId);
11
- }
12
- // depths (BFS)
13
- const depth = new Map(); if (root != null) depth.set(root, 0);
14
- const q = root != null ? [root] : [];
15
- while (q.length) {
16
- const u = q.shift();
17
- for (const v of (children.get(u) || [])) {
18
- depth.set(v, (depth.get(u) || 0) + 1);
19
- q.push(v);
20
- }
21
- }
22
- return { byId, children, depth, root };
23
- }
24
-
25
- /**
26
- * Compute the node->root path for highlighting (returns [tip,...,root]).
27
- */
28
- export function pathToRoot(dfIndex, nodeId) {
29
- const { byId } = dfIndex;
30
- const path = [];
31
- let cur = nodeId;
32
- while (cur != null) {
33
- path.push(cur);
34
- const row = byId.get(cur);
35
- cur = row?.parentId ?? null;
36
- }
37
- return path;
38
- }
39
-
40
- /**
41
- * Convert a path (node ids) to parent-child edge pairs.
42
- */
43
- export function edgesOnPath(pathIds) {
44
- const pairs = [];
45
- for (let i = 0; i < pathIds.length - 1; i++) {
46
- const child = pathIds[i], parent = pathIds[i + 1];
47
- pairs.push({ child, parent });
48
- }
49
- return pairs;
50
- }
51
-
52
- /**
53
- * Optional: split segments at midpoints (useful for finer-grain highlighting).
54
- */
55
- export function splitEdges(segments) {
56
- const out = [];
57
- for (const s of segments) {
58
- const mx = (s.x1 + s.x2) / 2, my = (s.y1 + s.y2) / 2;
59
- out.push({ ...s, x2: mx, y2: my, half: "proximal" });
60
- out.push({ ...s, x1: mx, y1: my, half: "distal" });
61
- }
62
- return out;
63
- }
64
-
package/src/utils/mean.js DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * Iterable mean
3
- * Poached from https://github.com/d3/d3-array/blob/master/src/mean.js
4
- * (Other array means buggered up the tree)
5
- */
6
-
7
- export default function (values, valueof) {
8
- let count = 0;
9
- let sum = 0;
10
- if (valueof === undefined) {
11
- for (let value of values) {
12
- if (value != null && (value = +value) >= value) {
13
- ++count, sum += value;
14
- }
15
- }
16
- } else {
17
- let index = -1;
18
- for (let value of values) {
19
- if ((value = valueof(value, ++index, values)) != null && (value = +value) >= value) {
20
- ++count, sum += value;
21
- }
22
- }
23
- }
24
- if (count) return sum / count;
25
- }
@@ -1,20 +0,0 @@
1
- /*
2
- * It's on my TODO list to split all branches in half
3
- * so they can be highlighted separately later in the plot.
4
- * Find the mid point of two angles in radians
5
- * there are some subtleties I don't fully understand
6
- * e.g. if the start/end angles are equal to pi, the sign must be flipped.
7
- */
8
-
9
- export default function meanAngle(a, b) {
10
- // normalize to [0, 2pi)
11
- const norm = (θ) => (θ % (2*Math.PI) + 2*Math.PI) % (2*Math.PI);
12
- a = norm(a); b = norm(b);
13
-
14
- // handle wraparound by averaging unit vectors
15
- const X = (Math.cos(a) + Math.cos(b)) / 2;
16
- const Y = (Math.sin(a) + Math.sin(b)) / 2;
17
- if (X === 0 && Y === 0) return 0; // opposite directions, arbitrary
18
- return norm(Math.atan2(Y, X));
19
- }
20
-
@@ -1,27 +0,0 @@
1
- /**
2
- * Recursive function for breadth-first search of a tree
3
- * the root node is visited first.
4
- */
5
-
6
- function levelorder(root) {
7
- const queue = [root], result = [];
8
- while (queue.length) {
9
- const curnode = queue.shift(); // <- FIFO
10
- result.push(curnode);
11
- for (const child of curnode.children) queue.push(child);
12
- }
13
- return result;
14
- }
15
-
16
-
17
- /**
18
- * Count the number of tips that descend from this node
19
- */
20
-
21
- export default function(thisnode) {
22
- var result = 0;
23
- for (const node of levelorder(thisnode)) {
24
- if (node.children.length == 0) result++;
25
- }
26
- return (result);
27
- }
@@ -1,9 +0,0 @@
1
- export function makeIndexById(rows, key = "thisId") {
2
- return new Map(rows.map(d => [d[key], d]));
3
- }
4
- export default function parentFisheye(d, data) {
5
- const byId = makeIndexById(data);
6
- const parent = byId.get(d.parentId);
7
- return parent ? { px: parent.fisheye.x, py: parent.fisheye.y } : null;
8
- }
9
-
@@ -1,38 +0,0 @@
1
- /**
2
- * Recursive function for pre-order traversal of tree (returns array)
3
- */
4
- export function preorder(node, list = []) {
5
- list.push(node);
6
- for (let i = 0; i < (node.children?.length || 0); i++) {
7
- list = preorder(node.children[i], list);
8
- }
9
- return list;
10
- }
11
-
12
- /**
13
- * Iterative generator traversals (avoid recursion limits on large trees)
14
- */
15
- export function* preorderIter(root) {
16
- const stack = [root];
17
- while (stack.length) {
18
- const n = stack.pop();
19
- yield n;
20
- if (n.children) for (let i = n.children.length - 1; i >= 0; --i) stack.push(n.children[i]);
21
- }
22
- }
23
-
24
- export function* postorderIter(root) {
25
- const stack = [[root, 0]];
26
- while (stack.length) {
27
- const top = stack[stack.length - 1];
28
- const [n, i] = top;
29
- if (!n.children || i >= n.children.length) {
30
- stack.pop();
31
- yield n;
32
- } else {
33
- top[1] = i + 1;
34
- stack.push([n.children[i], 0]);
35
- }
36
- }
37
- }
38
-
@@ -1,64 +0,0 @@
1
- /**
2
- * Parse a Newick tree string into a doubly-linked list of JS Objects.
3
- * Assigns labels, branch lengths, and node IDs (tips before internals if input emits them that way).
4
- *
5
- * Notes / limitations:
6
- * - Quoted labels and NHX annotations are not fully supported.
7
- * - Branch lengths in scientific notation are supported (parseFloat).
8
- */
9
-
10
- export default function readTree(text) {
11
- // Remove all whitespace (space, tabs, newlines)
12
- text = String(text).replace(/\s+/g, '');
13
-
14
- const tokens = text.split(/(;|\(|\)|,)/);
15
- const root = { parent: null, children: [] };
16
- let curnode = root;
17
- let nodeId = 0;
18
-
19
- for (const token of tokens) {
20
- if (!token || token === ';') continue;
21
-
22
- if (token === '(') {
23
- const child = { parent: curnode, children: [] };
24
- curnode.children.push(child);
25
- curnode = child; // descend
26
- } else if (token === ',') {
27
- // back to parent, then create sibling
28
- curnode = curnode.parent;
29
- const child = { parent: curnode, children: [] };
30
- curnode.children.push(child);
31
- curnode = child;
32
- } else if (token === ')') {
33
- // ascend one level
34
- curnode = curnode.parent;
35
- if (curnode === null) break;
36
- } else {
37
- // label/branch-length chunk (e.g., "A:0.01" or "A")
38
- const nodeinfo = token.split(':');
39
- if (nodeinfo.length === 1) {
40
- if (token.startsWith(':')) {
41
- curnode.label = '';
42
- curnode.branchLength = parseFloat(nodeinfo[0]);
43
- } else {
44
- curnode.label = nodeinfo[0];
45
- curnode.branchLength = null;
46
- }
47
- } else if (nodeinfo.length === 2) {
48
- curnode.label = nodeinfo[0];
49
- curnode.branchLength = parseFloat(nodeinfo[1]);
50
- } else {
51
- console.warn(token, "Unhandled token with multiple ':' characters");
52
- curnode.label = nodeinfo[0] || '';
53
- curnode.branchLength = parseFloat(nodeinfo[nodeinfo.length - 1]);
54
- }
55
- curnode.id = nodeId++; // assign then increment
56
- }
57
- }
58
-
59
- // Ensure root has an id if not assigned during parsing
60
- if (root.id == null) root.id = nodeId;
61
-
62
- return root;
63
- }
64
-
@@ -1,44 +0,0 @@
1
- /**
2
- * Subset a tree given a node - i.e. the node of interests and all the descendents
3
- */
4
-
5
- export default function (tree, node) {
6
- // Thanks Richard Challis!
7
- let fullTree = {};
8
- tree.data.forEach(obj => {
9
- fullTree[obj.thisId] = { ...obj };
10
- });
11
-
12
- let subTree = {};
13
- const getDescendants = function (rootNodeId) {
14
- if (fullTree[rootNodeId]) {
15
- subTree[rootNodeId] = fullTree[rootNodeId];
16
- if (fullTree[rootNodeId].children) {
17
- fullTree[rootNodeId].children.forEach(childNodeId => {
18
- getDescendants(childNodeId);
19
- });
20
- }
21
- }
22
- };
23
- // call the recursive function
24
- getDescendants(node);
25
-
26
- // in each of the functions, data contains the children key
27
- const data = [["data", Object.values(subTree)]];
28
-
29
- const nodes = data[0][1].map(d => d.thisId);
30
-
31
- var res = [];
32
- // in all keys except data, push to new array
33
- for (const node in tree) {
34
- if (node === "data") continue;
35
- res.push([node, tree[node]]);
36
- }
37
-
38
- var filtered = res.map(d => [
39
- d[0],
40
- d[1].filter(d => nodes.includes(d.thisId))
41
- ]);
42
-
43
- return Object.fromEntries(data.concat(filtered));
44
- }