@euphrasiologist/lwphylo 1.1.5 → 1.1.9

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,102 +1,84 @@
1
1
  import fortify from "../utils/fortify.js"
2
- import numTips from "../utils/numTips.js"
3
- import mean from "../utils/mean.js"
4
2
 
5
3
  /**
6
- * Take a parsed tree and get the important data from them (i.e. radii, arcs).
4
+ * Compute per-node polar coordinates for radial layout:
5
+ * - Tip angles: evenly spaced 0..2π in tip DFS order
6
+ * - Internal angles: circular mean of child angles
7
+ * - Radii: cumulative branch length from root
8
+ * - x,y: cartesian projection
9
+ *
10
+ * Returns the fortified array with added {angle, r, x, y}.
7
11
  */
8
-
9
- export default function (node) {
10
- // should start out the same as get_horizontal
11
- // it's very similar in fact, but keeping separate for clarity.
12
- var pd = fortify(node);
13
-
14
- var tip_number = numTips(node);
15
-
16
- // make tip angles
17
- var tipID = 1;
18
- for (let i = 0; i < pd.length; i++) {
19
- if (pd[i].isTip == true) {
20
- pd[i].angle = (tipID / tip_number) * 2 * Math.PI;
21
- tipID += 1;
22
- }
12
+ export default function radialData(node) {
13
+ const TAU = Math.PI * 2;
14
+ const norm = (t) => ((t % TAU) + TAU) % TAU;
15
+
16
+ const pd = fortify(node, /*sort*/ true);
17
+ const byId = new Map(pd.map(d => [d.thisId, d]));
18
+ const kids = new Map(pd.map(d => [d.thisId, d.children || []]));
19
+
20
+ // Find root id
21
+ let root = null;
22
+ for (const d of pd) {
23
+ if (d.parentId == null) { root = d.thisId; break; }
23
24
  }
24
25
 
25
- // probably incredibly inefficient for large trees.
26
- // gets the y values of two child branches by looping through the whole tree...
27
- function internalNodeAngle(child_1, child_2) {
28
- for (var i = 0; i < pd.length; i++) {
29
- if (pd[i].thisId === child_1) {
30
- var angle_1 = pd[i].angle;
31
- }
32
- if (pd[i].thisId === child_2) {
33
- var angle_2 = pd[i].angle;
34
- }
26
+ // Collect tip ids in DFS left->right order to preserve input ordering
27
+ const tipIds = [];
28
+ (function dfs(id) {
29
+ const c = kids.get(id) || [];
30
+ if (c.length === 0) {
31
+ tipIds.push(id);
32
+ return;
35
33
  }
36
- return mean([angle_1, angle_2]); // see utils.js
37
- }
38
-
39
- // if the node is not a tip...
40
- for (let i = 0; i < pd.length; i++) {
41
- if (pd[i].isTip === false) {
42
- // then y0 === y1 and is the mean of the parental nodes
43
- pd[i].angle = internalNodeAngle(pd[i].children[0], pd[i].children[1]);
44
- }
45
- }
46
-
47
- // find root
48
- var root = pd.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
49
-
50
- // sort the data temporarily in decreasing parentId
51
- pd.sort((a, b) => b.thisId - a.thisId);
52
-
53
- // get the branchlength of the parentID
54
- function getParentBranchLength(current_parentId) {
55
- for (var i = 0; i < pd.length; i++) {
56
- if (pd[i].thisId === current_parentId) {
57
- var branchLength = pd[i].r;
34
+ for (const ch of c) dfs(ch);
35
+ })(root);
36
+
37
+ // Assign tip angles evenly spaced 0..2π
38
+ const N = Math.max(1, tipIds.length);
39
+ const angle = new Map();
40
+ tipIds.forEach((id, i) => {
41
+ angle.set(id, (i / N) * TAU);
42
+ });
43
+
44
+ // Internal node angles: circular mean of child angles (post-order)
45
+ (function setInternalAngles(id) {
46
+ const c = kids.get(id) || [];
47
+ for (const ch of c) setInternalAngles(ch);
48
+ if (c.length > 0) {
49
+ let sx = 0, sy = 0;
50
+ for (const ch of c) {
51
+ const th = angle.get(ch);
52
+ sx += Math.cos(th);
53
+ sy += Math.sin(th);
58
54
  }
55
+ angle.set(id, norm(Math.atan2(sy, sx)));
59
56
  }
60
- return branchLength;
61
- }
62
-
63
- // assign depths (branch lengths) to radii
64
- for (let i = 0; i < pd.length; i++) {
65
- // special cases where parent is the root.
66
- if (pd[i].parentId === root) {
67
-
68
- if (pd[i].isTip === true) {
69
- // radius is branch length at root?
70
- pd[i].r = pd[i].branchLength;
71
- pd[i].x = pd[i].branchLength;
72
- pd[i].y = 0;
73
-
74
- } else { // it's a node
75
- // radius is branch length at root?
76
- pd[i].r = pd[i].branchLength;
77
- pd[i].x = pd[i].branchLength * Math.cos(pd[i].angle);
78
- pd[i].y = pd[i].branchLength * Math.sin(pd[i].angle);
79
- }
80
-
81
- } else {
82
- // the x0 is that of the parent
83
- var parent_branchLength = getParentBranchLength(pd[i].parentId);
84
- // the x1 is the sum of parent and current branchlength
85
- pd[i].r = parent_branchLength + pd[i].branchLength;
86
- // at the same time we can make x and y from polar coordinates
87
- // this is creating some NaN's, may cause problems later.
88
- pd[i].x = (parent_branchLength + pd[i].branchLength) * Math.cos(pd[i].angle);
89
- pd[i].y = (parent_branchLength + pd[i].branchLength) * Math.sin(pd[i].angle);
57
+ })(root);
58
+
59
+ // Radii: cumulative branch lengths from root (root r=0)
60
+ const radius = new Map();
61
+ radius.set(root, 0);
62
+ (function setR(id) {
63
+ const c = kids.get(id) || [];
64
+ const r0 = radius.get(id) || 0;
65
+ for (const ch of c) {
66
+ const child = byId.get(ch);
67
+ const bl = child?.branchLength ?? 0;
68
+ radius.set(ch, r0 + bl);
69
+ setR(ch);
90
70
  }
71
+ })(root);
72
+
73
+ // Enrich pd rows with angle, r, x, y
74
+ for (const d of pd) {
75
+ const th = angle.get(d.thisId) ?? 0;
76
+ const r = radius.get(d.thisId) ?? 0;
77
+ d.angle = th;
78
+ d.r = r;
79
+ d.x = r * Math.cos(th);
80
+ d.y = r * Math.sin(th);
91
81
  }
92
82
 
93
- // at this point the first element is the root
94
- pd[0].r = 0;
95
- pd[0].x = 0;
96
- pd[0].y = 0;
97
-
98
- // return the original sorted data
99
- //pd.sort((a,b) => a.thisId - b.thisId)
100
-
101
83
  return pd;
102
- }
84
+ }
@@ -3,17 +3,16 @@ import getRadii from "./getRadii.js"
3
3
  import getArcs from "./getArcs.js"
4
4
 
5
5
  /**
6
- * Simple wrapper function for getting the data,
7
- * radii, and arcs.
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
8
10
  */
9
-
10
- export default function (node) {
11
- var data = {};
12
-
13
- // TODO: does radial_data need to be printed out?
11
+ export default function radialLayout(node) {
12
+ const data = {};
14
13
  data.data = radialData(node);
15
14
  data.radii = getRadii(node);
16
15
  data.arcs = getArcs(data.data);
17
-
18
16
  return data;
19
- }
17
+ }
18
+
@@ -1,20 +1,10 @@
1
1
  /**
2
- * Takes parent data and returns a start value (radians),
3
- * end value (radians), and radius of circle to draw an
4
- * arc from.
5
- * thanks https://codereview.stackexchange.com/questions/187510/angle-reflection-function
2
+ * Legacy shim: normalize angle to [0, ).
3
+ * The new radial code does not need geometric reflections;
4
+ * it uses circular spans and atan2.
6
5
  */
6
+ export default function reflectAngle(rad /*, dir */) {
7
+ const TAU = Math.PI * 2;
8
+ return ((rad % TAU) + TAU) % TAU;
9
+ }
7
10
 
8
- export default function (rad, dir) {
9
- const c = Math.cos(rad), s = Math.sin(rad);
10
- const PI_sub = "3.1415";
11
-
12
- function checkSign(x) {
13
- if (x.toString().includes(PI_sub)) {
14
- x = Math.abs(x);
15
- }
16
- return x;
17
- }
18
-
19
- return checkSign(Math.atan2(...(dir === "X" ? [s, -c] : [-s, c])));
20
- }
@@ -0,0 +1,47 @@
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
+
@@ -2,89 +2,56 @@ import mean from "../utils/mean.js"
2
2
  import fortify from "../utils/fortify.js"
3
3
 
4
4
  /**
5
- * Rectangle layout algorithm.
6
- * Attempted copy of .layout.rect() function here https://github.com/ArtPoon/ggfree/blob/master/R/tree.R
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
7
9
  */
8
10
 
9
- export default function (node) {
10
- // phylodata layout...
11
- var pd = fortify(node);
12
- // set y to null... not needed here.
13
- pd.map(d => d.y = null);
14
- // where y corresponds to a tip, return tip number
15
- // make y0 and y1 equal.
16
- var tipID = 1;
17
- for (let i = 0; i < pd.length; i++) {
18
- if (pd[i].isTip === true) {
19
- pd[i].y0 = tipID;
20
- pd[i].y1 = tipID;
21
- tipID += 1;
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;
22
34
  }
23
- }
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
+ }
24
57
 
25
- // probably incredibly inefficient for large trees.
26
- // gets the y values of two child branches by looping through the whole tree...
27
- function yVals(child_1, child_2) {
28
- for (var i = 0; i < pd.length; i++) {
29
- if (pd[i].thisId === child_1) {
30
- var y1 = pd[i].y0;
31
- }
32
- if (pd[i].thisId === child_2) {
33
- var y2 = pd[i].y0;
34
- }
35
- }
36
- return mean(([y1, y2]))
37
- }
38
-
39
- // if the node is not a tip...
40
- for (let i = 0; i < pd.length; i++) {
41
- if (pd[i].isTip === false) {
42
- // then y0 === y1 and is the mean of the parental nodes
43
- pd[i].y0 = yVals(pd[i].children[0], pd[i].children[1]);
44
- pd[i].y1 = yVals(pd[i].children[0], pd[i].children[1]);
45
- }
46
- }
47
-
48
- // find root
49
- var root = pd.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
50
-
51
- // sort the data temporarily in decreasing parentId
52
- pd.sort((a, b) => b.thisId - a.thisId);
53
-
54
- // get the branchlength of the parentID
55
- function getParentBranchLength(current_parentId) {
56
- for (var i = 0; i < pd.length; i++) {
57
- if (pd[i].thisId === current_parentId) {
58
- var branchLength = pd[i].x1;
59
- }
60
- }
61
- return branchLength;
62
- }
63
-
64
- // last loop...
65
- // now get the x0 and x1 coordinates.
66
- for (let i = 0; i < pd.length; i++) {
67
- // special cases where parent is the root.
68
- if (pd[i].parentId === root) {
69
- // x0 = 0 and x1 is branch length
70
- pd[i].x0 = 0;
71
- pd[i].x1 = pd[i].branchLength;
72
- } else {
73
- // the x0 is that of the parent
74
- var parent_branchLength = getParentBranchLength(pd[i].parentId);
75
- pd[i].x0 = parent_branchLength;
76
- // the x1 is the sum of parent and current branchlength
77
- pd[i].x1 = parent_branchLength + pd[i].branchLength;
78
- }
79
- }
80
-
81
- // return the original sorted data
82
- pd.sort((a, b) => a.thisId - b.thisId);
83
-
84
- // remove root?
85
-
86
- // finally get rid of unwanted y, x and angle properties
87
- /* eslint-disable no-unused-vars */
88
- return pd.map(({ y, x, angle, ...item }) => item);
89
- /* eslint-enable no-unused-vars */
90
- }
@@ -1,41 +1,36 @@
1
1
  import getHorizontal from "./getHorizontal.js"
2
2
 
3
- /**
4
- * Get the vertical lines to draw.
5
- */
6
-
7
- export default function (node) {
8
- var data = getHorizontal(node);
9
-
10
- // for the current iteration of the loop find the matching parentId
11
- // then take the difference
12
- function findPairs(current_node) {
13
- for (var i = 0; i < data.length; i++) {
14
- if (data[i].parentId === current_node.parentId) {
15
- var height = Math.abs(data[i].y0 - current_node.y0);
16
- }
17
- }
18
- return height;
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]);
19
12
  }
20
13
 
21
- var verticals = [];
22
- // find root
23
- var root = data.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
24
-
25
- for (var i = 0; i < data.length; i++) {
26
- if (data[i].thisId !== root) {
27
-
28
- verticals.push({
29
- 'thisId': data[i].thisId,
30
- 'x0': data[i].x0,
31
- 'x1': data[i].x0, // x values remain constant
32
- 'y0': data[i].y0,
33
- 'y1': data[i].y0 + findPairs(data[i]),
34
- 'heights': findPairs(data[i])
35
- })
36
-
37
- }
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
+ });
38
32
  }
39
33
 
40
34
  return verticals;
41
- }
35
+ }
36
+
@@ -1,15 +1,28 @@
1
- import getHorizontal from "./getHorizontal.js"
2
- import getVertical from "./getVertical.js"
1
+ import getHorizontal from "./getHorizontal.js";
2
+ import getVertical from "./getVertical.js";
3
+ import getChildVerticals from "./getChildVerticals.js";
3
4
 
