@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.
- package/dist/index.js +2 -0
- package/package.json +11 -7
- package/src/phisheye/phisheye.js +111 -65
- package/src/radial/describeArc.js +22 -12
- package/src/radial/getArcs.js +47 -56
- package/src/radial/getRadii.js +32 -38
- package/src/radial/polarToCartesian.js +5 -7
- package/src/radial/radialData.js +71 -89
- package/src/radial/radialLayout.js +8 -9
- package/src/radial/reflectAngle.js +7 -17
- package/src/rectangle/getChildVerticals.js +47 -0
- package/src/rectangle/getHorizontal.js +49 -82
- package/src/rectangle/getVertical.js +29 -34
- package/src/rectangle/rectangleLayout.js +23 -10
- package/src/unrooted/equalAngleLayout.js +36 -25
- package/src/unrooted/unrooted.js +1 -1
- package/src/utils/edges.js +13 -42
- package/src/utils/indexing.js +64 -0
- package/src/utils/meanAngle.js +10 -35
- package/src/utils/numTips.js +15 -20
- package/src/utils/parentFisheye.js +9 -11
- package/src/utils/preorder.js +33 -6
- package/src/utils/readTree.js +53 -63
- package/dist/@euphrasiologist/lwphylo.js +0 -867
- package/dist/@euphrasiologist/lwphylo.min.js +0 -2
package/src/radial/radialData.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
for (
|
|
19
|
-
if (
|
|
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
|
-
//
|
|
26
|
-
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* thanks https://codereview.stackexchange.com/questions/187510/angle-reflection-function
|
|
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.
|
|
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
|
|
6
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
pd.map(d => d.
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
12
|
-
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
13
|
-
node.start = 0.;
|
|
14
|
-
node.end = 2.;
|
|
15
|
-
node.angle = 0.;
|
|
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
|
-
|
|
34
|
+
let lastStart = node.start;
|
|
22
35
|
|
|
23
|
-
for (
|
|
24
|
-
|
|
25
|
-
|
|
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 =
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
55
|
+
|
|
46
56
|
return node;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
export default equalAngleLayout
|
|
59
|
+
export default equalAngleLayout
|
|
60
|
+
|
package/src/unrooted/unrooted.js
CHANGED