@euphrasiologist/lwphylo 1.1.7 → 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.
@@ -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,11 +2,13 @@ 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) {
11
+ export default function getHorizontal(node) {
10
12
  const pd = fortify(node);
11
13
 
12
14
  // Fast lookup from id -> pd index
@@ -22,7 +24,7 @@ export default function(node) {
22
24
  // Map each leaf id to a vertical slot (1..N)
23
25
  const tipSlot = new Map(leafIds.map((id, i) => [id, i + 1]));
24
26
 
25
- // 2) Set Y for tips directly from that order; compute Y for internal nodes via postorder
27
+ // 2) Set Y for tips directly from that order; internal node Y via children mean
26
28
  (function setY(n) {
27
29
  const i = idIndex.get(n.id);
28
30
  if (!n.children || n.children.length === 0) {
@@ -36,7 +38,7 @@ export default function(node) {
36
38
  return y;
37
39
  })(node);
38
40
 
39
- // 3) Set X by accumulating branch lengths down the tree (no sorting-by-id needed)
41
+ // 3) Set X by accumulating branch lengths down the tree
40
42
  (function setX(n, xParent) {
41
43
  const i = idIndex.get(n.id);
42
44
  const bl = pd[i].branchLength ?? 0;
@@ -46,6 +48,10 @@ export default function(node) {
46
48
  if (n.children && n.children.length) n.children.forEach(c => setX(c, x1));
47
49
  })(node, 0);
48
50
 
49
- // Clean up and return
50
- return pd.map(({ y, x, angle, ...item }) => item);
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
+ });
51
56
  }
57
+
@@ -1,6 +1,6 @@
1
1
  import getHorizontal from "./getHorizontal.js"
2
2
 
3
- export default function (node) {
3
+ export default function getVertical(node) {
4
4
  const data = getHorizontal(node);
5
5
 
6
6
  // Group rows by parentId (children that share a parent)
@@ -33,3 +33,4 @@ export default function (node) {
33
33
 
34
34
  return verticals;
35
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
+ }
@@ -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
+