4
5
  /**
5
- * Simple wrapper for rectangle layout functions
6
+ * Rectangle layout wrapper.
7
+ * Returns:
8
+ * - data: per-node rows (x0,x1,y0=y1,...)
9
+ * - vertical_lines: single spanning vertical per parent (fast baseline draw)
10
+ * - child_vertical_lines: one vertical per edge (child->parent) for highlighting
11
+ * - horizontal_lines: child horizontals from parent.x -> child.x at child.y
6
12
  */
13
+ export default function rectangleLayout(node) {
14
+ const data = getHorizontal(node); // per-node horizontally resolved
15
+ const vertical_lines = getVertical(node); // single spanning vertical per parent
16
+ const child_vertical_lines = getChildVerticals(node); // one vertical per child edge
7
17
 
8
- export default function (node) {
9
- var data = {};
18
+ const horizontal_lines = data
19
+ .filter(d => d.parentId != null)
20
+ .map(d => ({
21
+ parentId: d.parentId,
22
+ childId: d.thisId,
23
+ x0: d.x0, x1: d.x1,
24
+ y: d.y0
25
+ }));
10
26
 
11
- data.data = getHorizontal(node); // horizontal_lines
12
- data.vertical_lines = getVertical(node);
13
-
14
- return data;
15
- }
27
+ return { data, vertical_lines, child_vertical_lines, horizontal_lines };
28
+ }
@@ -1,49 +1,60 @@
1
1
  import numTips from "../utils/numTips.js"
