Skip to content

Commit

Permalink
Ultrametric option (#444)
Browse files Browse the repository at this point in the history
* ENH add function for computing ultrametric lengths

Co-authored-by: Gibs <[email protected]>

* ENH make getUltraMetricLengths correspond to tree index

Co-authored-by: Gibs <[email protected]>

* MAINT refactor layouts to take arbitrary length getter

Co-authored-by: Gibs <[email protected]>

* DOC include docstring argument for getLength

Co-authored-by: Gibs <[email protected]>

* STY make jsstye for ultrametric

Co-authored-by: Gibs <[email protected]>

* DOC add explanation of ultrametric algorithm

* FIX lengthGetter pass into circularLayout

* ENH add radio button and improve logic for branch length choice

* FIX some comments

Co-authored-by: Marcus Fedarko <[email protected]>

* ENH fix html stuff from marcus code review

Co-authored-by: Marcus Fedarko <[email protected]>

* ENH add section for clade sorting

* ENH remove red from branch lengths warning

* ENH change caps

* ENH hide branch length warning when not in use

* ENH replace deteremine lenghs text

* MAINT jsstyle for branch methods

* DOC add comment for _determineLengthGetter

* TST ensure ultrametric tree stays same

* MAINT style on test

* ENH clarify logic for determining branch lengths

* DOC more specific comments

Co-authored-by: Marcus Fedarko <[email protected]>

* INT change interface display of branch lengths

* Update empress/support_files/js/side-panel-handler.js

Co-authored-by: Marcus Fedarko <[email protected]>

* MAINT remove ignoreLengths argument to layout functions

* MAINT tests style

* Update empress/support_files/templates/side-panel.html

* Update empress/support_files/js/side-panel-handler.js

Co-authored-by: Gibs <[email protected]>
Co-authored-by: Marcus Fedarko <[email protected]>
  • Loading branch information
3 people authored Nov 20, 2020
1 parent c7a7248 commit 9235709
Show file tree
Hide file tree
Showing 5 changed files with 346 additions and 68 deletions.
32 changes: 27 additions & 5 deletions empress/support_files/js/empress.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,12 @@ define([
*/
this.ignoreLengths = false;

/**
* @type{String}
* Branch length method: one of "normal", "ignore", or "ultrametric"
*/
this.branchMethod = "normal";

/**
* @type{String}
* Leaf sorting method: one of "none", "ascending", or "descending"
Expand Down Expand Up @@ -365,14 +371,28 @@ define([
*/
Empress.prototype.getLayoutInfo = function () {
var data, i;
// set up length getter
var branchMethod = this.branchMethod;
var lengthGetter = LayoutsUtil.getLengthMethod(
branchMethod,
this._tree
);

// Rectangular
if (this._currentLayout === "Rectangular") {
data = LayoutsUtil.rectangularLayout(
this._tree,
4020,
4020,
this.ignoreLengths,
this.leafSorting
// since lengths for "ignoreLengths" are set by `lengthGetter`,
// we don't need (and should likely deprecate) the ignoreLengths
// option for the Layout functions since the layout function only
// needs to know lengths in order to layout a tree, it doesn't
// really need encapsulate all of the logic for determining
// what lengths it should lay out.
this.leafSorting,
undefined,
lengthGetter
);
this._yrscf = data.yScalingFactor;
for (i = 1; i <= this._tree.size; i++) {
Expand All @@ -392,8 +412,9 @@ define([
this._tree,
4020,
4020,
this.ignoreLengths,
this.leafSorting
this.leafSorting,
undefined,
lengthGetter
);
for (i = 1; i <= this._tree.size; i++) {
// remove old layout information
Expand All @@ -417,7 +438,8 @@ define([
this._tree,
4020,
4020,
this.ignoreLengths
undefined,
lengthGetter
);
for (i = 1; i <= this._tree.size; i++) {
// remove old layout information
Expand Down
173 changes: 155 additions & 18 deletions empress/support_files/js/layouts-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,128 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
return postOrderNodes;
}

/**
* Compute ultrametric lengths on a tree
*
* @param {BPTree} tree The tree to generate the lengths for.
*
* @returns {Object} Keys are the index position of the node in tree.
* Values are the length of the node in an ultrametric tree.
*/
function getUltrametricLengths(tree) {
var lengths = {};
var i;
var j;
var maxNodeToTipDistance = new Array(tree.size);
var depths = new Array(tree.size);
var nodeIndex;
var children;
var child;
/*
This loop is responsible for finding the maximum distance from
each node to its deepest tip.
*/
for (i = 1; i <= tree.size; i++) {
nodeIndex = tree.postorderselect(i);
if (tree.isleaf(nodeIndex)) {
maxNodeToTipDistance[nodeIndex] = 0;
} else {
var maxDist = 0;
children = tree.getChildren(nodeIndex);
for (j = 0; j < children.length; j++) {
child = children[j];
var childMaxLen =
maxNodeToTipDistance[child] + tree.length(child);
if (childMaxLen > maxDist) {
maxDist = childMaxLen;
}
}
maxNodeToTipDistance[nodeIndex] = maxDist;
}
}
/*
This loop is responsible for determining new branch lengths.
The lengths for intermediate nodes are effectively "stretched" until
their deepest descendant hits the deepest level in the whole tree.
E.g., if we are at the node represented by * in the tree below:
|--------------------------maxDistance-------------------------|
|--distanceAbove--| |---distanceBelow---|
|-length--| |-remainder-|
____
___________|
*__________| |_______
__________________| |__
|
|___________________________________________
then the branch will be extended so that its deepest tip has the
same depth as the deepest tip in the whole tree,
i.e., newLength = length + remainder
however, below it is equivalently calculated with
newLength = maxDistance - distanceAbove - distanceBelow
E.g.,
|--------------------------maxDistance-------------------------|
|--distanceAbove--| |---distanceBelow---|
|-length--||-remainder-|
____
___________|
*_______________________| |_______
__________________| |__
|
|___________________________________________
Repeated in a pre-order traversal, this will result in an ultrametric tree
*/
var maxDistance = maxNodeToTipDistance[tree.root()];
depths[tree.root()] = 0;
lengths[tree.root()] = tree.depth(tree.root());
for (i = 1; i <= tree.size; i++) {
nodeIndex = tree.preorderselect(i);
children = tree.getChildren(nodeIndex);
for (j = 0; j < children.length; j++) {
child = children[j];
var distanceAbove = depths[nodeIndex];
var distanceBelow = maxNodeToTipDistance[child];
lengths[child] = maxDistance - distanceAbove - distanceBelow;
depths[child] = distanceAbove + lengths[child];
}
}
return lengths;
}

/**
* Gets a method for determining branch lengths by name, parameterized on a tree.
*
* @param {String} methodName Method for determing branch lengths.
* One of ("ultrametric", "ignore", "normal").
* @param {BPTree} tree Tree that needs branch lengths determined.
* @returns {Function} A function that maps node indices to branch lengths.
*/
function getLengthMethod(methodName, tree) {
var lengthGetter;
if (methodName === "ultrametric") {
var ultraMetricLengths = getUltrametricLengths(tree);
lengthGetter = function (i) {
return ultraMetricLengths[i];
};
} else if (methodName === "ignore") {
lengthGetter = function (i) {
return 1;
};
} else if (methodName === "normal") {
lengthGetter = function (i) {
return tree.length(i);
};
} else {
throw "Invalid method: '" + methodName + "'.";
}
return lengthGetter;
}

/**
* Computes the "scale factor" for the circular / unrooted layouts.
*
Expand Down Expand Up @@ -137,12 +259,13 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
* displayed.
* @param {Float} height Height of the canvas where the tree will be
* displayed.
* @param {Boolean} ignoreLengths If falsy, branch lengths are used in the
* layout; otherwise, a uniform length of 1
* is used.
* @param {String} leafSorting See the getPostOrderNodes() docs above.
* @param {Boolean} normalize If true, then the tree will be scaled up to
* fill the bounds of width and height.
* @param {Function} lengthGetter Is a function that takes a single argument
* that corresponds to the index of a node in
* tree. Returns the length of the node at that
* index. Defaults to 'normal' method.
* @return {Object} Object with the following properties:
* -xCoords
* -yCoords
Expand All @@ -157,9 +280,9 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
tree,
width,
height,
ignoreLengths,
leafSorting,
normalize = true
normalize = true,
lengthGetter = null
) {
var maxWidth = 0;
var maxHeight = 0;
Expand All @@ -168,6 +291,9 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
var yCoord = new Array(tree.size + 1).fill(0);
var highestChildYr = new Array(tree.size + 1);
var lowestChildYr = new Array(tree.size + 1);
if (lengthGetter === null) {
lengthGetter = getLengthMethod("normal", tree);
}

var postOrderNodes = getPostOrderNodes(tree, leafSorting);
var i;
Expand Down Expand Up @@ -203,7 +329,7 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
var node = tree.postorder(prepos);
parent = tree.postorder(tree.parent(prepos));

var nodeLen = ignoreLengths ? 1 : tree.length(prepos);
var nodeLen = lengthGetter(prepos);
xCoord[node] = xCoord[parent] + nodeLen;
if (maxWidth < xCoord[node]) {
maxWidth = xCoord[node];
Expand Down Expand Up @@ -340,12 +466,13 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
* displayed.
* @param {Float} height Height of the canvas where the tree will be
* displayed.
* @param {Boolean} ignoreLengths If falsy, branch lengths are used in the
* layout; otherwise, a uniform length of 1
* is used.
* @param {String} leafSorting See the getPostOrderNodes() docs above.
* @param {Boolean} normalize If true, then the tree will be scaled up to
* fill the bounds of width and height.
* @param {Function} lengthGetter Is a function that takes a single argument
* that corresponds to the index of a node in
* tree. Returns the length of the node at that
* index. Defaults to 'normal' method.
* @return {Object} Object with the following properties:
* -x0, y0 ("starting point" x and y)
* -x1, y1 ("ending point" x and y)
Expand All @@ -363,9 +490,9 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
tree,
width,
height,
ignoreLengths,
leafSorting,
normalize = true
normalize = true,
lengthGetter = null
) {
// Set up arrays we're going to store the results in
var x0 = new Array(tree.size + 1).fill(0);
Expand Down Expand Up @@ -399,6 +526,10 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
var maxY = 0,
minY = Number.POSITIVE_INFINITY;

if (lengthGetter === null) {
lengthGetter = getLengthMethod("normal", tree);
}

// Iterate over the tree in postorder, assigning angles
// Note that we skip the root (using "p < postOrderNodes.length - 1"),
// since the root's angle is irrelevant.
Expand Down Expand Up @@ -435,7 +566,7 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
var node = tree.postorder(prepos);
var parent = tree.postorder(tree.parent(prepos));

var nodeLen = ignoreLengths ? 1 : tree.length(prepos);
var nodeLen = lengthGetter(prepos);
radius[node] = radius[parent] + nodeLen;
}

Expand Down Expand Up @@ -572,11 +703,12 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
* displayed.
* @param {Float} height Height of the canvas where the tree will be
* displayed.
* @param {Boolean} ignoreLengths If falsy, branch lengths are used in the
* layout; otherwise, a uniform length of 1
* is used.
* @param {Boolean} normalize If true, then the tree will be scaled up to
* fill the bounds of width and height.
* @param {Function} lengthGetter Is a function that takes a single argument
* that corresponds to the index of a node in
* tree. Returns the length of the node at that
* index. Defaults to 'normal' method.
* @return {Object} Object with the following properties:
* -xCoords
* -yCoords
Expand All @@ -587,15 +719,18 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
tree,
width,
height,
ignoreLengths,
normalize = true
normalize = true,
lengthGetter = null
) {
var da = (2 * Math.PI) / tree.numleaves();
var x1Arr = new Array(tree.size + 1);
var x2Arr = new Array(tree.size + 1).fill(0);
var y1Arr = new Array(tree.size + 1);
var y2Arr = new Array(tree.size + 1).fill(0);
var aArr = new Array(tree.size + 1);
if (lengthGetter === null) {
lengthGetter = getLengthMethod("normal", tree);
}

var n = tree.postorderselect(tree.size);
var x1, y1, a;
Expand Down Expand Up @@ -628,7 +763,7 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
a += (tree.getNumTips(node) * da) / 2;

n = tree.postorderselect(node);
var nodeLen = ignoreLengths ? 1 : tree.length(n);
var nodeLen = lengthGetter(n);
x2 = x1 + nodeLen * Math.sin(a);
y2 = y1 + nodeLen * Math.cos(a);
x1Arr[node] = x1;
Expand Down Expand Up @@ -664,7 +799,9 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
}

return {
getLengthMethod: getLengthMethod,
getPostOrderNodes: getPostOrderNodes,
getUltrametricLengths: getUltrametricLengths,
computeScaleFactor: computeScaleFactor,
rectangularLayout: rectangularLayout,
circularLayout: circularLayout,
Expand Down
Loading

0 comments on commit 9235709

Please sign in to comment.