diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 60b2f5a08..794e5c1fe 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -275,6 +275,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" @@ -357,14 +363,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++) { @@ -384,8 +404,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 @@ -409,7 +430,8 @@ define([ this._tree, 4020, 4020, - this.ignoreLengths + undefined, + lengthGetter ); for (i = 1; i <= this._tree.size; i++) { // remove old layout information diff --git a/empress/support_files/js/layouts-util.js b/empress/support_files/js/layouts-util.js index ee2bd370e..efe767a04 100644 --- a/empress/support_files/js/layouts-util.js +++ b/empress/support_files/js/layouts-util.js @@ -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. * @@ -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 @@ -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; @@ -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; @@ -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]; @@ -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) @@ -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); @@ -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. @@ -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; } @@ -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 @@ -587,8 +719,8 @@ 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); @@ -596,6 +728,9 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) { 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; @@ -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; @@ -664,7 +799,9 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) { } return { + getLengthMethod: getLengthMethod, getPostOrderNodes: getPostOrderNodes, + getUltrametricLengths: getUltrametricLengths, computeScaleFactor: computeScaleFactor, rectangularLayout: rectangularLayout, circularLayout: circularLayout, diff --git a/empress/support_files/js/side-panel-handler.js b/empress/support_files/js/side-panel-handler.js index 24f9f35e4..c2d582ac7 100644 --- a/empress/support_files/js/side-panel-handler.js +++ b/empress/support_files/js/side-panel-handler.js @@ -83,16 +83,70 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { this.layoutMethodContainer = document.getElementById( "layout-method-container" ); - this.ignoreLengthsChk = document.getElementById("ignore-lengths-chk"); this.leafSortingContainer = document.getElementById( "leaf-sorting-container" ); this.leafSortingSel = document.getElementById("leaf-sorting-select"); this.leafSortingDesc = document.getElementById("leaf-sorting-desc"); - this.ignoreLengthsChk.onclick = function () { - empress.ignoreLengths = this.checked; - empress.reLayout(); - }; + + // Initialize the callbacks for selecting the branch method + var branchesMethodRadio = document.getElementsByName("branches-radio"); + var warnBranchMethods = ["ignore", "ultrametric"]; + this.branchLengthWarningContainer = document.getElementById( + "branch-length-warning" + ); + this.branchLengthWarningContainer.classList.add("hidden"); + + // For each branch method, we want to use the value of the radio button + // to set the branch method for Empress. + // checkBranchMethod() is a utility function that returns a function with no + // arguments (it works thanks to a closure) which can be set as a callback + // function for when a new branch method is selected. + function checkBranchMethod(option, empress, allOptions) { + function innerCheck() { + if (option.checked) { + // since these are coded in the interface as the empress options, + // they can be plugged directly into empress.branchMethod, but they + // theoretically could be remapped here + var value = option.value; + empress.branchMethod = value; + empress.reLayout(); + // some methods require a warning that branch lengths are being modified + if (warnBranchMethods.includes(value)) { + scope.branchLengthWarningContainer.classList.remove( + "hidden" + ); + } else { + scope.branchLengthWarningContainer.classList.add( + "hidden" + ); + } + } + // we want to make sure empress knows whether or not to ignore lengths + // at the moment; this is most critical for empress._collapseClade, + // since the lengths for the layout methods will be determined + // by setting empress.branchMethod. Note that we don't pass information + // about whether or not the tree is ultrametric; this is ok for now, + // because lengths are just used to determine the "depth" of collapsed + // clades, and by definition all tips in an ultrametric tree end at the + // same "length" from the root of the tree. However, we should fix + // this in the future; see https://github.com/biocore/empress/issues/448. + empress.ignoreLengths = allOptions.ignore.checked; + } + return innerCheck; + } + // branchOptions maps branch method names (e.g. "ultrametric") to the + // DOM object corresponding to the actual radio select. Objects in JS + // are mutable, so branchOptions is updated as we go through this loop + // (and this updated version of branchOptions should be available to + // every callback function created) + var branchOptions = {}; + for (var i = 0; i < branchesMethodRadio.length; i++) { + var option = branchesMethodRadio[i]; + branchOptions[option.value] = option; + option.onclick = checkBranchMethod(option, empress, branchOptions); + } + this.leafSortingSel.onchange = function () { empress.leafSorting = this.value; empress.reLayout(); diff --git a/empress/support_files/templates/side-panel.html b/empress/support_files/templates/side-panel.html index f77d3fd8c..c2a65a899 100644 --- a/empress/support_files/templates/side-panel.html +++ b/empress/support_files/templates/side-panel.html @@ -131,18 +131,49 @@

- Layout options -

-

- - -

-

- If checked, branch lengths are ignored when drawing the tree. Note that the - root node is never drawn with any length, regardless of if this - is checked or not. + Branch lengths

+
+

+ Warning: the "Ignore branch lengths" and "Make ultrametric" options display + modified branch lengths that can be useful for aesthetics. The resulting + trees should not be interpreted with respect to their branch lengths. +

+
+
+

+ + +

+

+ The branch lengths shown correspond to the branch lengths in the original tree. +

+

+ + +

+

+ If checked, all branches are drawn with the same length. Note that the + root node is never drawn with any length, regardless of if this + is checked or not. +

+

+ + +

+

+ If checked, branch lengths will be adjusted so that the root-to-tip distance + is the same for all tips. +

+
+
+

+ Clade sorting +