2
2
 
3
3
  /**
4
- * Equal-angle layout algorithm for unrooted trees.
5
- * Populates the nodes of a tree object with information on
6
- * the angles to draw branches such that they do not
7
- * intersect.
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
8
  */
9
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
+
10
23
  function equalAngleLayout(node) {
11
24
  if (node.parent === null) {
12
- // node is root
13
- node.start = 0.; // guarantees no arcs overlap 0
14
- node.end = 2.; // *pi
15
- node.angle = 0.; // irrelevant
16
- node.ntips = numTips(node);
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
17
30
  node.x = 0;
18
31
  node.y = 0;
19
32
  }
20
33
 
21
- var child, arc, lastStart = node.start;
34
+ let lastStart = node.start;
22
35
 
23
- for (var i = 0; i < node.children.length; i++) {
24
- // the child of the current node
25
- child = node.children[i];
26
- // the number of tips the child node has
27
- child.ntips = numTips(child);
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);
28
39
 
29
- // assign proportion of arc to this child
30
- arc = (node.end - node.start) * child.ntips / node.ntips;
31
40
  child.start = lastStart;
32
- child.end = child.start + arc;
41
+ child.end = lastStart + arc;
33
42
 
34
- // bisect the arc
43
+ // bisect the arc in π-units
35
44
  child.angle = child.start + (child.end - child.start) / 2.;
36
45
  lastStart = child.end;
37
46
 
38
- // map to coordinates
39
- child.x = node.x + child.branchLength * Math.sin(child.angle * Math.PI);
40
- child.y = node.y + child.branchLength * Math.cos(child.angle * Math.PI);
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);
41
52
 
42
- // climb up
43
53
  equalAngleLayout(child);
44
54
  }
45
- // had to add this!
55
+
46
56
  return node;
47
57
  }
48
58
 
49
- export default equalAngleLayout
59
+ export default equalAngleLayout
60
+
@@ -15,4 +15,4 @@ export default function (node) {
15
15
  data.edges = edges(eq);
16
16
 
17
17
  return data;
18
- }
18
+ }