@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/utils/edges.js
CHANGED
|
@@ -4,52 +4,23 @@
|
|
|
4
4
|
* are the $edge slot. I think.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export default function (df, rectangular = false) {
|
|
8
|
-
|
|
9
|
-
|
|
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 = [];
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
for (const row of df) {
|
|
17
|
-
if (row.parentId === null) {
|
|
18
|
-
continue; // skip the root
|
|
19
|
-
}
|
|
20
|
-
parent = df[row.parentId];
|
|
21
|
-
if (parent === null || parent === undefined) continue;
|
|
12
|
+
for (const row of rows) {
|
|
13
|
+
if (row.parentId == null) continue;
|
|
14
|
+
const parent = byId.get(row.parentId);
|
|
15
|
+
if (!parent) continue;
|
|
22
16
|
|
|
23
17
|
if (rectangular) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
y1: row.y,
|
|
27
|
-
id1: row.thisId,
|
|
28
|
-
x2: parent.x,
|
|
29
|
-
y2: row.y,
|
|
30
|
-
id2: undefined
|
|
31
|
-
};
|
|
32
|
-
result.push(pair1);
|
|
33
|
-
var pair2 = {
|
|
34
|
-
x1: parent.x,
|
|
35
|
-
y1: row.y,
|
|
36
|
-
id1: undefined,
|
|
37
|
-
x2: parent.x,
|
|
38
|
-
y2: parent.y,
|
|
39
|
-
id2: row.parentId
|
|
40
|
-
};
|
|
41
|
-
result.push(pair2);
|
|
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 });
|
|
42
20
|
} else {
|
|
43
|
-
|
|
44
|
-
x1: row.x,
|
|
45
|
-
y1: row.y,
|
|
46
|
-
id1: row.thisId,
|
|
47
|
-
x2: parent.x,
|
|
48
|
-
y2: parent.y,
|
|
49
|
-
id2: row.parentId
|
|
50
|
-
};
|
|
51
|
-
result.push(pair3);
|
|
21
|
+
result.push({ x1: row.x, y1: row.y, id1: row.thisId, x2: parent.x, y2: parent.y, id2: row.parentId });
|
|
52
22
|
}
|
|
53
23
|
}
|
|
54
24
|
return result;
|
|
55
|
-
}
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
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/meanAngle.js
CHANGED
|
@@ -6,40 +6,15 @@
|
|
|
6
6
|
* e.g. if the start/end angles are equal to pi, the sign must be flipped.
|
|
7
7
|
*/
|
|
8
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);
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
) {
|
|
17
|
-
return Math.PI;
|
|
18
|
-
}
|
|
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
|
+
}
|
|
19
20
|
|
|
20
|
-
// calculate the average sin and cosine for the angles
|
|
21
|
-
const X = (Math.cos(start) + Math.cos(end)) / 2,
|
|
22
|
-
Y = (Math.sin(start) + Math.sin(end)) / 2;
|
|
23
|
-
|
|
24
|
-
const signX = Math.sign(X),
|
|
25
|
-
signY = Math.sign(Y);
|
|
26
|
-
|
|
27
|
-
var meanAngle;
|
|
28
|
-
// adjustments for the sign of each of X, Y
|
|
29
|
-
if (signX === 1 && signY === 1) {
|
|
30
|
-
meanAngle = Math.atan(Y / X);
|
|
31
|
-
} else if (signX === 1 && signY === -1) {
|
|
32
|
-
meanAngle = 2 * Math.PI - Math.atan(Y / X);
|
|
33
|
-
} else if (signX === -1 && signY === 1) {
|
|
34
|
-
meanAngle = Math.PI - Math.atan(Y / X);
|
|
35
|
-
} else if (signX === -1 && signY === -1) {
|
|
36
|
-
meanAngle = Math.PI + Math.atan(Y / X);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// if the start or end angles are equal to PI (or close approximations)
|
|
40
|
-
// flip the sign.
|
|
41
|
-
return start.toString().indexOf("3.14159") > -1 ||
|
|
42
|
-
end.toString().indexOf("3.14159") > -1
|
|
43
|
-
? -meanAngle
|
|
44
|
-
: meanAngle;
|
|
45
|
-
}
|
package/src/utils/numTips.js
CHANGED
|
@@ -4,29 +4,24 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
function levelorder(root) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
result.push(curnode);
|
|
15
|
-
for (const child of curnode.children) {
|
|
16
|
-
queue.push(child);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return (result);
|
|
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;
|
|
20
14
|
}
|
|
21
15
|
|
|
16
|
+
|
|
22
17
|
/**
|
|
23
18
|
* Count the number of tips that descend from this node
|
|
24
19
|
*/
|
|
25
20
|
|
|
26
|
-
export default function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
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,11 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
}
|
|
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
|
+
|
package/src/utils/preorder.js
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Recursive function for pre-order traversal of tree
|
|
2
|
+
* Recursive function for pre-order traversal of tree (returns array)
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
4
|
export function preorder(node, list = []) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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]);
|
|
9
35
|
}
|
|
10
|
-
|
|
36
|
+
}
|
|
11
37
|
}
|
|
38
|
+
|
package/src/utils/readTree.js
CHANGED
|
@@ -1,74 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Parse a Newick tree string into a doubly-linked
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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).
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
export default function (text) {
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
export default function readTree(text) {
|
|
11
|
+
// Remove all whitespace (space, tabs, newlines)
|
|
12
|
+
text = String(text).replace(/\s+/g, '');
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const tokens = text.split(/(;|\(|\)|,)/);
|
|
15
|
+
const root = { parent: null, children: [] };
|
|
16
|
+
let curnode = root;
|
|
17
|
+
let nodeId = 0;
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
continue
|
|
20
|
-
}
|
|
21
|
-
if (token == '(') {
|
|
22
|
-
// add a child to current node
|
|
23
|
-
let child = {
|
|
24
|
-
'parent': curnode,
|
|
25
|
-
'children': []
|
|
26
|
-
};
|
|
27
|
-
curnode.children.push(child);
|
|
28
|
-
curnode = child; // climb up
|
|
29
|
-
}
|
|
30
|
-
else if (token == ',') {
|
|
31
|
-
// climb down, add another child to parent
|
|
32
|
-
curnode = curnode.parent;
|
|
33
|
-
let child = {
|
|
34
|
-
'parent': curnode,
|
|
35
|
-
'children': []
|
|
36
|
-
}
|
|
37
|
-
curnode.children.push(child);
|
|
38
|
-
curnode = child; // climb up
|
|
39
|
-
}
|
|
40
|
-
else if (token == ')') {
|
|
41
|
-
// climb down twice
|
|
42
|
-
curnode = curnode.parent;
|
|
43
|
-
if (curnode === null) {
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
var nodeinfo = token.split(':');
|
|
19
|
+
for (const token of tokens) {
|
|
20
|
+
if (!token || token === ';') continue;
|
|
49
21
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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;
|
|
68
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
|
|
69
56
|
}
|
|
57
|
+
}
|
|
70
58
|
|
|
71
|
-
|
|
59
|
+
// Ensure root has an id if not assigned during parsing
|
|
60
|
+
if (root.id == null) root.id = nodeId;
|
|
72
61
|
|
|
73
|
-
|
|
62
|
+
return root;
|
|
74
63
|
}
|
|
64
|
+
|