@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.
@@ -4,52 +4,23 @@
4
4
  * are the $edge slot. I think.
5
5
  */
6
6
 
7
- export default function (df, rectangular = false) {
8
- var result = [],
9
- parent
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
- // make sure data frame is sorted
12
- df.sort(function (a, b) {
13
- return a.thisId - b.thisId;
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
- var pair1 = {
25
- x1: row.x,
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
- var pair3 = {
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
+
@@ -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
- export default function (start, end) {
11
- // if the angles are exactly equal but opposite
12
- // return Math.PI (else undefined is returned...)
13
- if (
14
- Math.sign(start) !== Math.sign(end) &&
15
- Math.abs(start) === Math.abs(end)
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
- }
@@ -4,29 +4,24 @@
4
4
  */
5
5
 
6
6
  function levelorder(root) {
7
- // aka breadth-first search
8
- var queue = [root],
9
- result = [],
10
- curnode;
11
-
12
- while (queue.length > 0) {
13
- curnode = queue.pop();
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 (thisnode) {
27
- var result = 0;
28
- for (const node of levelorder(thisnode)) {
29
- if (node.children.length == 0) result++;
30
- }
31
- return (result);
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
- // find the x & y coordinates of the parental species
2
- export default function (d, data /* e.g. lwPhylo.unrooted.data */) {
3
- for (let i = 0; i < data.length; i++) {
4
- if (d.parentId === data[i].thisId) {
5
- return {
6
- px: data[i].fisheye.x,
7
- py: data[i].fisheye.y
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
+
@@ -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
- list.push(node);
7
- for (var i = 0; i < node.children.length; i++) {
8
- list = preorder(node.children[i], 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]);
9
35
  }
10
- return (list);
36
+ }
11
37
  }
38
+
@@ -1,74 +1,64 @@
1
1
  /**
2
- * Parse a Newick tree string into a doubly-linked
3
- * list of JS Objects. Assigns node labels, branch
4
- * lengths and node IDs (numbering terminal before
5
- * internal nodes).
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
- // remove whitespace
10
- text = text.replace(/ \t/g, '');
10
+ export default function readTree(text) {
11
+ // Remove all whitespace (space, tabs, newlines)
12
+ text = String(text).replace(/\s+/g, '');
11
13
 
12
- var tokens = text.split(/(;|\(|\)|,)/),
13
- root = { 'parent': null, 'children': [] },
14
- curnode = root,
15
- nodeId = 0;
14
+ const tokens = text.split(/(;|\(|\)|,)/);
15
+ const root = { parent: null, children: [] };
16
+ let curnode = root;
17
+ let nodeId = 0;
16
18
 
17
- for (const token of tokens) {
18
- if (token == "" || token == ';') {
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
- if (nodeinfo.length == 1) {
51
- if (token.startsWith(':')) {
52
- curnode.label = "";
53
- curnode.branchLength = parseFloat(nodeinfo[0]);
54
- } else {
55
- curnode.label = nodeinfo[0];
56
- curnode.branchLength = null;
57
- }
58
- }
59
- else if (nodeinfo.length == 2) {
60
- curnode.label = nodeinfo[0];
61
- curnode.branchLength = parseFloat(nodeinfo[1]);
62
- }
63
- else {
64
- // TODO: handle edge cases with >1 ":"
65
- console.warn(token, "I don't know what to do with two colons!");
66
- }
67
- curnode.id = nodeId++; // assign then increment
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
- curnode.id = nodeId;
59
+ // Ensure root has an id if not assigned during parsing
60
+ if (root.id == null) root.id = nodeId;
72
61
 
73
- return (root);
62
+ return root;
74
63
  }
64
+