From cbf4060bdf0ca205e14fba1e1cb127e249767397 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Mon, 1 Mar 2021 11:25:47 -0800 Subject: [PATCH 01/63] bp shear --- empress/support_files/js/bp-tree.js | 63 +++++++++++- tests/test-bp-tree.js | 150 ++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+), 2 deletions(-) diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index 7ca8a17fc..280231069 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -65,7 +65,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { /** * @type {Array} * @private - * stores the name of each node in preorder. If names are not provided + * stores the name of each node in postorder. If names are not provided * then the names will be set to null by default. * Note: if memory becomes an issue this could be converted into a * Uint16Array @@ -85,7 +85,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { /** * @type {Array} * @private - * Stores the length of the nodes in preorder. If lengths are not + * Stores the length of the nodes in postorder. If lengths are not * provided then lengths will be set to null. */ this.lengths_ = lengths ? lengths : null; @@ -971,5 +971,64 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { return this._nameToNodes[name]; }; + /** + * tips - Set + */ + BPTree.prototype.shear = function(keepTips) { + // closure + var scope = this; + + // create new names and lengths array + var names = [null]; + var lengths = [null]; + + // create new bit array + var mask = []; + + // function to that will set open/close bits for a node + var set_bits = (node) => { + mask[node] = 1; + mask[scope.close(node)] = 0; + } + + // set root open/close bits + set_bits(this.root()); + + // iterate over bp tree in post order and add all tips that are in + // keepTips plus their ancestors + var i; + for (i = 1; i <= this.size; i++) { + var node = this.postorderselect(i); + var name = this.name(node); + if (this.isleaf(node) && keepTips.has(name)) { + // set open/close bits for tip + set_bits(node); + + // set open/close bits for tips ancestors + var parent = this.parent(node); + while(parent !== this.root() || mask[parent] !== 1) { + set_bits(parent); + parent = this.parent(parent); + } + } + } + + var newBitArray = []; + for (i = 0; i < mask.length; i++) { + if (mask[i] !== undefined) { + newBitArray.push(mask[i]); + } + + // get name and length of node + // Note: names and lengths of nodes are stored in postorder + + if (mask[i] === 0) { + names.push(this.name(i)); + lengths.push(this.length(i)); + } + } + return new BPTree(newBitArray, names, lengths, null); + } + return BPTree; }); diff --git a/tests/test-bp-tree.js b/tests/test-bp-tree.js index 0bfb8282c..0a84679ed 100644 --- a/tests/test-bp-tree.js +++ b/tests/test-bp-tree.js @@ -877,5 +877,155 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { "Error thrown when no length info given" ); }); + + test("Test shear", function() { + // test modified from https://github.com/wasade/improved-octo-waddle/blob/master/bp/tests/test_bp.py#L228 + // newick represenation + // ((3,4,(6)5)2, 7, ((10, 11)9)8)r; + var preShearArr = [ + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0 + ]; + var preShearNames = [ + null, + "3", + "4", + "6", + "5", + "2", + "7", + "10", + "11", + "9", + "8", + "r" + ]; + var preShearLenghts = [ + null, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ]; + var preShearBPTree = new BPTree( + preShearArr, + preShearNames, + preShearLenghts, + null + ) + + var keep = new Set(["4", "6", "7", "10", "11"]); + var result = preShearBPTree.shear(keep); + deepEqual(result.b_, [ + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0 + ]); + deepEqual(result.names_, [ + null, + "4", + "6", + "5", + "2", + "7", + "10", + "11", + "9", + "8", + "r" + ]); + deepEqual(result.lengths_, [ + null, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ]); + + keep = new Set(["7", "10", "11"]); + result = preShearBPTree.shear(keep); + deepEqual(result.b_, [ + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0 + ]); + deepEqual(result.names_, [ + null, + "7", + "10", + "11", + "9", + "8", + "r" + ]); + deepEqual(result.lengths_, [ + null, + 6, + 7, + 8, + 9, + 10, + 11 + ]); + }); }); }); From 227ec295e4e00f80199625ad2cf6d86b7b2d905f Mon Sep 17 00:00:00 2001 From: kcantrel Date: Mon, 1 Mar 2021 15:26:43 -0800 Subject: [PATCH 02/63] style fix --- empress/support_files/js/bp-tree.js | 8 +-- tests/test-bp-tree.js | 76 +++++------------------------ 2 files changed, 15 insertions(+), 69 deletions(-) diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index 280231069..27265efec 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -974,7 +974,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { /** * tips - Set */ - BPTree.prototype.shear = function(keepTips) { + BPTree.prototype.shear = function (keepTips) { // closure var scope = this; @@ -989,7 +989,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { var set_bits = (node) => { mask[node] = 1; mask[scope.close(node)] = 0; - } + }; // set root open/close bits set_bits(this.root()); @@ -1006,7 +1006,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { // set open/close bits for tips ancestors var parent = this.parent(node); - while(parent !== this.root() || mask[parent] !== 1) { + while (parent !== this.root() || mask[parent] !== 1) { set_bits(parent); parent = this.parent(parent); } @@ -1028,7 +1028,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { } } return new BPTree(newBitArray, names, lengths, null); - } + }; return BPTree; }); diff --git a/tests/test-bp-tree.js b/tests/test-bp-tree.js index 0a84679ed..a327ca04a 100644 --- a/tests/test-bp-tree.js +++ b/tests/test-bp-tree.js @@ -878,7 +878,7 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { ); }); - test("Test shear", function() { + test("Test shear", function () { // test modified from https://github.com/wasade/improved-octo-waddle/blob/master/bp/tests/test_bp.py#L228 // newick represenation // ((3,4,(6)5)2, 7, ((10, 11)9)8)r; @@ -904,7 +904,7 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { 0, 0, 0, - 0 + 0, ]; var preShearNames = [ null, @@ -918,28 +918,15 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { "11", "9", "8", - "r" - ]; - var preShearLenghts = [ - null, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11 + "r", ]; + var preShearLenghts = [null, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; var preShearBPTree = new BPTree( preShearArr, preShearNames, preShearLenghts, null - ) + ); var keep = new Set(["4", "6", "7", "10", "11"]); var result = preShearBPTree.shear(keep); @@ -963,7 +950,7 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { 0, 0, 0, - 0 + 0, ]); deepEqual(result.names_, [ null, @@ -976,56 +963,15 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { "11", "9", "8", - "r" - ]); - deepEqual(result.lengths_, [ - null, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11 + "r", ]); + deepEqual(result.lengths_, [null, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); keep = new Set(["7", "10", "11"]); result = preShearBPTree.shear(keep); - deepEqual(result.b_, [ - 1, - 1, - 0, - 1, - 1, - 1, - 0, - 1, - 0, - 0, - 0, - 0 - ]); - deepEqual(result.names_, [ - null, - "7", - "10", - "11", - "9", - "8", - "r" - ]); - deepEqual(result.lengths_, [ - null, - 6, - 7, - 8, - 9, - 10, - 11 - ]); + deepEqual(result.b_, [1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0]); + deepEqual(result.names_, [null, "7", "10", "11", "9", "8", "r"]); + deepEqual(result.lengths_, [null, 6, 7, 8, 9, 10, 11]); }); }); }); From 66c2e56ce876afbe5630163fc219781fe4804915 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Fri, 5 Mar 2021 16:45:09 -0800 Subject: [PATCH 03/63] checkpoint --- empress/support_files/js/bp-tree.js | 8 +- empress/support_files/js/empress.js | 220 ++++++++++-------- empress/support_files/js/tree-handler.js | 183 +++++++++++++++ .../templates/empress-template.html | 1 + 4 files changed, 320 insertions(+), 92 deletions(-) create mode 100644 empress/support_files/js/tree-handler.js diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index 27265efec..f0c0c4f1e 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -1014,6 +1014,8 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { } var newBitArray = []; + var newIndxToOld = {}; + var postorderPos = 1; for (i = 0; i < mask.length; i++) { if (mask[i] !== undefined) { newBitArray.push(mask[i]); @@ -1025,9 +1027,13 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { if (mask[i] === 0) { names.push(this.name(i)); lengths.push(this.length(i)); + newIndxToOld[postorderPos++] = this.postorder(i); } } - return new BPTree(newBitArray, names, lengths, null); + return { + newIndxToOld: newIndxToOld, + tree: new BPTree(newBitArray, names, lengths, null), + } }; return BPTree; diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 976c3faa4..62759218f 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -11,6 +11,7 @@ define([ "chroma", "LayoutsUtil", "ExportUtil", + "TreeHandler" ], function ( _, Camera, @@ -23,7 +24,8 @@ define([ util, chroma, LayoutsUtil, - ExportUtil + ExportUtil, + TreeHandler ) { /** * @class EmpressTree @@ -87,6 +89,7 @@ define([ * @private */ this._tree = tree; + this._temp = new TreeHandler(this._tree); /** * Used to index into _treeData @@ -375,92 +378,94 @@ define([ * Also updates this._maxDisplacement. */ Empress.prototype.getLayoutInfo = function () { - var data, i; - // set up length getter - var branchMethod = this.branchMethod; - var checkLengthsChange = LayoutsUtil.shouldCheckBranchLengthsChanged( - branchMethod - ); - var lengthGetter = LayoutsUtil.getLengthMethod( - branchMethod, - this._tree - ); - - // Rectangular - if (this._currentLayout === "Rectangular") { - data = LayoutsUtil.rectangularLayout( - this._tree, - 4020, - 4020, - // 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, - checkLengthsChange - ); - this._yrscf = data.yScalingFactor; - for (i = 1; i <= this._tree.size; i++) { - // remove old layout information - this._treeData[i].length = this._numOfNonLayoutParams; - - // store new layout information - this._treeData[i][this._tdToInd.xr] = data.xCoord[i]; - this._treeData[i][this._tdToInd.yr] = data.yCoord[i]; - this._treeData[i][this._tdToInd.highestchildyr] = - data.highestChildYr[i]; - this._treeData[i][this._tdToInd.lowestchildyr] = - data.lowestChildYr[i]; - } - } else if (this._currentLayout === "Circular") { - data = LayoutsUtil.circularLayout( - this._tree, - 4020, - 4020, - this.leafSorting, - undefined, - lengthGetter, - checkLengthsChange - ); - for (i = 1; i <= this._tree.size; i++) { - // remove old layout information - this._treeData[i].length = this._numOfNonLayoutParams; - - // store new layout information - this._treeData[i][this._tdToInd.xc0] = data.x0[i]; - this._treeData[i][this._tdToInd.yc0] = data.y0[i]; - this._treeData[i][this._tdToInd.xc1] = data.x1[i]; - this._treeData[i][this._tdToInd.yc1] = data.y1[i]; - this._treeData[i][this._tdToInd.angle] = data.angle[i]; - this._treeData[i][this._tdToInd.arcx0] = data.arcx0[i]; - this._treeData[i][this._tdToInd.arcy0] = data.arcy0[i]; - this._treeData[i][this._tdToInd.arcstartangle] = - data.arcStartAngle[i]; - this._treeData[i][this._tdToInd.arcendangle] = - data.arcEndAngle[i]; - } - } else { - data = LayoutsUtil.unrootedLayout( - this._tree, - 4020, - 4020, - undefined, - lengthGetter, - checkLengthsChange - ); - for (i = 1; i <= this._tree.size; i++) { - // remove old layout information - this._treeData[i].length = this._numOfNonLayoutParams; - - // store new layout information - this._treeData[i][this._tdToInd.x2] = data.xCoord[i]; - this._treeData[i][this._tdToInd.y2] = data.yCoord[i]; - } - } + // var data, i; + // // set up length getter + // var branchMethod = this.branchMethod; + // var checkLengthsChange = LayoutsUtil.shouldCheckBranchLengthsChanged( + // branchMethod + // ); + // var lengthGetter = LayoutsUtil.getLengthMethod( + // branchMethod, + // this._tree + // ); + + // // Rectangular + // if (this._currentLayout === "Rectangular") { + // data = LayoutsUtil.rectangularLayout( + // this._tree, + // 4020, + // 4020, + // // 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, + // checkLengthsChange + // ); + // this._yrscf = data.yScalingFactor; + // for (i = 1; i <= this._tree.size; i++) { + // // remove old layout information + // this._treeData[i].length = this._numOfNonLayoutParams; + + // // store new layout information + // this._treeData[i][this._tdToInd.xr] = data.xCoord[i]; + // this._treeData[i][this._tdToInd.yr] = data.yCoord[i]; + // this._treeData[i][this._tdToInd.highestchildyr] = + // data.highestChildYr[i]; + // this._treeData[i][this._tdToInd.lowestchildyr] = + // data.lowestChildYr[i]; + // } + // } else if (this._currentLayout === "Circular") { + // data = LayoutsUtil.circularLayout( + // this._tree, + // 4020, + // 4020, + // this.leafSorting, + // undefined, + // lengthGetter, + // checkLengthsChange + // ); + // for (i = 1; i <= this._tree.size; i++) { + // // remove old layout information + // this._treeData[i].length = this._numOfNonLayoutParams; + + // // store new layout information + // this._treeData[i][this._tdToInd.xc0] = data.x0[i]; + // this._treeData[i][this._tdToInd.yc0] = data.y0[i]; + // this._treeData[i][this._tdToInd.xc1] = data.x1[i]; + // this._treeData[i][this._tdToInd.yc1] = data.y1[i]; + // this._treeData[i][this._tdToInd.angle] = data.angle[i]; + // this._treeData[i][this._tdToInd.arcx0] = data.arcx0[i]; + // this._treeData[i][this._tdToInd.arcy0] = data.arcy0[i]; + // this._treeData[i][this._tdToInd.arcstartangle] = + // data.arcStartAngle[i]; + // this._treeData[i][this._tdToInd.arcendangle] = + // data.arcEndAngle[i]; + // } + // } else { + // data = LayoutsUtil.unrootedLayout( + // this._tree, + // 4020, + // 4020, + // undefined, + // lengthGetter, + // checkLengthsChange + // ); + // for (i = 1; i <= this._tree.size; i++) { + // // remove old layout information + // this._treeData[i].length = this._numOfNonLayoutParams; + + // // store new layout information + // this._treeData[i][this._tdToInd.x2] = data.xCoord[i]; + // this._treeData[i][this._tdToInd.y2] = data.yCoord[i]; + // } + // } + + this._temp.assignCoords(this._treeData) this._drawer.loadTreeCoordsBuff(this.getTreeCoords()); this._computeMaxDisplacement(); }; @@ -469,6 +474,28 @@ define([ * Initializes WebGL and then draws the tree */ Empress.prototype.initialize = function () { + // this._currentLayout = "Circular" + var fmInfo = this.getUniqueFeatureMetadataInfo("Level 2", "tip"); + var sortedUniqueValues = fmInfo.sortedUniqueValues; + var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; + // convert observation IDs to _treeData keys. Notably, this includes + // converting the values of uniqueValueToFeatures from Arrays to Sets. + + var obs = {}; + _.each(sortedUniqueValues, function (uniqueVal, i) { + uniqueVal = sortedUniqueValues[i]; + obs[uniqueVal] = new Set(uniqueValueToFeatures[uniqueVal]); + }); + + var obs = [...obs["p__Firmicutes"]]; + var test = []; + for (var i = 0; i < obs.length; i++) { + var node = this._tree.postorderselect(obs[i]); + test.push(this._tree.name(node)) + } + this._temp.shear(new Set(test)); + + this._drawer.initialize(); this._events.setMouseEvents(); var nodeNames = this._tree.getAllNames(); @@ -1449,6 +1476,9 @@ define([ return; } for (var node = 1; node < this._tree.size; node++) { + if (!this.getNodeInfo(node, "visible")) { + continue; + } if (this._tree.isleaf(this._tree.postorderselect(node))) { maxD = this[compFunc](node, maxD); } @@ -2311,8 +2341,7 @@ define([ _.each(sortedUniqueValues, function (uniqueVal, i) { uniqueVal = sortedUniqueValues[i]; obs[uniqueVal] = new Set(uniqueValueToFeatures[uniqueVal]); - }); - + }); // assign colors to unique values var colorer = new Colorer( color, @@ -2376,7 +2405,8 @@ define([ if (!ignoreAbsentTips) { // find "non-represented" tips // Note: the following uses postorder traversal - for (i = 1; i < tree.size; i++) { + // for (i = 1; i < tree.size; i++) { + for (i of this._temp.postorder()) { if (tree.isleaf(tree.postorderselect(i))) { var represented = false; for (j = 0; j < categories.length; j++) { @@ -2396,7 +2426,9 @@ define([ // root (at index tree.size) in this loop, we iterate over all its // descendants; so in the event that all leaves are unique, // the root can still get assigned to a group. - for (i = 1; i < tree.size; i++) { + // for (i = 1; i < tree.size; i++) { + for (i of this._temp.postorder()) { + var node = i; var parent = tree.postorder(tree.parent(tree.postorderselect(i))); @@ -2446,6 +2478,7 @@ define([ for (var j = 0; j < keys.length; j++) { var node = keys[j]; + if (node === undefined) continue; this.setNodeInfo(node, "color", cm[category]); this.setNodeInfo(node, "isColored", true); } @@ -2568,6 +2601,7 @@ define([ if (this._layoutToCoordSuffix.hasOwnProperty(newLayout)) { // get new layout this._currentLayout = newLayout; + this._temp._currentLayout = newLayout; this.reLayout(); // recenter viewing window // NOTE: this function calls drawTree(), which is redundant @@ -2677,6 +2711,10 @@ define([ y = 0, zoomAmount = 0; for (var node = 1; node <= this._tree.size; node++) { + if (!this.getNodeInfo(node, "visible")) { + continue; + } + // node = this._treeData[node]; x += this.getX(node); y += this.getY(node); diff --git a/empress/support_files/js/tree-handler.js b/empress/support_files/js/tree-handler.js new file mode 100644 index 000000000..a080230bb --- /dev/null +++ b/empress/support_files/js/tree-handler.js @@ -0,0 +1,183 @@ +define(["LayoutsUtil"], function (LayoutsUtil) { + function TreeHandler(tree) { + this._tree = tree; + this._originalTreeSize = this._tree.size; + this._oldTree = tree; + this._newIndxToOld = {}; + this._currentLayout = "Unrooted"; + + /** + * Used to index into _treeData + * @type {Object} + * @private + */ + this._tdToInd = { + // all nodes (non-layout parameters) + color: 0, + isColored: 1, + visible: 2, + // unrooted layout + x2: 3, + y2: 4, + // rectangular layout + xr: 3, + yr: 4, + highestchildyr: 5, + lowestchildyr: 6, + // circular layout + xc0: 3, + yc0: 4, + xc1: 5, + yc1: 6, + angle: 7, + arcx0: 8, + arcy0: 9, + arcstartangle: 10, + arcendangle: 11, + }; + } + + TreeHandler.prototype.shear = function(tips, treeData) { + var result = this._tree.shear(tips); + this._oldTree = this._tree; + this._tree = result.tree; + this._newIndxToOld = result.newIndxToOld; + } + + TreeHandler.prototype.assignCoords = function(treeData) { + var data, i, j = 1; + + for (i = 1; i <= this._originalTreeSize; i++) { + treeData[i][this._tdToInd.visible] = false; + } + console.log(this._currentLayout) + // Rectangular + if (this._currentLayout === "Rectangular") { + data = LayoutsUtil.rectangularLayout( + this._tree, + 4020, + 4020, + // 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. + "descending", + undefined + // lengthGetter, + // checkLengthsChange + ); + // this._yrscf = data.yScalingFactor; + for (i of this.postorder()) { + // remove old layout information + treeData[i].length = 3; + + // store new layout information + treeData[i][this._tdToInd.xr] = data.xCoord[j]; + treeData[i][this._tdToInd.yr] = data.yCoord[j]; + treeData[i][this._tdToInd.highestchildyr] = + data.highestChildYr[j]; + treeData[i][this._tdToInd.lowestchildyr] = + data.lowestChildYr[j]; + treeData[i][this._tdToInd.visible] = true; + j++; + } + } else if (this._currentLayout === "Circular") { + data = LayoutsUtil.circularLayout( + this._tree, + 4020, + 4020, + "descending", + undefined, + // lengthGetter, + // checkLengthsChange + ); + for (i of this.postorder()) { + // remove old layout information + treeData[i].length = 3; + + // store new layout information + treeData[i][this._tdToInd.xc0] = data.x0[j]; + treeData[i][this._tdToInd.yc0] = data.y0[j]; + treeData[i][this._tdToInd.xc1] = data.x1[j]; + treeData[i][this._tdToInd.yc1] = data.y1[j]; + treeData[i][this._tdToInd.angle] = data.angle[j]; + treeData[i][this._tdToInd.arcx0] = data.arcx0[j]; + treeData[i][this._tdToInd.arcy0] = data.arcy0[j]; + treeData[i][this._tdToInd.arcstartangle] = + data.arcStartAngle[j]; + treeData[i][this._tdToInd.arcendangle] = + data.arcEndAngle[j]; + treeData[i][this._tdToInd.visible] = true; + j++; + } + } else { + data = LayoutsUtil.unrootedLayout( + this._tree, + 4020, + 4020, + undefined, + // lengthGetter, + // checkLengthsChange + ); + for (i of this.postorder()) { + // remove old layout information + treeData[i].length = 3; + + // store new layout information + treeData[i][this._tdToInd.x2] = data.xCoord[j]; + treeData[i][this._tdToInd.y2] = data.yCoord[j]; + treeData[i][this._tdToInd.visible] = true; + j++; + } + } + console.log(j, treeData) + + + // var j = 1; + // data = LayoutsUtil.circularLayout( + // this._tree, + // 4020, + // 4020, + // "ascending", + // undefined, + // ); + // for (var i of this.postorder()) { + // // remove old layout information + // treeData[i].length = 3; + + // // store new layout information + // treeData[i][this._tdToInd.xc0] = data.x0[j]; + // treeData[i][this._tdToInd.yc0] = data.y0[j]; + // treeData[i][this._tdToInd.xc1] = data.x1[j]; + // treeData[i][this._tdToInd.yc1] = data.y1[j]; + // treeData[i][this._tdToInd.angle] = data.angle[j]; + // treeData[i][this._tdToInd.arcx0] = data.arcx0[j]; + // treeData[i][this._tdToInd.arcy0] = data.arcy0[j]; + // treeData[i][this._tdToInd.arcstartangle] = + // data.arcStartAngle[j]; + // treeData[i][this._tdToInd.arcendangle] = + // data.arcEndAngle[j]; + // treeData[i][this._tdToInd.visible] = true; + // j++; + // } + } + + TreeHandler.prototype.postorder = function*(includeRoot=false) { + var nodes = [], + i; + if (Object.keys(this._newIndxToOld).length === 0) { + for (i = 1; i <= this._tree.size; i++) { + nodes.push(i); + } + } else { + for (i in this._newIndxToOld) { + nodes.push(this._newIndxToOld[i]) + } + } + yield* nodes; + }; + + return TreeHandler; +}); \ No newline at end of file diff --git a/empress/support_files/templates/empress-template.html b/empress/support_files/templates/empress-template.html index 9aafaa5b8..226a1a4dd 100644 --- a/empress/support_files/templates/empress-template.html +++ b/empress/support_files/templates/empress-template.html @@ -115,6 +115,7 @@ 'util' : './js/util', 'LayoutsUtil': './js/layouts-util', 'ExportUtil': './js/export-util', + 'TreeHandler': './js/tree-handler' } }); From 1183aa7f9993aa9c07da3fca636b91ca303934ee Mon Sep 17 00:00:00 2001 From: kcantrel Date: Tue, 9 Mar 2021 12:36:27 -0800 Subject: [PATCH 04/63] checkpoint --- empress/support_files/js/empress.js | 4 +++- empress/support_files/js/tree-handler.js | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 62759218f..228f36eeb 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -493,7 +493,8 @@ define([ var node = this._tree.postorderselect(obs[i]); test.push(this._tree.name(node)) } - this._temp.shear(new Set(test)); + // this._temp.shear(new Set(test)); + // this._temp.shear(new Set()); this._drawer.initialize(); @@ -2159,6 +2160,7 @@ define([ var rgb = Colorer.hex2RGB(group); for (var i = 0; i < obs.length; i++) { + console.log(obs[i]) this.setNodeInfo(obs[i], "color", rgb); } } diff --git a/empress/support_files/js/tree-handler.js b/empress/support_files/js/tree-handler.js index a080230bb..3c3ce7b04 100644 --- a/empress/support_files/js/tree-handler.js +++ b/empress/support_files/js/tree-handler.js @@ -35,6 +35,12 @@ define(["LayoutsUtil"], function (LayoutsUtil) { arcstartangle: 10, arcendangle: 11, }; + + /** + * @type {Array} + * The default color of the tree + */ + // this.DEFAULT_COLOR = Colorer.rgbToFloat([64, 64, 64]); } TreeHandler.prototype.shear = function(tips, treeData) { @@ -179,5 +185,7 @@ define(["LayoutsUtil"], function (LayoutsUtil) { yield* nodes; }; + TreeHandler + return TreeHandler; }); \ No newline at end of file From eafbea3cdbee8986b6263ec564ac4c44ff691d09 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Wed, 10 Mar 2021 13:14:24 -0800 Subject: [PATCH 05/63] Finished TreeController --- empress/support_files/js/bp-tree.js | 17 +- empress/support_files/js/empress.js | 245 +++++------ empress/support_files/js/tree-controller.js | 404 ++++++++++++++++++ empress/support_files/js/tree-handler.js | 191 --------- .../templates/empress-template.html | 2 +- tests/index.html | 60 +-- tests/test-bp-tree.js | 6 +- tests/test-tree-controller.js | 41 ++ 8 files changed, 606 insertions(+), 360 deletions(-) create mode 100644 empress/support_files/js/tree-controller.js delete mode 100644 empress/support_files/js/tree-handler.js create mode 100644 tests/test-tree-controller.js diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index f0c0c4f1e..571db3cf1 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -643,7 +643,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { */ BPTree.prototype.inOrderNodes = function () { if (this._inorder !== null) { - return this._inorder; + return _.clone(this._inorder); } // the root node of the tree @@ -658,7 +658,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { // append children to stack nodeStack = nodeStack.concat(this.getChildren(curNode)); } - return this._inorder; + return _.clone(this._inorder); }; /** @@ -958,7 +958,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { */ BPTree.prototype.getNodesWithName = function (name) { if (name in this._nameToNodes) { - return this._nameToNodes[name]; + return _.clone(this._nameToNodes[name]); } this._nameToNodes[name] = []; @@ -968,7 +968,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { } } - return this._nameToNodes[name]; + return _.clone(this._nameToNodes[name]); }; /** @@ -1014,7 +1014,8 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { } var newBitArray = []; - var newIndxToOld = {}; + var newToOld = {}; + var oldToNew = {} var postorderPos = 1; for (i = 0; i < mask.length; i++) { if (mask[i] !== undefined) { @@ -1027,11 +1028,13 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { if (mask[i] === 0) { names.push(this.name(i)); lengths.push(this.length(i)); - newIndxToOld[postorderPos++] = this.postorder(i); + newToOld[postorderPos] = this.postorder(i); + oldToNew[this.postorder(i)] = postorderPos++; } } return { - newIndxToOld: newIndxToOld, + newToOld: newToOld, + oldToNew: oldToNew, tree: new BPTree(newBitArray, names, lengths, null), } }; diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 228f36eeb..5254dead8 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -11,7 +11,7 @@ define([ "chroma", "LayoutsUtil", "ExportUtil", - "TreeHandler" + "TreeController", ], function ( _, Camera, @@ -25,7 +25,7 @@ define([ chroma, LayoutsUtil, ExportUtil, - TreeHandler + TreeController, ) { /** * @class EmpressTree @@ -88,8 +88,7 @@ define([ * The phylogenetic balance parenthesis tree * @private */ - this._tree = tree; - this._temp = new TreeHandler(this._tree); + this._tree = new TreeController(tree); /** * Used to index into _treeData @@ -378,94 +377,91 @@ define([ * Also updates this._maxDisplacement. */ Empress.prototype.getLayoutInfo = function () { - // var data, i; - // // set up length getter - // var branchMethod = this.branchMethod; - // var checkLengthsChange = LayoutsUtil.shouldCheckBranchLengthsChanged( - // branchMethod - // ); - // var lengthGetter = LayoutsUtil.getLengthMethod( - // branchMethod, - // this._tree - // ); - - // // Rectangular - // if (this._currentLayout === "Rectangular") { - // data = LayoutsUtil.rectangularLayout( - // this._tree, - // 4020, - // 4020, - // // 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, - // checkLengthsChange - // ); - // this._yrscf = data.yScalingFactor; - // for (i = 1; i <= this._tree.size; i++) { - // // remove old layout information - // this._treeData[i].length = this._numOfNonLayoutParams; - - // // store new layout information - // this._treeData[i][this._tdToInd.xr] = data.xCoord[i]; - // this._treeData[i][this._tdToInd.yr] = data.yCoord[i]; - // this._treeData[i][this._tdToInd.highestchildyr] = - // data.highestChildYr[i]; - // this._treeData[i][this._tdToInd.lowestchildyr] = - // data.lowestChildYr[i]; - // } - // } else if (this._currentLayout === "Circular") { - // data = LayoutsUtil.circularLayout( - // this._tree, - // 4020, - // 4020, - // this.leafSorting, - // undefined, - // lengthGetter, - // checkLengthsChange - // ); - // for (i = 1; i <= this._tree.size; i++) { - // // remove old layout information - // this._treeData[i].length = this._numOfNonLayoutParams; - - // // store new layout information - // this._treeData[i][this._tdToInd.xc0] = data.x0[i]; - // this._treeData[i][this._tdToInd.yc0] = data.y0[i]; - // this._treeData[i][this._tdToInd.xc1] = data.x1[i]; - // this._treeData[i][this._tdToInd.yc1] = data.y1[i]; - // this._treeData[i][this._tdToInd.angle] = data.angle[i]; - // this._treeData[i][this._tdToInd.arcx0] = data.arcx0[i]; - // this._treeData[i][this._tdToInd.arcy0] = data.arcy0[i]; - // this._treeData[i][this._tdToInd.arcstartangle] = - // data.arcStartAngle[i]; - // this._treeData[i][this._tdToInd.arcendangle] = - // data.arcEndAngle[i]; - // } - // } else { - // data = LayoutsUtil.unrootedLayout( - // this._tree, - // 4020, - // 4020, - // undefined, - // lengthGetter, - // checkLengthsChange - // ); - // for (i = 1; i <= this._tree.size; i++) { - // // remove old layout information - // this._treeData[i].length = this._numOfNonLayoutParams; - - // // store new layout information - // this._treeData[i][this._tdToInd.x2] = data.xCoord[i]; - // this._treeData[i][this._tdToInd.y2] = data.yCoord[i]; - // } - // } - - this._temp.assignCoords(this._treeData) + var data, i, j = 1; + // set up length getter + var branchMethod = this.branchMethod; + var checkLengthsChange = LayoutsUtil.shouldCheckBranchLengthsChanged( + branchMethod + ); + var lengthGetter = LayoutsUtil.getLengthMethod( + branchMethod, + this._tree.model.currentTree + ); + // Rectangular + if (this._currentLayout === "Rectangular") { + data = LayoutsUtil.rectangularLayout( + this._tree.model.currentTree, + 4020, + 4020, + // 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, + checkLengthsChange + ); + this._yrscf = data.yScalingFactor; + for (i of this._tree.postorderTraversal(includeRoot=true)) { + // remove old layout information + this._treeData[i].length = this._numOfNonLayoutParams; + + // store new layout information + this._treeData[i][this._tdToInd.xr] = data.xCoord[j]; + this._treeData[i][this._tdToInd.yr] = data.yCoord[j]; + this._treeData[i][this._tdToInd.highestchildyr] = + data.highestChildYr[j]; + this._treeData[i][this._tdToInd.lowestchildyr] = + data.lowestChildYr[j++]; + } + } else if (this._currentLayout === "Circular") { + data = LayoutsUtil.circularLayout( + this._tree.model.currentTree, + 4020, + 4020, + this.leafSorting, + undefined, + lengthGetter, + checkLengthsChange + ); + for (i of this._tree.postorderTraversal(includeRoot=true)) { + // remove old layout information + this._treeData[i].length = this._numOfNonLayoutParams; + + // store new layout information + this._treeData[i][this._tdToInd.xc0] = data.x0[j]; + this._treeData[i][this._tdToInd.yc0] = data.y0[j]; + this._treeData[i][this._tdToInd.xc1] = data.x1[j]; + this._treeData[i][this._tdToInd.yc1] = data.y1[j]; + this._treeData[i][this._tdToInd.angle] = data.angle[j]; + this._treeData[i][this._tdToInd.arcx0] = data.arcx0[j]; + this._treeData[i][this._tdToInd.arcy0] = data.arcy0[j]; + this._treeData[i][this._tdToInd.arcstartangle] = + data.arcStartAngle[j]; + this._treeData[i][this._tdToInd.arcendangle] = + data.arcEndAngle[j++]; + } + } else { + data = LayoutsUtil.unrootedLayout( + this._tree.model.currentTree, + 4020, + 4020, + undefined, + lengthGetter, + checkLengthsChange + ); + for (i of this._tree.postorderTraversal(includeRoot=true)) { + // remove old layout information + this._treeData[i].length = this._numOfNonLayoutParams; + + // store new layout information + this._treeData[i][this._tdToInd.x2] = data.xCoord[j]; + this._treeData[i][this._tdToInd.y2] = data.yCoord[j++]; + } + } this._drawer.loadTreeCoordsBuff(this.getTreeCoords()); this._computeMaxDisplacement(); }; @@ -474,7 +470,6 @@ define([ * Initializes WebGL and then draws the tree */ Empress.prototype.initialize = function () { - // this._currentLayout = "Circular" var fmInfo = this.getUniqueFeatureMetadataInfo("Level 2", "tip"); var sortedUniqueValues = fmInfo.sortedUniqueValues; var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; @@ -486,15 +481,16 @@ define([ uniqueVal = sortedUniqueValues[i]; obs[uniqueVal] = new Set(uniqueValueToFeatures[uniqueVal]); }); - - var obs = [...obs["p__Firmicutes"]]; - var test = []; - for (var i = 0; i < obs.length; i++) { - var node = this._tree.postorderselect(obs[i]); - test.push(this._tree.name(node)) + console.log((obs["p__Bacteroidetes"])) + names = [] + for (var i of obs["p__Bacteroidetes"].values()) { + console.log(i) + if (this._tree.isleaf(this._tree.postorderselect(i))){ + names.push(this._tree.name(this._tree.postorderselect(i))) + } } - // this._temp.shear(new Set(test)); - // this._temp.shear(new Set()); + console.log(names) + this._tree.shear(new Set(names)) this._drawer.initialize(); @@ -623,7 +619,7 @@ define([ ); } // iterate through the tree in postorder, skip root - for (var node = 1; node < tree.size; node++) { + for (var node of this._tree.postorderTraversal()) { // name of current node // var node = this._treeData[node]; var parent = tree.postorder( @@ -726,6 +722,7 @@ define([ addPoint(this.getX(node), this.getY(node)); } } + console.log(coords) return new Float32Array(coords); }; @@ -752,7 +749,7 @@ define([ addPoint(); } // iterate through the tree in postorder, skip root - for (var node = 1; node < tree.size; node++) { + for (var node of this._tree.postorderTraversal()) { if (!this.getNodeInfo(node, "visible")) { continue; } @@ -919,7 +916,7 @@ define([ throw new Error("getNodeCoords() drawNodeCircles is out of range"); } - for (var node = 1; node <= tree.size; node++) { + for (var node of this._tree.postorderTraversal(includeRoot=true)) { if (!comp(node)) { continue; } @@ -1260,7 +1257,7 @@ define([ this._addThickVerticalLineCoords(coords, tree.size, lwScaled); } // iterate through the tree in postorder, skip root - for (var node = 1; node < this._tree.size; node++) { + for (var node of this._tree.postorderTraversal()) { // name of current node var parent = tree.postorder( tree.parent(tree.postorderselect(node)) @@ -1476,10 +1473,7 @@ define([ this._maxDisplacement = null; return; } - for (var node = 1; node < this._tree.size; node++) { - if (!this.getNodeInfo(node, "visible")) { - continue; - } + for (var node of this._tree.postorderTraversal()) { if (this._tree.isleaf(this._tree.postorderselect(node))) { maxD = this[compFunc](node, maxD); } @@ -1967,7 +1961,7 @@ define([ } else { halfAngleRange = Math.PI / this._tree.numleaves(); } - for (node = 1; node < this._tree.size; node++) { + for (node of this._tree.postorderTraversal()) { if (this._tree.isleaf(this._tree.postorderselect(node))) { var name = this.getNodeInfo(node, "name"); var fm; @@ -2100,7 +2094,7 @@ define([ // For the circular layout, how to speed this up is less clear -- I // suspect it should be possible using WebGL and some fancy // trigonometry somehow, but I'm not sure. - for (var node = 1; node < this._tree.size; node++) { + for (var node of this._tree.postorderTraversal()) { if (this._tree.isleaf(this._tree.postorderselect(node))) { if (this._currentLayout === "Rectangular") { var y = this.getY(node); @@ -2160,7 +2154,6 @@ define([ var rgb = Colorer.hex2RGB(group); for (var i = 0; i < obs.length; i++) { - console.log(obs[i]) this.setNodeInfo(obs[i], "color", rgb); } } @@ -2343,7 +2336,8 @@ define([ _.each(sortedUniqueValues, function (uniqueVal, i) { uniqueVal = sortedUniqueValues[i]; obs[uniqueVal] = new Set(uniqueValueToFeatures[uniqueVal]); - }); + }); + // assign colors to unique values var colorer = new Colorer( color, @@ -2407,8 +2401,7 @@ define([ if (!ignoreAbsentTips) { // find "non-represented" tips // Note: the following uses postorder traversal - // for (i = 1; i < tree.size; i++) { - for (i of this._temp.postorder()) { + for (i of this._tree.postorderTraversal()) { if (tree.isleaf(tree.postorderselect(i))) { var represented = false; for (j = 0; j < categories.length; j++) { @@ -2428,9 +2421,7 @@ define([ // root (at index tree.size) in this loop, we iterate over all its // descendants; so in the event that all leaves are unique, // the root can still get assigned to a group. - // for (i = 1; i < tree.size; i++) { - for (i of this._temp.postorder()) { - + for (i of this._tree.postorderTraversal()) { var node = i; var parent = tree.postorder(tree.parent(tree.postorderselect(i))); @@ -2480,7 +2471,6 @@ define([ for (var j = 0; j < keys.length; j++) { var node = keys[j]; - if (node === undefined) continue; this.setNodeInfo(node, "color", cm[category]); this.setNodeInfo(node, "isColored", true); } @@ -2603,7 +2593,6 @@ define([ if (this._layoutToCoordSuffix.hasOwnProperty(newLayout)) { // get new layout this._currentLayout = newLayout; - this._temp._currentLayout = newLayout; this.reLayout(); // recenter viewing window // NOTE: this function calls drawTree(), which is redundant @@ -2712,11 +2701,7 @@ define([ var x = 0, y = 0, zoomAmount = 0; - for (var node = 1; node <= this._tree.size; node++) { - if (!this.getNodeInfo(node, "visible")) { - continue; - } - + for (var node of this._tree.postorderTraversal(includeRoot=true)) { // node = this._treeData[node]; x += this.getX(node); y += this.getY(node); @@ -2807,7 +2792,7 @@ define([ this._collapsedClades = {}; // Note: currently collapseClades is the only method that set // the node visibility property. - for (var i = 1; i <= this._tree.size; i++) { + for (var i of this._tree.postorderTraversal(includeRoot=true)) { this.setNodeInfo(i, "visible", true); } @@ -2847,7 +2832,7 @@ define([ // was not called. Thus, this loop is used to guarantee that if an // internal node belongs to a group then all of its descendants belong // to the same group. - for (var i = 1; i <= this._tree.size; i++) { + for (var i of this._tree.postorderTraversal()) { var parent = this._tree.postorder( this._tree.parent(this._tree.postorderselect(i)) ); @@ -2863,10 +2848,8 @@ define([ // collaped. // Collapsing a clade will set the .visible property of members to // false and will then be skipped in the for loop. - var inorder = this._tree.inOrderNodes(); - for (var node in inorder) { - node = inorder[node]; - + // var inorder = this._tree.inOrderTraversal(); + for (var node of this._tree.inOrderTraversal()) { // dont collapse clade if (this._dontCollapse.has(node)) { continue; diff --git a/empress/support_files/js/tree-controller.js b/empress/support_files/js/tree-controller.js new file mode 100644 index 000000000..3cc88fad4 --- /dev/null +++ b/empress/support_files/js/tree-controller.js @@ -0,0 +1,404 @@ +define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { + function TreeModel(tree) { + this.currentTree = tree; + this.originalTree = tree; + this.curToOrig = {}; + this.origToCur = {}; + + // initialize + for (var i = 1; i <= this.currentTree.size; i++) { + this.origToCur[i] = i; + this.curToOrig[i] = i; + } + } + + TreeModel.prototype.shear = function(tips, keepNames=false) { + var result = this.originalTree.shear(tips, keepNames); + this.currentTree = result.tree; + this.curToOrig = result.newToOld; + this.origToCur = result.oldToNew; + console.log(this.curToOrig) + console.log(this.origToCur) + } + + TreeModel.prototype.unshear = function() { + this.currentTree = this.originalTree; + // initialize + for (var i = 1; i <= this.currentTree.size; i++) { + this.origToCur[i] = i; + this.curToOrig[i] = i; + } + } + + TreeModel.prototype.postorderTraversal = function*(includeRoot=false) { + var nodes = [], + i; + for (i = 1; i <= Object.keys(this.curToOrig).length; i++) { + nodes.push(this.curToOrig[i]) + } + if (!includeRoot) { + nodes.pop(); + } + + yield* nodes; + } + + function TreeController(tree) { + this.model = new TreeModel(tree); + this.size = this.model.originalTree.size; + } + + TreeController.prototype.indexInCurrentTree = function(i) { + var node = this.model.originalTree.postorder(i); + return this.model.origToCur.hasOwnProperty(node); + } + + /** + * tips - Set + */ + TreeController.prototype.shear = function(tips, keepNames=false) { + this.model.shear(tips, keepNames); + } + + + // add option for original + TreeController.prototype.postorderTraversal = function*(includeRoot=false) { + yield* this.model.postorderTraversal(includeRoot); + }; + + /** + * Returns an Object describing the minimum, maximum, and average of all + * non-root node lengths. + * + * @param{Boolean} original if true returns the length stats of the original + * tree else returns the length stats of the + * sheared tree. + * + * @return {Object} Contains three keys: "min", "max", and "avg", mapping + * to Numbers representing the minimum, maximum, and + * average non-root node length in the tree. + */ + TreeController.prototype.getLengthStats = function(origianl=false) { + if (origianl) { + return this.model.originalTree.getLengthStats(); + } else { + return this.model.currentTree.getLengthStats(); + } + } + + /** + * + * The name of the ith node in the ORIGINAL bp tree + * + * @param{Number} i Node i in the ORIGINAL bp tree + * + * @return{String} + */ + TreeController.prototype.name = function(i) { + // check for error + // if (!this.indexInCurrentTree(i)) { + // throw "Node index " + i + " is not in currentTree"; + // } + return this.model.originalTree.name(i); + } + + /** + * Returns an array of all node names in tree. + * + * @param{Boolean} original if true returns the length stats of the original + * tree else returns the length stats of the + * sheared tree. + * + */ + TreeController.prototype.getAllNames = function(original=false) { + if (original) { + return this.model.originalTree.getAllNames(); + } else { + return this.model.currentTree.getAllNames(); + } + }; + + /** + * + * The number of leaf nodes in tree + * + * @param{Boolean} original if true returns the length stats of the original + * tree else returns the length stats of the + * sheared tree. + * + * @return {Number} + */ + TreeController.prototype.numleaves = function(original=false) { + if (original) { + return this.model.originalTree.numleaves(); + } else { + return this.model.currentTree.numleaves(); + } + }; + + /** + * + * The length of the ith node in bp tree + * + * @param{Number} i Node i in bp tree + * + * @return{Number} + */ + TreeController.prototype.length = function (i) { + return this.model.originalTree.length(i); + }; + + /** + * The opening index of the smallest matching pair that contains the node + * represented by i in the ORIGINAL tree. + * + * @param{Number} i Current node + * i can be either an open or close parenthesis + * + * @return {Number} + */ + TreeController.prototype.parent = function (i) { + return this.model.originalTree.parent(i); + } + + /** + * returns the index of the opening index of the root node + * + * @return {Number} + */ + TreeController.prototype.root = function () { + return this.model.originalTree.root(); + } + + /** + * Return true if i represents a leaf node + * + * @return {Boolean} + */ + TreeController.prototype.isleaf = function (i) { + // check for err + return this.model.originalTree.isleaf(i); + }; + + TreeController.prototype._curToOrigNodeFunction = function(i, func) { + var curTree = this.model.currentTree; + var origTree = this.model.originalTree; + + // check for err + var node = curTree.postorderselect( + this.model.origToCur[origTree.postorder(i)] + ); + + var node = curTree.postorder(curTree[func](node)); + node = origTree.postorderselect( + this.model.curToOrig[node] + ); + return node; + } + + /** + * returns the opening index the first child of the node represented by i + * + * @param {Number} i Current node + * i can be either an open or close parenthesis + * + * @return {Number} return 0 if i is a leaf node + */ + TreeController.prototype.fchild = function (i) { + return this._curToOrigNodeFunction(i, "fchild"); + }; + + /** + * returns the opening index the last child of the node represented by i + * + * @param {Number} i Current node + * i can be either an open or close parenthesis + * @return {Number} return 0 if i is a leaf node + */ + TreeController.prototype.lchild = function (i) { + return this._curToOrigNodeFunction(i, "lchild") + }; + + /** + * returns the opening index of the next sibling of i + * + * @param {Number} i Current node. + * i can be either an open or close parenthesis + * + * @return {Number} + */ + TreeController.prototype.nsibling = function (i) { + return this._curToOrigNodeFunction(i, "nsibling"); + }; + + /** + * returns the opening index of the previous sibling of i + * + * @param {Number} i Current node. + * i can be either an open or close parenthesis + * + * @return {Number} returns 0 if no previous sibling + */ + TreeController.prototype.psibling = function (i) { + return this._curToOrigNodeFunction(i, "psibling"); + }; + + /** + * Finds postorder rank of node i + * + * @param {Number} i The node index to asses postorder rank + * + * @return {Number} The postorder rank of node i + */ + TreeController.prototype.postorder = function (i) { + // check error + return this.model.originalTree.postorder(i); + }; + + /** + * Find the index of the node with postorder k + * + * @param {Number} k The postorder to search for + * Note: k starts at 1 + * + * @return {Number} The index position of the node in the tree + */ + TreeController.prototype.postorderselect = function (k) { + // check error + return this.model.originalTree.postorderselect(k); + }; + + /** + * Finds preorder rank of node i + * + * @param {Number} i The node index to asses preorder rank + * + * @return {Number} The preorder rank of node i + */ + TreeController.prototype.preorder = function (i) { + // check error + return this.model.originalTree.preorder(i); + }; + + /** + * Find the index of the node with preorder k + * + * @param {Number} k The preorder to search for. + * Note: k starts at 1. + * + * @return {Number} The index position of the node in the tree + */ + TreeController.prototype.preorderselect = function (k) { + // check error + return this.model.originalTree.preorderselect(k); + }; + + /** + * Returns an array of nodes sorted by their inorder position. + * + * Note: empress uses a nodes postorder position as its key in _treeData + * so this method will use a nodes postorder position to represent + * it in the resulting array. + * This method will also cache the resulting array. + */ + TreeController.prototype.inOrderTraversal = function*(includeRoot=false) { + var inOrderNodes = this.model.currentTree.inOrderNodes(); + if (Object.keys(this.model.curToOrig).length !== 0) { + for (var i = 0; i < inOrderNodes.length; i++) { + inOrderNodes[i] = this.model.curToOrig[inOrderNodes[i]]; + } + } + if (!includeRoot) { + inOrderNodes.shift(); + } + console.log("inorder", inOrderNodes) + yield* inOrderNodes; + }; + + /** + * Finds the sum of lengths from start to end. + * + * Note: start must be a descendant of end. An error will be thrown if start + * is not a descendant of end. Also, this method does not take into + * account the length of end since that length would represent the + * length of end to its parent. + * + * @param {Number} start The postorder position of a node + * @param {Number} end The postorder position of a node + * @param {Boolean} ignoreLengths If truthy, treat all node lengths as 1; + * if falsy, actually consider node lengths + * + * @return {Number} the sum of length from start to end + */ + TreeController.prototype.getTotalLength = function (start, end, ignoreLengths) { + start = this.model.origToCur[start]; + end = this.model.origToCur[end]; + return this.model.currentTree.getTotalLength(start, end, ignoreLengths); + }; + + /** + * Retrieve the tips in the subtree of a given (internal) node key. + * + * @param {Number} nodeKey Key value of internal node. + * @return {Array} tips Tips of the subtree. + */ + TreeController.prototype.findTips = function (nodeKey) { + // throw error + nodeKey = this.model.origToCur[nodeKey]; + var tips = this.model.currentTree.findTips(nodeKey); + for (var i = 0; i < tips.length; i++) { + tips[i] = this.model.curToOrig[tips[i]] + } + return tips; + }; + + /** + * Retrieve number of tips in the subtree of a given node. + * + * @param {Integer} nodeKey The postorder position of a node + * @return {Integer} The number of tips on the subtree rooted at nodeKey. + */ + TreeController.prototype.getNumTips = function (nodeKey) { + nodeKey = this.model.origToCur[nodeKey]; + return this.model.currentTree.getNumTips(nodeKey); + }; + + + /** + * True if name is in the names array for the tree + * + * @param {String} name The name to search for. + * @return {Boolean} If the name is in the tree. + */ + TreeController.prototype.containsNode = function (name) { + return this.model.currentTree.containsNode(name); + }; + + /** + * Returns all nodes with a given name. Once a name has been searched for, + * the returned object is cached in this._nameToNodes. + * + * NOTE: Care should be taken to make sure that this._nameToNodes is not + * populated with a literal null at any point, since Objects in JS store + * all keys as Strings (so having a literal null in this._nameToNodes [due + * to unnamed nodes] will cause this null to get confused with node(s) + * literally named "null" in the Newick file). I don't think this is + * currently possible in the code, but we should probably add tests that + * verify this. + * + * @param {String} name The name of node(s) + * @return {Array} An array of postorder positions of nodes with a given + * name. If no nodes have the specified name, this will be + * an empty array. + */ + TreeController.prototype.getNodesWithName = function (name) { + // check for error + var nodes = this.model.currentTree.getNodesWithName(name); + for (var i = 0; i < nodes.length; i++) { + nodes[i] = this.model.curToOrig[nodes[i]]; + } + return nodes; + }; + + return TreeController; +}); \ No newline at end of file diff --git a/empress/support_files/js/tree-handler.js b/empress/support_files/js/tree-handler.js deleted file mode 100644 index 3c3ce7b04..000000000 --- a/empress/support_files/js/tree-handler.js +++ /dev/null @@ -1,191 +0,0 @@ -define(["LayoutsUtil"], function (LayoutsUtil) { - function TreeHandler(tree) { - this._tree = tree; - this._originalTreeSize = this._tree.size; - this._oldTree = tree; - this._newIndxToOld = {}; - this._currentLayout = "Unrooted"; - - /** - * Used to index into _treeData - * @type {Object} - * @private - */ - this._tdToInd = { - // all nodes (non-layout parameters) - color: 0, - isColored: 1, - visible: 2, - // unrooted layout - x2: 3, - y2: 4, - // rectangular layout - xr: 3, - yr: 4, - highestchildyr: 5, - lowestchildyr: 6, - // circular layout - xc0: 3, - yc0: 4, - xc1: 5, - yc1: 6, - angle: 7, - arcx0: 8, - arcy0: 9, - arcstartangle: 10, - arcendangle: 11, - }; - - /** - * @type {Array} - * The default color of the tree - */ - // this.DEFAULT_COLOR = Colorer.rgbToFloat([64, 64, 64]); - } - - TreeHandler.prototype.shear = function(tips, treeData) { - var result = this._tree.shear(tips); - this._oldTree = this._tree; - this._tree = result.tree; - this._newIndxToOld = result.newIndxToOld; - } - - TreeHandler.prototype.assignCoords = function(treeData) { - var data, i, j = 1; - - for (i = 1; i <= this._originalTreeSize; i++) { - treeData[i][this._tdToInd.visible] = false; - } - console.log(this._currentLayout) - // Rectangular - if (this._currentLayout === "Rectangular") { - data = LayoutsUtil.rectangularLayout( - this._tree, - 4020, - 4020, - // 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. - "descending", - undefined - // lengthGetter, - // checkLengthsChange - ); - // this._yrscf = data.yScalingFactor; - for (i of this.postorder()) { - // remove old layout information - treeData[i].length = 3; - - // store new layout information - treeData[i][this._tdToInd.xr] = data.xCoord[j]; - treeData[i][this._tdToInd.yr] = data.yCoord[j]; - treeData[i][this._tdToInd.highestchildyr] = - data.highestChildYr[j]; - treeData[i][this._tdToInd.lowestchildyr] = - data.lowestChildYr[j]; - treeData[i][this._tdToInd.visible] = true; - j++; - } - } else if (this._currentLayout === "Circular") { - data = LayoutsUtil.circularLayout( - this._tree, - 4020, - 4020, - "descending", - undefined, - // lengthGetter, - // checkLengthsChange - ); - for (i of this.postorder()) { - // remove old layout information - treeData[i].length = 3; - - // store new layout information - treeData[i][this._tdToInd.xc0] = data.x0[j]; - treeData[i][this._tdToInd.yc0] = data.y0[j]; - treeData[i][this._tdToInd.xc1] = data.x1[j]; - treeData[i][this._tdToInd.yc1] = data.y1[j]; - treeData[i][this._tdToInd.angle] = data.angle[j]; - treeData[i][this._tdToInd.arcx0] = data.arcx0[j]; - treeData[i][this._tdToInd.arcy0] = data.arcy0[j]; - treeData[i][this._tdToInd.arcstartangle] = - data.arcStartAngle[j]; - treeData[i][this._tdToInd.arcendangle] = - data.arcEndAngle[j]; - treeData[i][this._tdToInd.visible] = true; - j++; - } - } else { - data = LayoutsUtil.unrootedLayout( - this._tree, - 4020, - 4020, - undefined, - // lengthGetter, - // checkLengthsChange - ); - for (i of this.postorder()) { - // remove old layout information - treeData[i].length = 3; - - // store new layout information - treeData[i][this._tdToInd.x2] = data.xCoord[j]; - treeData[i][this._tdToInd.y2] = data.yCoord[j]; - treeData[i][this._tdToInd.visible] = true; - j++; - } - } - console.log(j, treeData) - - - // var j = 1; - // data = LayoutsUtil.circularLayout( - // this._tree, - // 4020, - // 4020, - // "ascending", - // undefined, - // ); - // for (var i of this.postorder()) { - // // remove old layout information - // treeData[i].length = 3; - - // // store new layout information - // treeData[i][this._tdToInd.xc0] = data.x0[j]; - // treeData[i][this._tdToInd.yc0] = data.y0[j]; - // treeData[i][this._tdToInd.xc1] = data.x1[j]; - // treeData[i][this._tdToInd.yc1] = data.y1[j]; - // treeData[i][this._tdToInd.angle] = data.angle[j]; - // treeData[i][this._tdToInd.arcx0] = data.arcx0[j]; - // treeData[i][this._tdToInd.arcy0] = data.arcy0[j]; - // treeData[i][this._tdToInd.arcstartangle] = - // data.arcStartAngle[j]; - // treeData[i][this._tdToInd.arcendangle] = - // data.arcEndAngle[j]; - // treeData[i][this._tdToInd.visible] = true; - // j++; - // } - } - - TreeHandler.prototype.postorder = function*(includeRoot=false) { - var nodes = [], - i; - if (Object.keys(this._newIndxToOld).length === 0) { - for (i = 1; i <= this._tree.size; i++) { - nodes.push(i); - } - } else { - for (i in this._newIndxToOld) { - nodes.push(this._newIndxToOld[i]) - } - } - yield* nodes; - }; - - TreeHandler - - return TreeHandler; -}); \ No newline at end of file diff --git a/empress/support_files/templates/empress-template.html b/empress/support_files/templates/empress-template.html index 226a1a4dd..8a933ebd3 100644 --- a/empress/support_files/templates/empress-template.html +++ b/empress/support_files/templates/empress-template.html @@ -115,7 +115,7 @@ 'util' : './js/util', 'LayoutsUtil': './js/layouts-util', 'ExportUtil': './js/export-util', - 'TreeHandler': './js/tree-handler' + 'TreeController': './js/tree-controller' } }); diff --git a/tests/index.html b/tests/index.html index b39ad8f52..c38146c24 100644 --- a/tests/index.html +++ b/tests/index.html @@ -203,6 +203,7 @@ 'SelectedNodeMenu' : './support_files/js/select-node-menu', 'LayoutsUtil' : './support_files/js/layouts-util', 'ExportUtil' : './support_files/js/export-util', + 'TreeController' : './support_files/js/tree-controller', /* test utility code */ 'UtilitiesForTesting' : './../tests/utilities-for-testing', @@ -223,6 +224,7 @@ 'testLegend': './../tests/test-legend', 'testLayoutsUtil': './../tests/test-layouts-util', 'testSelectedNodeMenu': './../tests/test-select-node-menu', + 'testTreeController': './../tests/test-tree-controller', } }); @@ -247,21 +249,22 @@ 'LayoutsUtil', 'ExportUtil', 'UtilitiesForTesting', - 'testBPTree', - 'testByteTree', - 'testBIOMTable', - 'testCamera', - 'testColorer', - 'testUtil', - 'testCircularLayoutComputation', - 'testVectorOps', - 'testEmpress', - 'testExport', - 'testAnimationHandler', - 'testBarplots', - 'testLegend', - 'testLayoutsUtil', - 'testSelectedNodeMenu', + // 'testBPTree', + // 'testByteTree', + // 'testBIOMTable', + // 'testCamera', + // 'testColorer', + // 'testUtil', + // 'testCircularLayoutComputation', + // 'testVectorOps', + // 'testEmpress', + // 'testExport', + // 'testAnimationHandler', + // 'testBarplots', + // 'testLegend', + // 'testLayoutsUtil', + // 'testSelectedNodeMenu', + 'testTreeController', ], // start tests @@ -286,20 +289,21 @@ LayoutsUtil, ExportUtil, UtilitiesForTesting, - testBPTree, - testByteTree, - testCamera, - testColorer, - testUtil, - testCircularLayoutComputation, - testVectorOps, - testEmpress, - testExport, - testAnimationHandler, - testBarplots, - testLegend, - testLayoutsUtil, + // testBPTree, + // testByteTree, + // testCamera, + // testColorer, + // testUtil, + // testCircularLayoutComputation, + // testVectorOps, + // testEmpress, + // testExport, + // testAnimationHandler, + // testBarplots, + // testLegend, + // testLayoutsUtil, testSelectedNodeMenu, + testTreeController ) { $(document).ready(function() { QUnit.start(); diff --git a/tests/test-bp-tree.js b/tests/test-bp-tree.js index a327ca04a..90b0c3911 100644 --- a/tests/test-bp-tree.js +++ b/tests/test-bp-tree.js @@ -882,6 +882,8 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { // test modified from https://github.com/wasade/improved-octo-waddle/blob/master/bp/tests/test_bp.py#L228 // newick represenation // ((3,4,(6)5)2, 7, ((10, 11)9)8)r; + + // TODO: add tests for oldToNew var preShearArr = [ 1, 1, @@ -929,7 +931,7 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { ); var keep = new Set(["4", "6", "7", "10", "11"]); - var result = preShearBPTree.shear(keep); + var result = preShearBPTree.shear(keep).tree; deepEqual(result.b_, [ 1, 1, @@ -968,7 +970,7 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { deepEqual(result.lengths_, [null, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); keep = new Set(["7", "10", "11"]); - result = preShearBPTree.shear(keep); + result = preShearBPTree.shear(keep).tree; deepEqual(result.b_, [1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0]); deepEqual(result.names_, [null, "7", "10", "11", "9", "8", "r"]); deepEqual(result.lengths_, [null, 6, 7, 8, 9, 10, 11]); diff --git a/tests/test-tree-controller.js b/tests/test-tree-controller.js new file mode 100644 index 000000000..7d6cf22a2 --- /dev/null +++ b/tests/test-tree-controller.js @@ -0,0 +1,41 @@ +require([ + "jquery", + "UtilitiesForTesting", + "util", + "TreeController" +], function ($, UtilitiesForTesting, util, TreeController) { + $(document).ready(function () { + // Setup test variables + // Note: This is ran for each test() so tests can modify bpArray + // without affecting other tests. + module("TreeController", { + setup: function () { + this.tree = UtilitiesForTesting.getTestData(false).tree; + this.tree.names_ = ["", "1", "2", "3", "4", "5", "6", "7"]; + this.treeController = new TreeController(this.tree); + }, + + teardown: function () { + this.tree = null; + this.treeController = null; + }, + }); + + test("Test shear", function () { + this.treeController.shear(new Set(["2", "3"])); + // console.log(this.treeController.getAllNames()); + var node = this.treeController.fchild(0); + while (node !== 0) { + console.log(this.treeController.name(node), this.treeController.isleaf(node) ,node); + node = this.treeController.fchild(node) + } + + node = 7; + while (node !== 0) { + console.log(this.treeController.name(node), this.treeController.isleaf(node) ,node); + node = this.treeController.parent(node) + } + console.log(this.treeController.model.currentTree.name(this.treeController.model.currentTree.postorderselect(this.treeController.model.currentTree.size))) + }); + }); +}); \ No newline at end of file From 7301e8263efc8cb3142455b1fd40d60b394f6e54 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Tue, 16 Mar 2021 18:36:21 -0700 Subject: [PATCH 06/63] tree-controller --- empress/support_files/js/bp-tree.js | 4 +- empress/support_files/js/empress.js | 58 ++- empress/support_files/js/tree-controller.js | 358 ++++++++++-------- tests/index.html | 59 +-- tests/test-tree-controller.js | 382 ++++++++++++++++++-- 5 files changed, 627 insertions(+), 234 deletions(-) diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index 571db3cf1..09c4d4825 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -1015,7 +1015,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { var newBitArray = []; var newToOld = {}; - var oldToNew = {} + var oldToNew = {}; var postorderPos = 1; for (i = 0; i < mask.length; i++) { if (mask[i] !== undefined) { @@ -1036,7 +1036,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { newToOld: newToOld, oldToNew: oldToNew, tree: new BPTree(newBitArray, names, lengths, null), - } + }; }; return BPTree; diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 5254dead8..7c9ac5ccd 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -25,7 +25,7 @@ define([ chroma, LayoutsUtil, ExportUtil, - TreeController, + TreeController ) { /** * @class EmpressTree @@ -377,7 +377,9 @@ define([ * Also updates this._maxDisplacement. */ Empress.prototype.getLayoutInfo = function () { - var data, i, j = 1; + var data, + i, + j = 1; // set up length getter var branchMethod = this.branchMethod; var checkLengthsChange = LayoutsUtil.shouldCheckBranchLengthsChanged( @@ -405,7 +407,7 @@ define([ checkLengthsChange ); this._yrscf = data.yScalingFactor; - for (i of this._tree.postorderTraversal(includeRoot=true)) { + for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; @@ -427,7 +429,7 @@ define([ lengthGetter, checkLengthsChange ); - for (i of this._tree.postorderTraversal(includeRoot=true)) { + for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; @@ -453,7 +455,7 @@ define([ lengthGetter, checkLengthsChange ); - for (i of this._tree.postorderTraversal(includeRoot=true)) { + for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; @@ -470,29 +472,6 @@ define([ * Initializes WebGL and then draws the tree */ Empress.prototype.initialize = function () { - var fmInfo = this.getUniqueFeatureMetadataInfo("Level 2", "tip"); - var sortedUniqueValues = fmInfo.sortedUniqueValues; - var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; - // convert observation IDs to _treeData keys. Notably, this includes - // converting the values of uniqueValueToFeatures from Arrays to Sets. - - var obs = {}; - _.each(sortedUniqueValues, function (uniqueVal, i) { - uniqueVal = sortedUniqueValues[i]; - obs[uniqueVal] = new Set(uniqueValueToFeatures[uniqueVal]); - }); - console.log((obs["p__Bacteroidetes"])) - names = [] - for (var i of obs["p__Bacteroidetes"].values()) { - console.log(i) - if (this._tree.isleaf(this._tree.postorderselect(i))){ - names.push(this._tree.name(this._tree.postorderselect(i))) - } - } - console.log(names) - this._tree.shear(new Set(names)) - - this._drawer.initialize(); this._events.setMouseEvents(); var nodeNames = this._tree.getAllNames(); @@ -722,7 +701,6 @@ define([ addPoint(this.getX(node), this.getY(node)); } } - console.log(coords) return new Float32Array(coords); }; @@ -916,7 +894,7 @@ define([ throw new Error("getNodeCoords() drawNodeCircles is out of range"); } - for (var node of this._tree.postorderTraversal(includeRoot=true)) { + for (var node of this._tree.postorderTraversal((includeRoot = true))) { if (!comp(node)) { continue; } @@ -2701,7 +2679,7 @@ define([ var x = 0, y = 0, zoomAmount = 0; - for (var node of this._tree.postorderTraversal(includeRoot=true)) { + for (var node of this._tree.postorderTraversal((includeRoot = true))) { // node = this._treeData[node]; x += this.getX(node); y += this.getY(node); @@ -2792,7 +2770,7 @@ define([ this._collapsedClades = {}; // Note: currently collapseClades is the only method that set // the node visibility property. - for (var i of this._tree.postorderTraversal(includeRoot=true)) { + for (var i of this._tree.postorderTraversal((includeRoot = true))) { this.setNodeInfo(i, "visible", true); } @@ -3585,5 +3563,21 @@ define([ } }; + Empress.prototype.shear = function (cat, value) { + var nodeNames = this._tree.getAllNames(); + + var fmInfo = this.getUniqueFeatureMetadataInfo(cat, "tip"); + var sortedUniqueValues = fmInfo.sortedUniqueValues; + var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; + // convert observation IDs to _treeData keys. Notably, this includes + // converting the values of uniqueValueToFeatures from Arrays to Sets. + var obs = uniqueValueToFeatures[value]; + var tipNames = []; + for (var i of obs.values()) { + tipNames.push(this._tree.name(this._tree.postorderselect(i))); + } + this._tree.shear(new Set(tipNames)); + }; + return Empress; }); diff --git a/empress/support_files/js/tree-controller.js b/empress/support_files/js/tree-controller.js index 3cc88fad4..250fdd2ce 100644 --- a/empress/support_files/js/tree-controller.js +++ b/empress/support_files/js/tree-controller.js @@ -12,135 +12,150 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { } } - TreeModel.prototype.shear = function(tips, keepNames=false) { - var result = this.originalTree.shear(tips, keepNames); + TreeModel.prototype.shear = function (tips) { + var result = this.originalTree.shear(tips); this.currentTree = result.tree; this.curToOrig = result.newToOld; this.origToCur = result.oldToNew; - console.log(this.curToOrig) - console.log(this.origToCur) - } + }; - TreeModel.prototype.unshear = function() { + TreeModel.prototype.unshear = function () { this.currentTree = this.originalTree; - // initialize for (var i = 1; i <= this.currentTree.size; i++) { this.origToCur[i] = i; this.curToOrig[i] = i; } - } + }; - TreeModel.prototype.postorderTraversal = function*(includeRoot=false) { + TreeModel.prototype.postorderTraversal = function* (includeRoot = false) { var nodes = [], i; for (i = 1; i <= Object.keys(this.curToOrig).length; i++) { - nodes.push(this.curToOrig[i]) + nodes.push(this.curToOrig[i]); } if (!includeRoot) { nodes.pop(); } yield* nodes; - } - - function TreeController(tree) { - this.model = new TreeModel(tree); - this.size = this.model.originalTree.size; - } + }; - TreeController.prototype.indexInCurrentTree = function(i) { - var node = this.model.originalTree.postorder(i); - return this.model.origToCur.hasOwnProperty(node); + function TreeController(tree) { + /** + * + * @class TreeController + * + * Initialzes a new TreeController. This class is extends BPTree and allows + * EMPress to dynamically shear the tree. TreeController's UI is similar to + * BPTree. The input/output to all functions shared between TreeController + * and BPTree are in respect to the original tree. For example, + * postorderselect(5) will return the index of the 5th node in a postorder + * traversal of the original tree. However, TreeController implements a new + * function __curToOrigNodeFunction() that uses the topology of the sheared + * tree to execute fchild, lchild, nsibling, and psibling. Thus, + * fchild(5) will return the first child of node 5 in the sheared tree. + * However, the input/output of fchild, lchild, nsibling, and psibling are + * still in relation to the original tree. So, fchild(5) means the first + * child of a node in the sheared tree that corresponds to the 5th node + * found in a post order traversal of the original tree. In addition the + * traversal methods such as postorderTraversal will also use the topology + * of the sheared tree but will output the results in relation to the + * original tree. The reason for this behavior is due to the fact that + * empress uses a nodes postorder postion (in the orginal tree) as its key + * in the various metadata structures. + * + * @param {BPTree} tree This should be the original BPTree created when + * initializing empress. + * + * @return {TreeController} + * @constructs TreeController + */ + this.model = new TreeModel(tree); + this.size = this.model.originalTree.size; } /** - * tips - Set + * Removes nodes from the original tree until only the nodes found in tips + * and there ancestors remain in the tree. + * + * @param{Set} tips A set of tip names that will be kept. */ - TreeController.prototype.shear = function(tips, keepNames=false) { - this.model.shear(tips, keepNames); - } + TreeController.prototype.shear = function (tips) { + this.model.shear(tips); + }; + /** + * Restores the original tree. + */ + TreeController.prototype.unshear = function () { + this.model.unshear(); + }; - // add option for original - TreeController.prototype.postorderTraversal = function*(includeRoot=false) { - yield* this.model.postorderTraversal(includeRoot); - }; + /** + * Returns an iterator for nodes in a post order traversal of the current + * tree. + * + * Note: This method will use the topology of the currect tree but will + * return the nodes position in the original tree. + * + * @param{Boolean} includeRoot If true then the root will be included. + */ + TreeController.prototype.postorderTraversal = function* ( + includeRoot = false + ) { + yield* this.model.postorderTraversal(includeRoot); + }; /** * Returns an Object describing the minimum, maximum, and average of all - * non-root node lengths. + * non-root node lengths in the current tree. * - * @param{Boolean} original if true returns the length stats of the original - * tree else returns the length stats of the - * sheared tree. - * * @return {Object} Contains three keys: "min", "max", and "avg", mapping * to Numbers representing the minimum, maximum, and * average non-root node length in the tree. */ - TreeController.prototype.getLengthStats = function(origianl=false) { - if (origianl) { - return this.model.originalTree.getLengthStats(); - } else { - return this.model.currentTree.getLengthStats(); - } - } + TreeController.prototype.getLengthStats = function () { + return this.model.currentTree.getLengthStats(); + }; /** + * Return the name of the ith index in the ORIGINAL bp tree. + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. * - * The name of the ith node in the ORIGINAL bp tree * - * @param{Number} i Node i in the ORIGINAL bp tree + * @param{Number} i The index corresponding to a node in the ORIGINAL tree * * @return{String} */ - TreeController.prototype.name = function(i) { - // check for error - // if (!this.indexInCurrentTree(i)) { - // throw "Node index " + i + " is not in currentTree"; - // } + TreeController.prototype.name = function (i) { return this.model.originalTree.name(i); - } + }; /** - * Returns an array of all node names in tree. - * - * @param{Boolean} original if true returns the length stats of the original - * tree else returns the length stats of the - * sheared tree. - * + * Returns an array of all node names in current tree. */ - TreeController.prototype.getAllNames = function(original=false) { - if (original) { - return this.model.originalTree.getAllNames(); - } else { - return this.model.currentTree.getAllNames(); - } + TreeController.prototype.getAllNames = function () { + return this.model.currentTree.getAllNames(); }; /** + * Returns the number of leaf nodes in current tree * - * The number of leaf nodes in tree - * - * @param{Boolean} original if true returns the length stats of the original - * tree else returns the length stats of the - * sheared tree. - * * @return {Number} */ - TreeController.prototype.numleaves = function(original=false) { - if (original) { - return this.model.originalTree.numleaves(); - } else { - return this.model.currentTree.numleaves(); - } + TreeController.prototype.numleaves = function () { + return this.model.currentTree.numleaves(); }; /** + * Returns the length of the ith index in the ORIGINAL bp tree. * - * The length of the ith node in bp tree + * Note: The input of this method should the result of either preorderselect + * or postorderselect. * - * @param{Number} i Node i in bp tree + * @param{Number} i The index corresponding to a node in the ORIGINAL tree * * @return{Number} */ @@ -149,58 +164,81 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { }; /** - * The opening index of the smallest matching pair that contains the node - * represented by i in the ORIGINAL tree. + * Return the parent index of the node that corresponds to the ith index in + * the ORIGINAL bp tree. * - * @param{Number} i Current node - * i can be either an open or close parenthesis + * Note: The input of this method should the result of either preorderselect + * or postorderselect. * - * @return {Number} + * Note: The output of this method is also in relation to the original tree. + * + * @param{Number} i The index corresponding to a node in the ORIGINAL tree + * + * @return{Number} */ TreeController.prototype.parent = function (i) { return this.model.originalTree.parent(i); - } + }; /** - * returns the index of the opening index of the root node + * Returns the index of the opening index of the root node. + * + * Note: This will always be 0. * * @return {Number} */ TreeController.prototype.root = function () { return this.model.originalTree.root(); - } + }; /** - * Return true if i represents a leaf node + * Returns true if i represents a leaf node + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. + * + * @param{Number} i The index corresponding to a node in the ORIGINAL tree * * @return {Boolean} */ TreeController.prototype.isleaf = function (i) { - // check for err return this.model.originalTree.isleaf(i); }; - TreeController.prototype._curToOrigNodeFunction = function(i, func) { + /** + * This method is used in fchild, lchild, nsibling, and psibling and it what + * allows TreeController to use the topology of the current tree but return + * the results w.r.t the original tree. + * + * @param{Number} i The index correspond to a node in the ORIGINAL tree. + * @param{String} func The function to use. This should only be fchild, + * nchild, nsibling or psibling. + * + * @return{Number} The result of func w.r.t the ORIGINAL tree. + */ + + TreeController.prototype._curToOrigNodeFunction = function (i, func) { var curTree = this.model.currentTree; var origTree = this.model.originalTree; - // check for err var node = curTree.postorderselect( this.model.origToCur[origTree.postorder(i)] ); var node = curTree.postorder(curTree[func](node)); - node = origTree.postorderselect( - this.model.curToOrig[node] - ); + node = origTree.postorderselect(this.model.curToOrig[node]); return node; - } + }; /** - * returns the opening index the first child of the node represented by i + * Returns the opening index of first child of the node represented by i. + * This method will use the topology of the current (sheared) tree but its + * input and output will be w.r.t the ORGINAL tree. + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. * - * @param {Number} i Current node - * i can be either an open or close parenthesis + * @param{Number} i The index corresponding to a node in the ORIGINAL tree * * @return {Number} return 0 if i is a leaf node */ @@ -209,54 +247,69 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { }; /** - * returns the opening index the last child of the node represented by i + * Returns the opening index of last child of the node represented by i. + * This method will use the topology of the current (sheared) tree but its + * input and output will be w.r.t the ORGINAL tree. + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. + * + * @param{Number} i The index corresponding to a node in the ORIGINAL tree * - * @param {Number} i Current node - * i can be either an open or close parenthesis * @return {Number} return 0 if i is a leaf node */ TreeController.prototype.lchild = function (i) { - return this._curToOrigNodeFunction(i, "lchild") + return this._curToOrigNodeFunction(i, "lchild"); }; /** - * returns the opening index of the next sibling of i + * Returns the opening index of next sibling of the node represented by i. + * This method will use the topology of the current (sheared) tree but its + * input and output will be w.r.t the ORGINAL tree. * - * @param {Number} i Current node. - * i can be either an open or close parenthesis + * Note: The input of this method should the result of either preorderselect + * or postorderselect. * - * @return {Number} + * @param{Number} i The index corresponding to a node in the ORIGINAL tree + * + * @return {Number} return 0 if i does not have a next sibling */ TreeController.prototype.nsibling = function (i) { return this._curToOrigNodeFunction(i, "nsibling"); }; /** - * returns the opening index of the previous sibling of i + * Returns the opening index of previous sibling of the node represented by + * i. This method will use the topology of the current (sheared) tree but + * its input and output will be w.r.t the ORGINAL tree. * - * @param {Number} i Current node. - * i can be either an open or close parenthesis + * Note: The input of this method should the result of either preorderselect + * or postorderselect. * - * @return {Number} returns 0 if no previous sibling + * @param{Number} i The index corresponding to a node in the ORIGINAL tree + * + * @return {Number} return 0 if i does not have a previous sibling */ TreeController.prototype.psibling = function (i) { return this._curToOrigNodeFunction(i, "psibling"); }; - /** - * Finds postorder rank of node i + /** + * Returns the postorder rank of index i in the ORIGINAL tree. * - * @param {Number} i The node index to asses postorder rank + * Note: The input of this method should the result of parent, fchild, + * lchild, nsibling or psibling. * - * @return {Number} The postorder rank of node i + * @param {Number} i The index to assess postorder rank + * + * @return {Number} The postorder rank of index i */ TreeController.prototype.postorder = function (i) { - // check error return this.model.originalTree.postorder(i); }; /** - * Find the index of the node with postorder k + * Find the index of the node with postorder k in the ORIGINAL tree. * * @param {Number} k The postorder to search for * Note: k starts at 1 @@ -264,24 +317,25 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Number} The index position of the node in the tree */ TreeController.prototype.postorderselect = function (k) { - // check error return this.model.originalTree.postorderselect(k); }; /** - * Finds preorder rank of node i + * Returns the preorder rank of index i in the ORIGINAL tree. + * + * Note: The input of this method should the result of parent, fchild, + * lchild, nsibling or psibling. * - * @param {Number} i The node index to asses preorder rank + * @param {Number} i The index to assess preorder rank * - * @return {Number} The preorder rank of node i + * @return {Number} The preorder rank of index i */ TreeController.prototype.preorder = function (i) { - // check error return this.model.originalTree.preorder(i); }; /** - * Find the index of the node with preorder k + * Find the index of the node with preorder k in the ORIGINAL tree. * * @param {Number} k The preorder to search for. * Note: k starts at 1. @@ -289,19 +343,21 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Number} The index position of the node in the tree */ TreeController.prototype.preorderselect = function (k) { - // check error return this.model.originalTree.preorderselect(k); }; /** - * Returns an array of nodes sorted by their inorder position. + * Returns an iterator for nodes in an in-order traversal of the current + * tree. * - * Note: empress uses a nodes postorder position as its key in _treeData - * so this method will use a nodes postorder position to represent - * it in the resulting array. - * This method will also cache the resulting array. + * Note: This method will use the topology of the currect tree but will + * return the nodes position in the original tree. + * + * @param{Boolean} includeRoot If true then the root will be included. */ - TreeController.prototype.inOrderTraversal = function*(includeRoot=false) { + TreeController.prototype.inOrderTraversal = function* ( + includeRoot = false + ) { var inOrderNodes = this.model.currentTree.inOrderNodes(); if (Object.keys(this.model.curToOrig).length !== 0) { for (var i = 0; i < inOrderNodes.length; i++) { @@ -311,12 +367,13 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { if (!includeRoot) { inOrderNodes.shift(); } - console.log("inorder", inOrderNodes) yield* inOrderNodes; }; /** - * Finds the sum of lengths from start to end. + * Finds the sum of lengths from start to end. This method will use the + * topology of the current tree but its input must be w.r.t the ORIGINAL + * tree. * * Note: start must be a descendant of end. An error will be thrown if start * is not a descendant of end. Also, this method does not take into @@ -330,32 +387,43 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * * @return {Number} the sum of length from start to end */ - TreeController.prototype.getTotalLength = function (start, end, ignoreLengths) { + TreeController.prototype.getTotalLength = function ( + start, + end, + ignoreLengths + ) { start = this.model.origToCur[start]; end = this.model.origToCur[end]; return this.model.currentTree.getTotalLength(start, end, ignoreLengths); }; /** - * Retrieve the tips in the subtree of a given (internal) node key. + * Retrieve the tips in the subtree of a given (internal) node key. This + * method will use the topology of the current tree but its input/output + * will be w.r.t the ORIGINAL tree. + * + * @param {Number} nodeKey The post-order position of a node in the ORIGINAL + * tree * - * @param {Number} nodeKey Key value of internal node. * @return {Array} tips Tips of the subtree. */ TreeController.prototype.findTips = function (nodeKey) { - // throw error nodeKey = this.model.origToCur[nodeKey]; var tips = this.model.currentTree.findTips(nodeKey); for (var i = 0; i < tips.length; i++) { - tips[i] = this.model.curToOrig[tips[i]] + tips[i] = this.model.curToOrig[tips[i]]; } return tips; }; /** - * Retrieve number of tips in the subtree of a given node. + * Retrieve number of tips in the subtree of a given node. This method will + * use the topology of the current tree but its input must be w.r.t the + * ORIGINAL tree. + * + * @param {Integer} nodeKey The postorder position of a node in the ORIGINAL + * tree * - * @param {Integer} nodeKey The postorder position of a node * @return {Integer} The number of tips on the subtree rooted at nodeKey. */ TreeController.prototype.getNumTips = function (nodeKey) { @@ -363,11 +431,11 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { return this.model.currentTree.getNumTips(nodeKey); }; - /** - * True if name is in the names array for the tree + * Checks to see if name is in the current tree. * * @param {String} name The name to search for. + * * @return {Boolean} If the name is in the tree. */ TreeController.prototype.containsNode = function (name) { @@ -375,24 +443,16 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { }; /** - * Returns all nodes with a given name. Once a name has been searched for, - * the returned object is cached in this._nameToNodes. - * - * NOTE: Care should be taken to make sure that this._nameToNodes is not - * populated with a literal null at any point, since Objects in JS store - * all keys as Strings (so having a literal null in this._nameToNodes [due - * to unnamed nodes] will cause this null to get confused with node(s) - * literally named "null" in the Newick file). I don't think this is - * currently possible in the code, but we should probably add tests that - * verify this. - * - * @param {String} name The name of node(s) + * Returns all nodes with a given name. This method will use the topology + * of the current tree but its output will be w.r.t the ORIGINAL tree. + * + * @param {String} name The name of the node(s) + * * @return {Array} An array of postorder positions of nodes with a given * name. If no nodes have the specified name, this will be * an empty array. */ TreeController.prototype.getNodesWithName = function (name) { - // check for error var nodes = this.model.currentTree.getNodesWithName(name); for (var i = 0; i < nodes.length; i++) { nodes[i] = this.model.curToOrig[nodes[i]]; @@ -400,5 +460,5 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { return nodes; }; - return TreeController; -}); \ No newline at end of file + return TreeController; +}); diff --git a/tests/index.html b/tests/index.html index c38146c24..78f79dc83 100644 --- a/tests/index.html +++ b/tests/index.html @@ -230,7 +230,8 @@ // load tests require( - ['jquery', + [ + 'jquery', 'glMatrix', 'chroma', 'underscore', @@ -249,21 +250,21 @@ 'LayoutsUtil', 'ExportUtil', 'UtilitiesForTesting', - // 'testBPTree', - // 'testByteTree', - // 'testBIOMTable', - // 'testCamera', - // 'testColorer', - // 'testUtil', - // 'testCircularLayoutComputation', - // 'testVectorOps', - // 'testEmpress', - // 'testExport', - // 'testAnimationHandler', - // 'testBarplots', - // 'testLegend', - // 'testLayoutsUtil', - // 'testSelectedNodeMenu', + 'testBPTree', + 'testByteTree', + 'testBIOMTable', + 'testCamera', + 'testColorer', + 'testUtil', + 'testCircularLayoutComputation', + 'testVectorOps', + 'testEmpress', + 'testExport', + 'testAnimationHandler', + 'testBarplots', + 'testLegend', + 'testLayoutsUtil', + 'testSelectedNodeMenu', 'testTreeController', ], @@ -289,19 +290,19 @@ LayoutsUtil, ExportUtil, UtilitiesForTesting, - // testBPTree, - // testByteTree, - // testCamera, - // testColorer, - // testUtil, - // testCircularLayoutComputation, - // testVectorOps, - // testEmpress, - // testExport, - // testAnimationHandler, - // testBarplots, - // testLegend, - // testLayoutsUtil, + testBPTree, + testByteTree, + testCamera, + testColorer, + testUtil, + testCircularLayoutComputation, + testVectorOps, + testEmpress, + testExport, + testAnimationHandler, + testBarplots, + testLegend, + testLayoutsUtil, testSelectedNodeMenu, testTreeController ) { diff --git a/tests/test-tree-controller.js b/tests/test-tree-controller.js index 7d6cf22a2..bfc41289b 100644 --- a/tests/test-tree-controller.js +++ b/tests/test-tree-controller.js @@ -1,9 +1,9 @@ -require([ - "jquery", - "UtilitiesForTesting", - "util", - "TreeController" -], function ($, UtilitiesForTesting, util, TreeController) { +require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( + $, + UtilitiesForTesting, + util, + TreeController +) { $(document).ready(function () { // Setup test variables // Note: This is ran for each test() so tests can modify bpArray @@ -11,7 +11,10 @@ require([ module("TreeController", { setup: function () { this.tree = UtilitiesForTesting.getTestData(false).tree; - this.tree.names_ = ["", "1", "2", "3", "4", "5", "6", "7"]; + this.names = ["", "t1", "t2", "t3", "i4", "i5", "t6", "r"]; + this.tree.names_ = this.names; + this.lengths = [null, 1, 2, 3, 4, 5, 6, null]; + this.tree.lengths_ = this.lengths; this.treeController = new TreeController(this.tree); }, @@ -22,20 +25,355 @@ require([ }); test("Test shear", function () { - this.treeController.shear(new Set(["2", "3"])); - // console.log(this.treeController.getAllNames()); - var node = this.treeController.fchild(0); - while (node !== 0) { - console.log(this.treeController.name(node), this.treeController.isleaf(node) ,node); - node = this.treeController.fchild(node) - } - - node = 7; - while (node !== 0) { - console.log(this.treeController.name(node), this.treeController.isleaf(node) ,node); - node = this.treeController.parent(node) - } - console.log(this.treeController.model.currentTree.name(this.treeController.model.currentTree.postorderselect(this.treeController.model.currentTree.size))) + this.treeController.shear(new Set(["t2", "t3"])); + + // checks to make sure correct names are kept + var shearNames = [null, "t2", "t3", "i4", "i5", "r"]; + var resutlNames = this.treeController.model.currentTree.names_; + deepEqual(resutlNames, shearNames); + + var shearLengths = [null, 2, 3, 4, 5, null]; + var resultLengts = this.treeController.model.currentTree.lengths_; + deepEqual(resultLengts, shearLengths); + + // checks to make sure structre of tree is correct + var shearTree = [1, 1, 1, 1, 0, 1, 0, 0, 0, 0]; + var resultTree = this.treeController.model.currentTree.b_; + deepEqual(resultTree, shearTree); + + // checks to make sure the mappings from orignal tree to shear tree + // is correct and vice-versa + var origToCur = { + "2": 1, + "3": 2, + "4": 3, + "5": 4, + "7": 5, + }; + var curToOrig = { + "1": 2, + "2": 3, + "3": 4, + "4": 5, + "5": 7, + }; + var resultOrigToCur = this.treeController.model.origToCur; + var resultCurToOrig = this.treeController.model.curToOrig; + deepEqual(resultOrigToCur, origToCur); + deepEqual(resultCurToOrig, curToOrig); + }); + + test("Test unshear", function () { + this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.unshear(); + + deepEqual(this.treeController.model.currentTree.names_, this.names); + deepEqual( + this.treeController.model.currentTree.lengths_, + this.lengths + ); + + var map = { + "1": 1, + "2": 2, + "3": 3, + "4": 4, + "5": 5, + "6": 6, + "7": 7, + }; + deepEqual(this.treeController.model.curToOrig, map); + deepEqual(this.treeController.model.origToCur, map); + }); + + test("Test postorderTraversal", function () { + this.treeController.shear(new Set(["t2", "t3"])); + var nodes = [2, 3, 4, 5, 7]; + var result = [ + ...this.treeController.postorderTraversal((includeRoot = true)), + ]; + deepEqual(result, nodes); + + nodes.pop(); + result = [ + ...this.treeController.postorderTraversal( + (includeRoot = false) + ), + ]; + deepEqual(result, nodes); + + this.treeController.unshear(); + nodes = [1, 2, 3, 4, 5, 6, 7]; + result = [ + ...this.treeController.postorderTraversal((includeRoot = true)), + ]; + deepEqual(result, nodes); + }); + + test("Test getLengthStats", function () { + this.treeController.shear(new Set(["t2", "t3"])); + var stats = { + avg: 3.5, + min: 2, + max: 5, + }; + var result = this.treeController.getLengthStats(); + deepEqual(result, stats); + + this.treeController.unshear(); + stats = { + avg: 3.5, + min: 1, + max: 6, + }; + result = this.treeController.getLengthStats(); + deepEqual(result, stats); + }); + + test("Test name", function () { + // name() only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + var name = this.treeController.name(index); + deepEqual(name, "t1"); + }); + + test("Test getAllNames", function () { + this.treeController.shear(new Set(["t2", "t3"])); + var shearNames = ["t2", "t3", "i4", "i5", "r"]; + var resutlNames = this.treeController.getAllNames(); + deepEqual(resutlNames, shearNames); + + this.treeController.unshear(); + shearNames = ["t1", "t2", "t3", "i4", "i5", "t6", "r"]; + deepEqual(this.treeController.getAllNames(), shearNames); + }); + + test("Test numleaves", function () { + this.treeController.shear(new Set(["t2", "t3"])); + equal(this.treeController.numleaves(), 2); + + this.treeController.unshear(); + equal(this.treeController.numleaves(), 4); + }); + + test("Test length", function () { + // length() only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + var length = this.treeController.length(index); + equal(length, 1); + }); + + test("Test parent", function () { + // parent() only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + var parent = this.treeController.parent(index); + parent = this.treeController.postorder(parent); + equal(parent, 5); + }); + + test("Test root", function () { + // root() only uses the original tree and thus is not effected + // by the shear operation + equal(this.treeController.root(), 0); + }); + + test("Test isleaf", function () { + // isleaf() only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + var isleaf = this.treeController.isleaf(index); + equal(isleaf, true); + + index = this.treeController.postorderselect(7); + var isleaf = this.treeController.isleaf(index); + equal(isleaf, false); + }); + + test("Test fchild", function () { + // fchild's input/output is in respect to the original tree. + // However, fchild will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var index = this.treeController.postorderselect(5); + var fchild = this.treeController.fchild(index); + var expected = this.treeController.postorderselect(4); + equal(fchild, expected); + + this.treeController.unshear(); + index = this.treeController.postorderselect(5); + fchild = this.treeController.fchild(index); + expected = this.treeController.postorderselect(1); + equal(fchild, expected); + }); + + test("Test lchild", function () { + // lchild's input/output is in respect to the original tree. + // However, lchild will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var index = this.treeController.postorderselect(7); + var lchild = this.treeController.lchild(index); + var expected = this.treeController.postorderselect(5); + equal(lchild, expected); + + this.treeController.unshear(); + index = this.treeController.postorderselect(7); + lchild = this.treeController.lchild(index); + expected = this.treeController.postorderselect(6); + equal(lchild, expected); + }); + + test("Test nsibling", function () { + // nsibling's input/output is in respect to the original tree. + // However, nsibling will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var index = this.treeController.postorderselect(5); + var nsibling = this.treeController.nsibling(index); + var expected = 0; // doesn't have a next sibling + equal(nsibling, expected); + + this.treeController.unshear(); + index = this.treeController.postorderselect(5); + nsibling = this.treeController.nsibling(index); + expected = this.treeController.postorderselect(6); + equal(nsibling, expected); + }); + + test("Test psibling", function () { + // psibling's input/output is in respect to the original tree. + // However, psibling will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var index = this.treeController.postorderselect(4); + var psibling = this.treeController.psibling(index); + var expected = 0; // doesn't have a next sibling + equal(psibling, expected); + + this.treeController.unshear(); + index = this.treeController.postorderselect(4); + psibling = this.treeController.psibling(index); + expected = this.treeController.postorderselect(1); + equal(psibling, expected); + }); + + test("Test postorder", function () { + // postorder only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + var postorder = this.treeController.postorder(index); + equal(postorder, 1); + }); + + test("Test postorderselect", function () { + // postorderselect only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + equal(index, 2); + }); + + test("Test preorder", function () { + // preorder only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.preorderselect(1); + var preorder = this.treeController.preorder(index); + equal(preorder, 1); + }); + + test("Test preorderselect", function () { + // preorderselect only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.preorderselect(1); + equal(index, 0); + }); + + test("Test inOrderTraversal", function () { + // inOrderTraversal's input/output is in respect to the original tree. + // However, inOrderTraversal will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var expected = [7, 5, 4, 2, 3]; + var result = [ + ...this.treeController.inOrderTraversal((includeRoot = true)), + ]; + deepEqual(result, expected); + expected.shift(); + result = [ + ...this.treeController.inOrderTraversal((includeRoot = false)), + ]; + deepEqual(result, expected); + + this.treeController.unshear(); + var expected = [7, 5, 6, 1, 4, 2, 3]; + var result = [ + ...this.treeController.inOrderTraversal((includeRoot = true)), + ]; + deepEqual(result, expected); + expected.shift(); + result = [ + ...this.treeController.inOrderTraversal((includeRoot = false)), + ]; + deepEqual(result, expected); + }); + + test("Test getTotalLength", function () { + // getTotalLength's input/output is in respect to the original tree. + // However, getTotalLength will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var result = this.treeController.getTotalLength(2, 7); + equal(result, 11); + + this.treeController.unshear(); + var result = this.treeController.getTotalLength(2, 7); + equal(result, 11); + }); + + test("Test findTips", function () { + // findTips's input/output is in respect to the original tree. + // However, findTips will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var result = this.treeController.findTips(5); + deepEqual(result, [2, 3]); + + this.treeController.unshear(); + result = this.treeController.findTips(5); + deepEqual(result, [1, 2, 3]); + }); + + test("Test getNumTips", function () { + // getNumTips's input/output is in respect to the original tree. + // However, getNumTips will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var result = this.treeController.getNumTips(5); + deepEqual(result, 2); + + this.treeController.unshear(); + result = this.treeController.getNumTips(5); + deepEqual(result, 3); + }); + + test("Test containsNode", function () { + this.treeController.shear(new Set(["t2", "t3"])); + var result = this.treeController.containsNode("t1"); + equal(result, false); + + this.treeController.unshear(); + result = this.treeController.containsNode("t1"); + equal(result, true); + }); + + test("Test getNodesWithName", function () { + // getNodesWithName's input/output is in respect to the original tree. + // However, getNodesWithName will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var result = this.treeController.getNodesWithName("t2"); + deepEqual(result, [2]); + result = this.treeController.getNodesWithName("t1"); + deepEqual(result, []); + + this.treeController.unshear(); + result = this.treeController.getNodesWithName("t2"); + deepEqual(result, [2]); + result = this.treeController.getNodesWithName("t1"); + deepEqual(result, [1]); }); }); -}); \ No newline at end of file +}); From f4f668f9168e6547ad932c49d3d3fe19c2d0fb99 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Tue, 16 Mar 2021 19:06:15 -0700 Subject: [PATCH 07/63] fix doc issue --- empress/support_files/js/bp-tree.js | 12 +++++++++++- empress/support_files/js/empress.js | 1 - 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index 09c4d4825..6876e8da6 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -972,7 +972,17 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { }; /** - * tips - Set + * Returns a new BPTree object that contains just the tips (and ancestors) + * of the nodes in keepTips. + * + * This method was ported from iow. + * https://github.com/wasade/improved-octo-waddle/blob/0e9e75b77238acda6752f59d940620f89607ba6b/bp/_bp.pyx#L732 + * + * @param {Set} keepTips The set of tip names to keep. + * + * @return {Object} An object containing the new tree and two maps that + * convert the original postorder positions to the sheared + * tree postorder positions and vice-versa. */ BPTree.prototype.shear = function (keepTips) { // closure diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 7c9ac5ccd..c0747f537 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -2826,7 +2826,6 @@ define([ // collaped. // Collapsing a clade will set the .visible property of members to // false and will then be skipped in the for loop. - // var inorder = this._tree.inOrderTraversal(); for (var node of this._tree.inOrderTraversal()) { // dont collapse clade if (this._dontCollapse.has(node)) { From a408326334584737fa2a613e3ba296cd809d9af4 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Tue, 16 Mar 2021 20:05:28 -0700 Subject: [PATCH 08/63] style fix --- empress/support_files/js/empress.js | 2 +- empress/support_files/js/tree-controller.js | 2 +- tests/test-tree-controller.js | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index c0747f537..8cd33c9a8 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -1939,7 +1939,7 @@ define([ } else { halfAngleRange = Math.PI / this._tree.numleaves(); } - for (node of this._tree.postorderTraversal()) { + for (var node of this._tree.postorderTraversal()) { if (this._tree.isleaf(this._tree.postorderselect(node))) { var name = this.getNodeInfo(node, "name"); var fm; diff --git a/empress/support_files/js/tree-controller.js b/empress/support_files/js/tree-controller.js index 250fdd2ce..6cc549c59 100644 --- a/empress/support_files/js/tree-controller.js +++ b/empress/support_files/js/tree-controller.js @@ -225,7 +225,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { this.model.origToCur[origTree.postorder(i)] ); - var node = curTree.postorder(curTree[func](node)); + node = curTree.postorder(curTree[func](node)); node = origTree.postorderselect(this.model.curToOrig[node]); return node; }; diff --git a/tests/test-tree-controller.js b/tests/test-tree-controller.js index bfc41289b..8faa3a991 100644 --- a/tests/test-tree-controller.js +++ b/tests/test-tree-controller.js @@ -188,7 +188,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( equal(isleaf, true); index = this.treeController.postorderselect(7); - var isleaf = this.treeController.isleaf(index); + isleaf = this.treeController.isleaf(index); equal(isleaf, false); }); @@ -302,8 +302,8 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( deepEqual(result, expected); this.treeController.unshear(); - var expected = [7, 5, 6, 1, 4, 2, 3]; - var result = [ + expected = [7, 5, 6, 1, 4, 2, 3]; + result = [ ...this.treeController.inOrderTraversal((includeRoot = true)), ]; deepEqual(result, expected); @@ -322,7 +322,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( equal(result, 11); this.treeController.unshear(); - var result = this.treeController.getTotalLength(2, 7); + result = this.treeController.getTotalLength(2, 7); equal(result, 11); }); From b04382664c3f2522859a72c7821fd6a9f426ef17 Mon Sep 17 00:00:00 2001 From: kwcantrell Date: Thu, 18 Mar 2021 17:55:29 -0700 Subject: [PATCH 09/63] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit merged @ElDeveloper's suggestions Co-authored-by: Yoshiki Vázquez Baeza --- empress/support_files/js/bp-tree.js | 4 ++-- empress/support_files/js/empress.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index 6876e8da6..b0c7b2d7d 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -980,9 +980,9 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { * * @param {Set} keepTips The set of tip names to keep. * - * @return {Object} An object containing the new tree and two maps that + * @return {Object} An object containing the new tree ("tree") and two maps that * convert the original postorder positions to the sheared - * tree postorder positions and vice-versa. + * tree postorder positions ("newToOld") and vice-versa ("oldToNew"). */ BPTree.prototype.shear = function (keepTips) { // closure diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 8cd33c9a8..0752d442b 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -407,7 +407,7 @@ define([ checkLengthsChange ); this._yrscf = data.yScalingFactor; - for (i of this._tree.postorderTraversal((includeRoot = true))) { + for (i of this._tree.postorderTraversal(includeRoot = true)) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; From f59892820539e5f9c23549998bfb89601686b169 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 18 Mar 2021 17:57:42 -0700 Subject: [PATCH 10/63] fix style --- empress/support_files/js/empress.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 0752d442b..8cd33c9a8 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -407,7 +407,7 @@ define([ checkLengthsChange ); this._yrscf = data.yScalingFactor; - for (i of this._tree.postorderTraversal(includeRoot = true)) { + for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; From 5ca58095277a28944732596ebb484273f69c4aea Mon Sep 17 00:00:00 2001 From: kcantrel Date: Tue, 23 Mar 2021 14:43:55 -0700 Subject: [PATCH 11/63] addressed comments --- empress/support_files/js/bp-tree.js | 13 +- empress/support_files/js/empress.js | 17 ++- empress/support_files/js/tree-controller.js | 141 +++++++++++--------- tests/test-bp-tree.js | 92 +++++++++++-- tests/test-tree-controller.js | 68 +++++----- 5 files changed, 207 insertions(+), 124 deletions(-) diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index b0c7b2d7d..a9077da94 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -1024,8 +1024,8 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { } var newBitArray = []; - var newToOld = {}; - var oldToNew = {}; + var shearedToFull = new Map(); + var fullToSheared = new Map(); var postorderPos = 1; for (i = 0; i < mask.length; i++) { if (mask[i] !== undefined) { @@ -1038,13 +1038,14 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { if (mask[i] === 0) { names.push(this.name(i)); lengths.push(this.length(i)); - newToOld[postorderPos] = this.postorder(i); - oldToNew[this.postorder(i)] = postorderPos++; + shearedToFull.set(postorderPos, this.postorder(i)); + fullToSheared.set(this.postorder(i), postorderPos); + postorderPos += 1; } } return { - newToOld: newToOld, - oldToNew: oldToNew, + shearedToFull: shearedToFull, + fullToSheared: fullToSheared, tree: new BPTree(newBitArray, names, lengths, null), }; }; diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 8cd33c9a8..18fef6e25 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -387,12 +387,12 @@ define([ ); var lengthGetter = LayoutsUtil.getLengthMethod( branchMethod, - this._tree.model.currentTree + this._tree.getTree() ); // Rectangular if (this._currentLayout === "Rectangular") { data = LayoutsUtil.rectangularLayout( - this._tree.model.currentTree, + this._tree.getTree(), 4020, 4020, // since lengths for "ignoreLengths" are set by `lengthGetter`, @@ -417,11 +417,12 @@ define([ this._treeData[i][this._tdToInd.highestchildyr] = data.highestChildYr[j]; this._treeData[i][this._tdToInd.lowestchildyr] = - data.lowestChildYr[j++]; + data.lowestChildYr[j]; + j += 1; } } else if (this._currentLayout === "Circular") { data = LayoutsUtil.circularLayout( - this._tree.model.currentTree, + this._tree.getTree(), 4020, 4020, this.leafSorting, @@ -444,11 +445,12 @@ define([ this._treeData[i][this._tdToInd.arcstartangle] = data.arcStartAngle[j]; this._treeData[i][this._tdToInd.arcendangle] = - data.arcEndAngle[j++]; + data.arcEndAngle[j]; + j += 1; } } else { data = LayoutsUtil.unrootedLayout( - this._tree.model.currentTree, + this._tree.getTree(), 4020, 4020, undefined, @@ -461,7 +463,8 @@ define([ // store new layout information this._treeData[i][this._tdToInd.x2] = data.xCoord[j]; - this._treeData[i][this._tdToInd.y2] = data.yCoord[j++]; + this._treeData[i][this._tdToInd.y2] = data.yCoord[j]; + j += 1; } } this._drawer.loadTreeCoordsBuff(this.getTreeCoords()); diff --git a/empress/support_files/js/tree-controller.js b/empress/support_files/js/tree-controller.js index 6cc549c59..b09c8c2a1 100644 --- a/empress/support_files/js/tree-controller.js +++ b/empress/support_files/js/tree-controller.js @@ -1,37 +1,41 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { function TreeModel(tree) { - this.currentTree = tree; - this.originalTree = tree; - this.curToOrig = {}; - this.origToCur = {}; + this.shearedTree = tree; + this.fullTree = tree; + this.shearedToFull = new Map(); + this.fullToSheared = new Map(); // initialize - for (var i = 1; i <= this.currentTree.size; i++) { - this.origToCur[i] = i; - this.curToOrig[i] = i; + for (var i = 1; i <= this.shearedTree.size; i++) { + this.fullToSheared.set(i, i); + this.shearedToFull.set(i, i); } } + TreeModel.prototype.getTree = function () { + return this.shearedTree; + }; + TreeModel.prototype.shear = function (tips) { - var result = this.originalTree.shear(tips); - this.currentTree = result.tree; - this.curToOrig = result.newToOld; - this.origToCur = result.oldToNew; + var result = this.fullTree.shear(tips); + this.shearedTree = result.tree; + this.shearedToFull = result.shearedToFull; + this.fullToSheared = result.fullToSheared; }; TreeModel.prototype.unshear = function () { - this.currentTree = this.originalTree; - for (var i = 1; i <= this.currentTree.size; i++) { - this.origToCur[i] = i; - this.curToOrig[i] = i; + this.shearedTree = this.fullTree; + for (var i = 1; i <= this.shearedTree.size; i++) { + this.fullToSheared.set(i, i); + this.shearedToFull.set(i, i); } }; TreeModel.prototype.postorderTraversal = function* (includeRoot = false) { var nodes = [], i; - for (i = 1; i <= Object.keys(this.curToOrig).length; i++) { - nodes.push(this.curToOrig[i]); + for (i = 1; i <= this.shearedToFull.size; i++) { + nodes.push(this.shearedToFull.get(i)); } if (!includeRoot) { nodes.pop(); @@ -71,9 +75,18 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @constructs TreeController */ this.model = new TreeModel(tree); - this.size = this.model.originalTree.size; + this.size = this.model.fullTree.size; } + /** + * Returns the current (sheared) tree + * + * @return {BPTree} + */ + TreeController.prototype.getTree = function () { + return this.model.getTree(); + }; + /** * Removes nodes from the original tree until only the nodes found in tips * and there ancestors remain in the tree. @@ -92,7 +105,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { }; /** - * Returns an iterator for nodes in a post order traversal of the current + * Returns an iterator for nodes in a post order traversal of the sheared * tree. * * Note: This method will use the topology of the currect tree but will @@ -108,14 +121,14 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { /** * Returns an Object describing the minimum, maximum, and average of all - * non-root node lengths in the current tree. + * non-root node lengths in the sheared tree. * * @return {Object} Contains three keys: "min", "max", and "avg", mapping * to Numbers representing the minimum, maximum, and * average non-root node length in the tree. */ TreeController.prototype.getLengthStats = function () { - return this.model.currentTree.getLengthStats(); + return this.model.shearedTree.getLengthStats(); }; /** @@ -130,23 +143,23 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return{String} */ TreeController.prototype.name = function (i) { - return this.model.originalTree.name(i); + return this.model.fullTree.name(i); }; /** - * Returns an array of all node names in current tree. + * Returns an array of all node names in sheared tree. */ TreeController.prototype.getAllNames = function () { - return this.model.currentTree.getAllNames(); + return this.model.shearedTree.getAllNames(); }; /** - * Returns the number of leaf nodes in current tree + * Returns the number of leaf nodes in sheared tree * * @return {Number} */ TreeController.prototype.numleaves = function () { - return this.model.currentTree.numleaves(); + return this.model.shearedTree.numleaves(); }; /** @@ -160,7 +173,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return{Number} */ TreeController.prototype.length = function (i) { - return this.model.originalTree.length(i); + return this.model.fullTree.length(i); }; /** @@ -177,7 +190,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return{Number} */ TreeController.prototype.parent = function (i) { - return this.model.originalTree.parent(i); + return this.model.fullTree.parent(i); }; /** @@ -188,7 +201,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Number} */ TreeController.prototype.root = function () { - return this.model.originalTree.root(); + return this.model.fullTree.root(); }; /** @@ -202,12 +215,12 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Boolean} */ TreeController.prototype.isleaf = function (i) { - return this.model.originalTree.isleaf(i); + return this.model.fullTree.isleaf(i); }; /** * This method is used in fchild, lchild, nsibling, and psibling and it what - * allows TreeController to use the topology of the current tree but return + * allows TreeController to use the topology of the sheared tree but return * the results w.r.t the original tree. * * @param{Number} i The index correspond to a node in the ORIGINAL tree. @@ -218,21 +231,21 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { */ TreeController.prototype._curToOrigNodeFunction = function (i, func) { - var curTree = this.model.currentTree; - var origTree = this.model.originalTree; + var curTree = this.model.shearedTree; + var origTree = this.model.fullTree; var node = curTree.postorderselect( - this.model.origToCur[origTree.postorder(i)] + this.model.fullToSheared.get(origTree.postorder(i)) ); node = curTree.postorder(curTree[func](node)); - node = origTree.postorderselect(this.model.curToOrig[node]); + node = origTree.postorderselect(this.model.shearedToFull.get(node)); return node; }; /** * Returns the opening index of first child of the node represented by i. - * This method will use the topology of the current (sheared) tree but its + * This method will use the topology of the sheared (sheared) tree but its * input and output will be w.r.t the ORGINAL tree. * * Note: The input of this method should the result of either preorderselect @@ -248,7 +261,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { /** * Returns the opening index of last child of the node represented by i. - * This method will use the topology of the current (sheared) tree but its + * This method will use the topology of the sheared (sheared) tree but its * input and output will be w.r.t the ORGINAL tree. * * Note: The input of this method should the result of either preorderselect @@ -264,7 +277,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { /** * Returns the opening index of next sibling of the node represented by i. - * This method will use the topology of the current (sheared) tree but its + * This method will use the topology of the sheared (sheared) tree but its * input and output will be w.r.t the ORGINAL tree. * * Note: The input of this method should the result of either preorderselect @@ -280,7 +293,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { /** * Returns the opening index of previous sibling of the node represented by - * i. This method will use the topology of the current (sheared) tree but + * i. This method will use the topology of the sheared (sheared) tree but * its input and output will be w.r.t the ORGINAL tree. * * Note: The input of this method should the result of either preorderselect @@ -305,7 +318,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Number} The postorder rank of index i */ TreeController.prototype.postorder = function (i) { - return this.model.originalTree.postorder(i); + return this.model.fullTree.postorder(i); }; /** @@ -317,7 +330,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Number} The index position of the node in the tree */ TreeController.prototype.postorderselect = function (k) { - return this.model.originalTree.postorderselect(k); + return this.model.fullTree.postorderselect(k); }; /** @@ -331,7 +344,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Number} The preorder rank of index i */ TreeController.prototype.preorder = function (i) { - return this.model.originalTree.preorder(i); + return this.model.fullTree.preorder(i); }; /** @@ -343,11 +356,11 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Number} The index position of the node in the tree */ TreeController.prototype.preorderselect = function (k) { - return this.model.originalTree.preorderselect(k); + return this.model.fullTree.preorderselect(k); }; /** - * Returns an iterator for nodes in an in-order traversal of the current + * Returns an iterator for nodes in an in-order traversal of the sheared * tree. * * Note: This method will use the topology of the currect tree but will @@ -358,11 +371,9 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { TreeController.prototype.inOrderTraversal = function* ( includeRoot = false ) { - var inOrderNodes = this.model.currentTree.inOrderNodes(); - if (Object.keys(this.model.curToOrig).length !== 0) { - for (var i = 0; i < inOrderNodes.length; i++) { - inOrderNodes[i] = this.model.curToOrig[inOrderNodes[i]]; - } + var inOrderNodes = this.model.shearedTree.inOrderNodes(); + for (var i = 0; i < inOrderNodes.length; i++) { + inOrderNodes[i] = this.model.shearedToFull.get(inOrderNodes[i]); } if (!includeRoot) { inOrderNodes.shift(); @@ -372,7 +383,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { /** * Finds the sum of lengths from start to end. This method will use the - * topology of the current tree but its input must be w.r.t the ORIGINAL + * topology of the sheared tree but its input must be w.r.t the ORIGINAL * tree. * * Note: start must be a descendant of end. An error will be thrown if start @@ -392,14 +403,14 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { end, ignoreLengths ) { - start = this.model.origToCur[start]; - end = this.model.origToCur[end]; - return this.model.currentTree.getTotalLength(start, end, ignoreLengths); + start = this.model.fullToSheared.get(start); + end = this.model.fullToSheared.get(end); + return this.model.shearedTree.getTotalLength(start, end, ignoreLengths); }; /** * Retrieve the tips in the subtree of a given (internal) node key. This - * method will use the topology of the current tree but its input/output + * method will use the topology of the sheared tree but its input/output * will be w.r.t the ORIGINAL tree. * * @param {Number} nodeKey The post-order position of a node in the ORIGINAL @@ -408,17 +419,17 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Array} tips Tips of the subtree. */ TreeController.prototype.findTips = function (nodeKey) { - nodeKey = this.model.origToCur[nodeKey]; - var tips = this.model.currentTree.findTips(nodeKey); + nodeKey = this.model.fullToSheared.get(nodeKey); + var tips = this.model.shearedTree.findTips(nodeKey); for (var i = 0; i < tips.length; i++) { - tips[i] = this.model.curToOrig[tips[i]]; + tips[i] = this.model.shearedToFull.get(tips[i]); } return tips; }; /** * Retrieve number of tips in the subtree of a given node. This method will - * use the topology of the current tree but its input must be w.r.t the + * use the topology of the sheared tree but its input must be w.r.t the * ORIGINAL tree. * * @param {Integer} nodeKey The postorder position of a node in the ORIGINAL @@ -427,24 +438,24 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Integer} The number of tips on the subtree rooted at nodeKey. */ TreeController.prototype.getNumTips = function (nodeKey) { - nodeKey = this.model.origToCur[nodeKey]; - return this.model.currentTree.getNumTips(nodeKey); + nodeKey = this.model.fullToSheared.get(nodeKey); + return this.model.shearedTree.getNumTips(nodeKey); }; /** - * Checks to see if name is in the current tree. + * Checks to see if name is in the sheared tree. * * @param {String} name The name to search for. * * @return {Boolean} If the name is in the tree. */ TreeController.prototype.containsNode = function (name) { - return this.model.currentTree.containsNode(name); + return this.model.shearedTree.containsNode(name); }; /** * Returns all nodes with a given name. This method will use the topology - * of the current tree but its output will be w.r.t the ORIGINAL tree. + * of the sheared tree but its output will be w.r.t the ORIGINAL tree. * * @param {String} name The name of the node(s) * @@ -453,9 +464,9 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * an empty array. */ TreeController.prototype.getNodesWithName = function (name) { - var nodes = this.model.currentTree.getNodesWithName(name); + var nodes = this.model.shearedTree.getNodesWithName(name); for (var i = 0; i < nodes.length; i++) { - nodes[i] = this.model.curToOrig[nodes[i]]; + nodes[i] = this.model.shearedToFull.get(nodes[i]); } return nodes; }; diff --git a/tests/test-bp-tree.js b/tests/test-bp-tree.js index 49b67bce6..2d8c56eaf 100644 --- a/tests/test-bp-tree.js +++ b/tests/test-bp-tree.js @@ -929,8 +929,32 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { ); var keep = new Set(["4", "6", "7", "10", "11"]); - var result = preShearBPTree.shear(keep).tree; - deepEqual(result.b_, [ + var shearedToFull = new Map([ + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10], + [10, 11], + ]); + var fullToSheared = new Map([ + [2, 1], + [3, 2], + [4, 3], + [5, 4], + [6, 5], + [7, 6], + [8, 7], + [9, 8], + [10, 9], + [11, 10], + ]); + var result = preShearBPTree.shear(keep); + deepEqual(result.tree.b_, [ 1, 1, 1, @@ -952,7 +976,7 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { 0, 0, ]); - deepEqual(result.names_, [ + deepEqual(result.tree.names_, [ null, "4", "6", @@ -965,19 +989,63 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { "8", "r", ]); - deepEqual(result.lengths_, [null, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + deepEqual(result.tree.lengths_, [ + null, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + ]); + deepEqual(result.shearedToFull, shearedToFull); + deepEqual(result.fullToSheared, fullToSheared); keep = new Set(["7", "10", "11"]); - result = preShearBPTree.shear(keep).tree; - deepEqual(result.b_, [1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0]); - deepEqual(result.names_, [null, "7", "10", "11", "9", "8", "r"]); - deepEqual(result.lengths_, [null, 6, 7, 8, 9, 10, 11]); + shearedToFull = new Map([ + [1, 6], + [2, 7], + [3, 8], + [4, 9], + [5, 10], + [6, 11], + ]); + fullToSheared = new Map([ + [6, 1], + [7, 2], + [8, 3], + [9, 4], + [10, 5], + [11, 6], + ]); + result = preShearBPTree.shear(keep); + deepEqual(result.tree.b_, [1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0]); + deepEqual(result.tree.names_, [ + null, + "7", + "10", + "11", + "9", + "8", + "r", + ]); + deepEqual(result.tree.lengths_, [null, 6, 7, 8, 9, 10, 11]); + deepEqual(result.shearedToFull, shearedToFull); + deepEqual(result.fullToSheared, fullToSheared); keep = new Set([]); - result = preShearBPTree.shear(keep).tree; - deepEqual(result.b_, [1, 0]); - deepEqual(result.names_, [null, "r"]); - deepEqual(result.lengths_, [null, 11]); + shearedToFull = new Map([[1, 11]]); + fullToSheared = new Map([[11, 1]]); + result = preShearBPTree.shear(keep); + deepEqual(result.tree.b_, [1, 0]); + deepEqual(result.tree.names_, [null, "r"]); + deepEqual(result.tree.lengths_, [null, 11]); + deepEqual(result.shearedToFull, shearedToFull); + deepEqual(result.fullToSheared, fullToSheared); }); }); }); diff --git a/tests/test-tree-controller.js b/tests/test-tree-controller.js index 8faa3a991..c511a6260 100644 --- a/tests/test-tree-controller.js +++ b/tests/test-tree-controller.js @@ -29,61 +29,61 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( // checks to make sure correct names are kept var shearNames = [null, "t2", "t3", "i4", "i5", "r"]; - var resutlNames = this.treeController.model.currentTree.names_; + var resutlNames = this.treeController.model.shearedTree.names_; deepEqual(resutlNames, shearNames); var shearLengths = [null, 2, 3, 4, 5, null]; - var resultLengts = this.treeController.model.currentTree.lengths_; + var resultLengts = this.treeController.model.shearedTree.lengths_; deepEqual(resultLengts, shearLengths); // checks to make sure structre of tree is correct var shearTree = [1, 1, 1, 1, 0, 1, 0, 0, 0, 0]; - var resultTree = this.treeController.model.currentTree.b_; + var resultTree = this.treeController.model.shearedTree.b_; deepEqual(resultTree, shearTree); // checks to make sure the mappings from orignal tree to shear tree // is correct and vice-versa - var origToCur = { - "2": 1, - "3": 2, - "4": 3, - "5": 4, - "7": 5, - }; - var curToOrig = { - "1": 2, - "2": 3, - "3": 4, - "4": 5, - "5": 7, - }; - var resultOrigToCur = this.treeController.model.origToCur; - var resultCurToOrig = this.treeController.model.curToOrig; - deepEqual(resultOrigToCur, origToCur); - deepEqual(resultCurToOrig, curToOrig); + var fullToSheared = new Map([ + [2, 1], + [3, 2], + [4, 3], + [5, 4], + [7, 5], + ]); + var shearedToFull = new Map([ + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 7], + ]); + var resultOrigToCur = this.treeController.model.fullToSheared; + var resultCurToOrig = this.treeController.model.shearedToFull; + deepEqual(resultOrigToCur, fullToSheared); + deepEqual(resultCurToOrig, shearedToFull); }); test("Test unshear", function () { this.treeController.shear(new Set(["t2", "t3"])); this.treeController.unshear(); - deepEqual(this.treeController.model.currentTree.names_, this.names); + deepEqual(this.treeController.model.shearedTree.names_, this.names); deepEqual( - this.treeController.model.currentTree.lengths_, + this.treeController.model.shearedTree.lengths_, this.lengths ); - var map = { - "1": 1, - "2": 2, - "3": 3, - "4": 4, - "5": 5, - "6": 6, - "7": 7, - }; - deepEqual(this.treeController.model.curToOrig, map); - deepEqual(this.treeController.model.origToCur, map); + var map = new Map([ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + ]); + deepEqual(this.treeController.model.shearedToFull, map); + deepEqual(this.treeController.model.fullToSheared, map); }); test("Test postorderTraversal", function () { From 89a4326f8ebb9ed7c5dd15a8944c1d0a075c1a66 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Tue, 23 Mar 2021 14:46:34 -0700 Subject: [PATCH 12/63] removed shear --- empress/support_files/js/empress.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 18fef6e25..a0424d231 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -3565,21 +3565,5 @@ define([ } }; - Empress.prototype.shear = function (cat, value) { - var nodeNames = this._tree.getAllNames(); - - var fmInfo = this.getUniqueFeatureMetadataInfo(cat, "tip"); - var sortedUniqueValues = fmInfo.sortedUniqueValues; - var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; - // convert observation IDs to _treeData keys. Notably, this includes - // converting the values of uniqueValueToFeatures from Arrays to Sets. - var obs = uniqueValueToFeatures[value]; - var tipNames = []; - for (var i of obs.values()) { - tipNames.push(this._tree.name(this._tree.postorderselect(i))); - } - this._tree.shear(new Set(tipNames)); - }; - return Empress; }); From 654df69d64b9371c5b1be53d53095cf9fa4f86bc Mon Sep 17 00:00:00 2001 From: kcantrel Date: Tue, 23 Mar 2021 18:25:47 -0700 Subject: [PATCH 13/63] start ui --- empress/support_files/js/empress.js | 16 ++++++++++++ tests/python/make-dev-page.py | 38 ++++++++++++++--------------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index a0424d231..18fef6e25 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -3565,5 +3565,21 @@ define([ } }; + Empress.prototype.shear = function (cat, value) { + var nodeNames = this._tree.getAllNames(); + + var fmInfo = this.getUniqueFeatureMetadataInfo(cat, "tip"); + var sortedUniqueValues = fmInfo.sortedUniqueValues; + var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; + // convert observation IDs to _treeData keys. Notably, this includes + // converting the values of uniqueValueToFeatures from Arrays to Sets. + var obs = uniqueValueToFeatures[value]; + var tipNames = []; + for (var i of obs.values()) { + tipNames.push(this._tree.name(this._tree.postorderselect(i))); + } + this._tree.shear(new Set(tipNames)); + }; + return Empress; }); diff --git a/tests/python/make-dev-page.py b/tests/python/make-dev-page.py index 2f7341079..a86c8e95e 100755 --- a/tests/python/make-dev-page.py +++ b/tests/python/make-dev-page.py @@ -58,8 +58,8 @@ def main(tree, table, sample_metadata, feature_metadata, ordination, feature_metadata = q2.Artifact.load( feature_metadata).view(q2.Metadata) - if ordination is not None: - ordination = q2.Artifact.load(ordination) + # if ordination is not None: + # ordination = q2.Artifact.load(ordination) else: raise ValueError('Tree, table and sample metadata are required!') @@ -70,23 +70,23 @@ def main(tree, table, sample_metadata, feature_metadata, ordination, sample_metadata = sample_metadata.to_dataframe() feature_metadata = feature_metadata.to_dataframe() - if ordination is not None: - ordination = ordination.view(OrdinationResults) - - if ordination.features is not None: - # select the top N most important features based on the vector's - # magnitude (coped from q2-emperor) - feats = ordination.features.copy() - # in cases where the the axes are all zero there might be all-NA - # columns - feats.fillna(0, inplace=True) - origin = np.zeros_like(feats.columns) - feats['importance'] = feats.apply(euclidean, axis=1, - args=(origin,)) - feats.sort_values('importance', inplace=True, ascending=False) - feats.drop(['importance'], inplace=True, axis=1) - ordination.features = feats[:number_of_features].copy() - + # if ordination is not None: + # ordination = ordination.view(OrdinationResults) + + # if ordination.features is not None: + # # select the top N most important features based on the vector's + # # magnitude (coped from q2-emperor) + # feats = ordination.features.copy() + # # in cases where the the axes are all zero there might be all-NA + # # columns + # feats.fillna(0, inplace=True) + # origin = np.zeros_like(feats.columns) + # feats['importance'] = feats.apply(euclidean, axis=1, + # args=(origin,)) + # feats.sort_values('importance', inplace=True, ascending=False) + # feats.drop(['importance'], inplace=True, axis=1) + # ordination.features = feats[:number_of_features].copy() + ordination = None # These two lines fetch the JS files for both apps directly from the # installation directory - this makes testing/developing easier empress_resources = pkg_resources.resource_filename('empress', From d6aa515f29112de80b9b40dba44b0d86daa82511 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 25 Mar 2021 12:54:46 -0700 Subject: [PATCH 14/63] finished basic shear ui --- empress/support_files/css/empress.css | 18 ++ empress/support_files/js/empress.js | 210 ++++++++++++----- empress/support_files/js/legend.js | 9 - empress/support_files/js/shear-panel.js | 214 ++++++++++++++++++ .../support_files/js/side-panel-handler.js | 23 +- empress/support_files/js/tree-controller.js | 29 +-- .../templates/empress-template.html | 16 +- .../support_files/templates/side-panel.html | 28 +++ tests/python/make-dev-page.py | 39 ++-- 9 files changed, 488 insertions(+), 98 deletions(-) create mode 100644 empress/support_files/js/shear-panel.js diff --git a/empress/support_files/css/empress.css b/empress/support_files/css/empress.css index 7c792e172..4971d1fdb 100644 --- a/empress/support_files/css/empress.css +++ b/empress/support_files/css/empress.css @@ -585,6 +585,24 @@ p.side-header button:hover, white-space: nowrap; } +.shear-layer-legend div{ + max-height: 15em; + /* The side panel width is 450px */ + max-width: 430px; + overflow: auto; +} + +/*.shear-layer-legend label { + display: inline-block; + vertical-align: top; + width: 200px; +}*/ + +/*.shear-layer-legend p { + margin:0; + padding:0; +}*/ + .barplot-layer-legend { max-height: 15em; /* The side panel width is 450px */ diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 18fef6e25..c35ff6a86 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -389,23 +389,36 @@ define([ branchMethod, this._tree.getTree() ); + + // Rectangular if (this._currentLayout === "Rectangular") { - data = LayoutsUtil.rectangularLayout( - this._tree.getTree(), - 4020, - 4020, - // 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, - checkLengthsChange - ); + // tree is just root + if (this._tree.currentSize == 1) { + data = { + xCoord: [null, 0], + yCoord: [null, 0], + highestChildYr: [null, 0], + lowestChildYr: [null, 0], + yScalingFactor: [null, 0] + } + } else { + data = LayoutsUtil.rectangularLayout( + this._tree.getTree(), + 4020, + 4020, + // 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, + checkLengthsChange + ); + } this._yrscf = data.yScalingFactor; for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information @@ -421,15 +434,30 @@ define([ j += 1; } } else if (this._currentLayout === "Circular") { - data = LayoutsUtil.circularLayout( - this._tree.getTree(), - 4020, - 4020, - this.leafSorting, - undefined, - lengthGetter, - checkLengthsChange - ); + if (this._tree.currentSize == 1) { + data = { + // store new layout information + x0: [null, 0], + y0: [null, 0], + x1: [null, 0], + y1: [null, 0], + angle: [null, 0], + arcx0: [null, 0], + arcy0: [null, 0], + arcStartAngle: [null, 0], + arcEndAngle: [null, 0], + } + } else { + data = LayoutsUtil.circularLayout( + this._tree.getTree(), + 4020, + 4020, + this.leafSorting, + undefined, + lengthGetter, + checkLengthsChange + ); + } for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; @@ -449,14 +477,22 @@ define([ j += 1; } } else { - data = LayoutsUtil.unrootedLayout( - this._tree.getTree(), - 4020, - 4020, - undefined, - lengthGetter, - checkLengthsChange - ); + if (this._tree.currentSize == 1) { + data = { + // store new layout information + xCoord: [null, 0], + yCoord: [null, 0], + } + } else { + data = LayoutsUtil.unrootedLayout( + this._tree.getTree(), + 4020, + 4020, + undefined, + lengthGetter, + checkLengthsChange + ); + } for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; @@ -2142,6 +2178,18 @@ define([ this.drawTree(); }; + /** + * + */ + Empress.prototype.getUniqueSampleMetadataInfo = function(cat) { + var obs = this._biom.getObsBy(cat); + var nodes = new Set([...this._tree.postorderTraversal()]); + _.each(obs, function (observations, key) { + obs[key] = observations.filter(x => nodes.has(x)); + }); + return obs; + }; + /** * Color the tree using sample metadata * @@ -2162,7 +2210,8 @@ define([ reverse = false ) { var tree = this._tree; - var obs = this._biom.getObsBy(cat); + // var obs = this._biom.getObsBy(cat); + var obs = this.getUniqueSampleMetadataInfo(cat); var categories = Object.keys(obs); // Assign colors to categories @@ -2177,6 +2226,11 @@ define([ var cm = colorer.getMapRGB(); // colors for the legend var keyInfo = colorer.getMapHex(); + for (var key in keyInfo) { + if (obs[key].length === 0) { + delete keyInfo[key]; + } + } // shared by the following for loops var i, j, category; @@ -2232,6 +2286,9 @@ define([ * an array of the node name(s) with each value. */ Empress.prototype.getUniqueFeatureMetadataInfo = function (cat, method) { + // get nodes in tree + var nodes = new Set([...this._tree.postorderTraversal()]); + // In order to access feature metadata for a given node, we need to // find the 0-based index in this._featureMetadataColumns that the // specified f.m. column corresponds to. (We *could* get around this by @@ -2258,15 +2315,14 @@ define([ var uniqueValueToFeatures = {}; _.each(fmObjs, function (mObj) { _.mapObject(mObj, function (fmRow, node) { - // need to convert to integer - node = parseInt(node); - // This is loosely based on how BIOMTable.getObsBy() works. var fmVal = fmRow[fmIdx]; - if (_.has(uniqueValueToFeatures, fmVal)) { - uniqueValueToFeatures[fmVal].push(node); - } else { - uniqueValueToFeatures[fmVal] = [node]; + if (!_.has(uniqueValueToFeatures, fmVal)) { + uniqueValueToFeatures[fmVal] = []; } + // need to convert to integer + node = parseInt(node); + if (!nodes.has(node)) {return}; + uniqueValueToFeatures[fmVal].push(node); }); }); @@ -2329,9 +2385,14 @@ define([ ); // colors for drawing the tree var cm = colorer.getMapRGB(); + // colors for the legend var keyInfo = colorer.getMapHex(); - + for (var key in keyInfo) { + if (uniqueValueToFeatures[key].length === 0) { + delete keyInfo[key]; + } + } // Do upwards propagation only if the coloring method is "tip" if (method === "tip") { obs = this._projectObservations(obs, false); @@ -3564,21 +3625,66 @@ define([ return this._tree.length(this._tree.postorderselect(nodeKey)); } }; + + /** + * This will shear/unshear + */ + Empress.prototype.shear = function (shearMap) { + this._tree.unshear(); + var scope = this; + var removeNodes = new Set(); + + shearMap.forEach(function(values, cat) { + var fmInfo = scope.getUniqueFeatureMetadataInfo(cat, "tip"); + var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; + console.log("Test",uniqueValueToFeatures) + _.each(values, function(val) { + var obs = uniqueValueToFeatures[val]; + for(var node of obs) { + removeNodes.add(node); + } + }); + }); + + + // remove removeNodes + var allNodes = Array.from(Array(this._tree.size + 1).keys()); + allNodes.shift(); + + allNodes = new Set(allNodes); + var keepNodes = new Set([...allNodes].filter(x => !removeNodes.has(x))); - Empress.prototype.shear = function (cat, value) { + var keepNames = [] + for(var node of keepNodes) { + var name = this._tree.name(this._tree.postorderselect(node)) + keepNames.push(name); + } + + this._tree.shear(new Set(keepNames)); var nodeNames = this._tree.getAllNames(); + // Don't include nodes with the name null (i.e. nodes without a + // specified name in the Newick file) in the auto-complete. + nodeNames = nodeNames.filter((n) => n !== null); + this._events.autocomplete(nodeNames); - var fmInfo = this.getUniqueFeatureMetadataInfo(cat, "tip"); - var sortedUniqueValues = fmInfo.sortedUniqueValues; - var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; - // convert observation IDs to _treeData keys. Notably, this includes - // converting the values of uniqueValueToFeatures from Arrays to Sets. - var obs = uniqueValueToFeatures[value]; - var tipNames = []; - for (var i of obs.values()) { - tipNames.push(this._tree.name(this._tree.postorderselect(i))); + this.getLayoutInfo(); + + // if (this._currentLayout !== "Unrooted") { + // this.drawBarplots(); + // } + // Undraw or redraw barplots as needed (assuming barplots are supported + // in the first place, of course; if no feature or sample metadata at + // all was passed then barplots are not available :() + if (!_.isNull(this._barplotPanel)) { + var supported = this._barplotPanel.updateLayoutAvailability( + this._currentLayout + ); + if (!supported && this._barplotsDrawn) { + this.undrawBarplots(); + } else if (supported && this._barplotPanel.enabled) { + this.drawBarplots(); + } } - this._tree.shear(new Set(tipNames)); }; return Empress; diff --git a/empress/support_files/js/legend.js b/empress/support_files/js/legend.js index 1c70308d6..b10e3ae29 100644 --- a/empress/support_files/js/legend.js +++ b/empress/support_files/js/legend.js @@ -218,17 +218,8 @@ define(["jquery", "underscore", "util"], function ($, _, util) { * @param {Object} info Color key information. This should map unique * values (e.g. in sample or feature metadata) to * their assigned color, expressed in hex format. - * - * @throws {Error} If info has no keys. This check is done before anything - * else is done in this function. */ Legend.prototype.addCategoricalKey = function (name, info) { - if (_.isEmpty(info)) { - throw new Error( - "Can't create a categorical legend when there are no " + - "categories in the info" - ); - } this.clear(); this.addTitle(name); this._sortedCategories = util.naturalSort(_.keys(info)); diff --git a/empress/support_files/js/shear-panel.js b/empress/support_files/js/shear-panel.js new file mode 100644 index 000000000..c3aff0e84 --- /dev/null +++ b/empress/support_files/js/shear-panel.js @@ -0,0 +1,214 @@ +define([ + "underscore", + "util", + "TreeController", +], function ( + _, + util, + TreeController +) { + function Layer( + fCol, + fVals, + container, + chkBxClickFunction, + removeClickFunction + ) { + this.fCol = fCol; + this.fVals = fVals; + this.container = container; + this.layerDiv = null; + + var scope = this; + + // create layer div + this.layerDiv = document.createElement("div"); + this.container.appendChild(this.layerDiv); + + // create checkbox legend div + var chkBoxLegendDiv = document.createElement("div"); + this.layerDiv.appendChild(chkBoxLegendDiv); + chkBoxLegendDiv.classList.add("shear-layer-legend"); + chkBoxLegendDiv.classList.add("legend"); + + // create chckbox legend title + var legendTitle = document.createElement("div"); + chkBoxLegendDiv.appendChild(legendTitle); + legendTitle.innerText = this.fCol; + legendTitle.classList.add("legend-title"); + + // create chcbox div + var legendChkBoxs = document.createElement("div"); + chkBoxLegendDiv.appendChild(legendChkBoxs); + + // create checkboxes + var table = document.createElement("table"); + legendChkBoxs.appendChild(table); + _.each(this.fVals, function(val) { + var row = document.createElement("tr"); + + // add checkbox + var dataCheck = document.createElement("td"); + var input = document.createElement("INPUT"); + input.setAttribute("type", "checkbox"); + input.checked = true; + input.onchange = function(){ + chkBxClickFunction( + !input.checked, + scope.fCol, + val + ); + }; + dataCheck.appendChild(input); + row.appendChild(dataCheck); + + // add checkbox label + var dataLabel = document.createElement("td"); + dataLabel.innerText = val; + row.appendChild(dataLabel); + + // add row to table + table.appendChild(row); + }); + + // create remove container + var removeContainer = document.createElement("p"); + this.layerDiv.appendChild(removeContainer); + + // create remove label + var removeLabel = document.createElement("label"); + removeLabel.innerText = "Remove this layer"; + removeContainer.appendChild(removeLabel); + + // create remove button + var removeButton = document.createElement("button"); + removeButton.innerText = "-"; + removeButton.onclick = function() { + removeClickFunction(scope.fCol); + scope.layerDiv.remove(); + scope.layerDiv = null; + } + removeContainer.appendChild(removeButton); + + // create border line + this.layerDiv.appendChild(document.createElement("hr")) + } + + function Model(empress, container) { + this.empress = empress; + this.layers = new Map(); + this.shearMap = new Map(); + this.container = container; + this.observers = []; + } + + Model.prototype.addLayer = function(fCol) { + var fVals = this.empress + .getUniqueFeatureMetadataInfo(fCol, "tip") + .sortedUniqueValues; + var layer = new Layer( + fCol, + fVals, + this.container, + (add, col, val) => {Model.addRemoveShearItem(this, add,col, val);}, + (col) => {Model.removeLayer(this, col);} + ) + this.layers.set(fCol, layer); + }; + + Model.prototype.getShearItem = function(fCol) { + return this.shearMap.get(fCol); + }; + + Model.prototype.hasLayer = function(fCol) { + return this.layers.has(fCol); + }; + + Model.prototype.notify = function() { + console.log("this", this); + this.empress.shear(this.shearMap); + this.empress.drawTree(); + _.each(this.observers, function(obs) { + obs.shearUpdate(); + }); + }; + + Model.prototype.registerObserver = function(obs) { + this.observers.push(obs); + }; + + Model.addRemoveShearItem = function(model, remove, col, val) { + if (remove) { + Model.addShearItem(model, col, val); + } else { + Model.removeShearItem(model, col, val); + } + } + + Model.removeLayer = function(model, fCol) { + model.layers.delete(fCol); + model.shearMap.delete(fCol); + model.notify(); + console.log(model) + }; + + Model.addShearItem = function(model, fCol, fVal) { + if(model.shearMap.has(fCol)) { + model.shearMap.get(fCol).push(fVal); + } else { + model.shearMap.set(fCol, [fVal]); + } + model.notify(); + }; + + Model.removeShearItem = function(model, fCol, fVal) { + var items = model.getShearItem(fCol); + if (items === undefined) {return;} + var index = items.indexOf(fVal) + if (index > -1) { + items.splice(index, 1); + } + model.notify() + }; + + + function Controller(empress, container) { + this.model = new Model(empress, container) + } + + Controller.prototype.addLayer = function(fCol) { + if(!this.model.hasLayer(fCol)) { + this.model.addLayer(fCol); + } + } + + Controller.prototype.registerObserver = function(obs) { + this.model.registerObserver(obs); + }; + + function Shearer(empress, fCols) { + this.fCols = fCols; + this.shearSelect = document.getElementById("shear-feature-select"); + this.addLayerButton = document.getElementById("shear-add-btn"); + this.shearContainer = document.getElementById("shear-legends"); + this.controller = new Controller(empress, this.shearContainer); + + var scope = this; + _.each(this.fCols, function(col) { + var opt = document.createElement("option"); + opt.innerText = col; + opt.value = col; + scope.shearSelect.appendChild(opt); + }); + + this.addLayerButton.onclick = function() { + scope.controller.addLayer(scope.shearSelect.value) + } + } + + Shearer.prototype.registerObserver = function(obs) { + this.controller.registerObserver(obs); + }; + + return Shearer; +}); \ No newline at end of file diff --git a/empress/support_files/js/side-panel-handler.js b/empress/support_files/js/side-panel-handler.js index bc8a475fc..0b3e226a5 100644 --- a/empress/support_files/js/side-panel-handler.js +++ b/empress/support_files/js/side-panel-handler.js @@ -316,6 +316,8 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { var col = this.sColor.value; var reverse = this.sReverseColor.checked; var keyInfo = this.empress.colorBySampleCat(colBy, col, reverse); + console.log("Mother Fuck", keyInfo) + if (keyInfo === null) { util.toastMsg( "No unique branches found for this metadata category" @@ -333,12 +335,20 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { var col = this.fColor.value; var coloringMethod = this.fMethodChk.checked ? "tip" : "all"; var reverse = this.fReverseColor.checked; - this.empress.colorByFeatureMetadata( + var keyInfo = this.empress.colorByFeatureMetadata( colBy, col, coloringMethod, reverse ); + console.log("Mother Fuck", keyInfo) + if (_.isEmpty(keyInfo)) { + util.toastMsg( + "No unique branches found for this metadata category" + ); + this.fUpdateBtn.classList.remove("hidden"); + return; + } }; /** @@ -450,6 +460,17 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { } }; + SidePanel.prototype.shearUpdate = function() { + console.log("????") + if (this.sChk.checked) { + this.sUpdateBtn.click() + } + + if (this.fChk.checked) { + this.fUpdateBtn.click(); + } + } + /** * Initializes exporting options. */ diff --git a/empress/support_files/js/tree-controller.js b/empress/support_files/js/tree-controller.js index b09c8c2a1..aca8716c7 100644 --- a/empress/support_files/js/tree-controller.js +++ b/empress/support_files/js/tree-controller.js @@ -76,6 +76,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { */ this.model = new TreeModel(tree); this.size = this.model.fullTree.size; + this.currentSize = this.model.shearedTree.size; } /** @@ -95,6 +96,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { */ TreeController.prototype.shear = function (tips) { this.model.shear(tips); + this.currentSize = this.model.shearedTree.size; }; /** @@ -102,6 +104,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { */ TreeController.prototype.unshear = function () { this.model.unshear(); + this.currentSize = this.model.shearedTree.size; }; /** @@ -219,8 +222,8 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { }; /** - * This method is used in fchild, lchild, nsibling, and psibling and it what - * allows TreeController to use the topology of the sheared tree but return + * This method is used in fchild, lchild, nsibling, and psibling and is what + * allows TreeController to use the topology of the sheared tree but returns * the results w.r.t the original tree. * * @param{Number} i The index correspond to a node in the ORIGINAL tree. @@ -230,16 +233,16 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return{Number} The result of func w.r.t the ORIGINAL tree. */ - TreeController.prototype._curToOrigNodeFunction = function (i, func) { - var curTree = this.model.shearedTree; - var origTree = this.model.fullTree; + TreeController.prototype._shearedToFullNodeFunction = function (i, func) { + var shearedTreeTree = this.model.shearedTree; + var fullTree = this.model.fullTree; - var node = curTree.postorderselect( - this.model.fullToSheared.get(origTree.postorder(i)) + var node = shearedTreeTree.postorderselect( + this.model.fullToSheared.get(fullTree.postorder(i)) ); - node = curTree.postorder(curTree[func](node)); - node = origTree.postorderselect(this.model.shearedToFull.get(node)); + node = shearedTreeTree.postorder(shearedTreeTree[func](node)); + node = fullTree.postorderselect(this.model.shearedToFull.get(node)); return node; }; @@ -256,7 +259,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Number} return 0 if i is a leaf node */ TreeController.prototype.fchild = function (i) { - return this._curToOrigNodeFunction(i, "fchild"); + return this._shearedToFullNodeFunction(i, "fchild"); }; /** @@ -272,7 +275,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Number} return 0 if i is a leaf node */ TreeController.prototype.lchild = function (i) { - return this._curToOrigNodeFunction(i, "lchild"); + return this._shearedToFullNodeFunction(i, "lchild"); }; /** @@ -288,7 +291,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Number} return 0 if i does not have a next sibling */ TreeController.prototype.nsibling = function (i) { - return this._curToOrigNodeFunction(i, "nsibling"); + return this._shearedToFullNodeFunction(i, "nsibling"); }; /** @@ -304,7 +307,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * @return {Number} return 0 if i does not have a previous sibling */ TreeController.prototype.psibling = function (i) { - return this._curToOrigNodeFunction(i, "psibling"); + return this._shearedToFullNodeFunction(i, "psibling"); }; /** diff --git a/empress/support_files/templates/empress-template.html b/empress/support_files/templates/empress-template.html index 8a933ebd3..c8ccabebc 100644 --- a/empress/support_files/templates/empress-template.html +++ b/empress/support_files/templates/empress-template.html @@ -115,7 +115,8 @@ 'util' : './js/util', 'LayoutsUtil': './js/layouts-util', 'ExportUtil': './js/export-util', - 'TreeController': './js/tree-controller' + 'TreeController': './js/tree-controller', + 'ShearPanel': './js/shear-panel' } }); @@ -124,19 +125,18 @@ 'SidePanel', 'AnimationPanel', 'Animator', 'BarplotLayer', 'BarplotPanel', 'BIOMTable', 'Empress', 'Legend', 'Colorer', 'VectorOps', 'CanvasEvents', 'SelectedNodeMenu', - 'util', 'LayoutsUtil', 'ExportUtil'], + 'util', 'LayoutsUtil', 'ExportUtil', 'ShearPanel'], function($, gl, chroma, underscore, spectrum, filesaver, ByteArray, BPTree, Camera, Drawer, SidePanel, AnimationPanel, Animator, BarplotLayer, BarplotPanel, BIOMTable, Empress, Legend, Colorer, VectorOps, CanvasEvents, SelectedNodeMenu, util, - LayoutsUtil, ExportUtil) { + LayoutsUtil, ExportUtil, ShearPanel) { // initialze the tree and model var tree = new BPTree( {{ tree }}, {{ names | tojson }}, {{ lengths | tojson }} ); - var fmCols = {{ feature_metadata_columns | tojson }}; var canvas = document.getElementById('tree-surface'); @@ -175,6 +175,14 @@ sPanel.addLayoutTab(); sPanel.addExportTab(); + // this.empress.getFeatureMetadataCategories(); + var TESTSHEAR = new ShearPanel( + empress, + empress.getFeatureMetadataCategories() + ); + TESTSHEAR.registerObserver(sPanel); + // TESTSHEAR.addLayer("Level 1"); + // Only show the sample metadata coloring / animation panels if a // feature table and sample metadata file were provided if (isCommunityPlot) { diff --git a/empress/support_files/templates/side-panel.html b/empress/support_files/templates/side-panel.html index 2a59552fe..86772df46 100644 --- a/empress/support_files/templates/side-panel.html +++ b/empress/support_files/templates/side-panel.html @@ -257,6 +257,34 @@ + + +
+

+ Shear tree using feature metadata +

+
+

+ + +

+

+ + +

+
+
+
+
+
+
+
+
+
+ diff --git a/tests/python/make-dev-page.py b/tests/python/make-dev-page.py index a86c8e95e..e5b0a907d 100755 --- a/tests/python/make-dev-page.py +++ b/tests/python/make-dev-page.py @@ -58,8 +58,8 @@ def main(tree, table, sample_metadata, feature_metadata, ordination, feature_metadata = q2.Artifact.load( feature_metadata).view(q2.Metadata) - # if ordination is not None: - # ordination = q2.Artifact.load(ordination) + if ordination is not None: + ordination = q2.Artifact.load(ordination) else: raise ValueError('Tree, table and sample metadata are required!') @@ -69,24 +69,25 @@ def main(tree, table, sample_metadata, feature_metadata, ordination, table = table.view(biom.Table) sample_metadata = sample_metadata.to_dataframe() feature_metadata = feature_metadata.to_dataframe() - - # if ordination is not None: - # ordination = ordination.view(OrdinationResults) - - # if ordination.features is not None: - # # select the top N most important features based on the vector's - # # magnitude (coped from q2-emperor) - # feats = ordination.features.copy() - # # in cases where the the axes are all zero there might be all-NA - # # columns - # feats.fillna(0, inplace=True) - # origin = np.zeros_like(feats.columns) - # feats['importance'] = feats.apply(euclidean, axis=1, - # args=(origin,)) - # feats.sort_values('importance', inplace=True, ascending=False) - # feats.drop(['importance'], inplace=True, axis=1) - # ordination.features = feats[:number_of_features].copy() + ordination = None + if ordination is not None: + ordination = ordination.view(OrdinationResults) + + if ordination.features is not None: + # select the top N most important features based on the vector's + # magnitude (coped from q2-emperor) + feats = ordination.features.copy() + # in cases where the the axes are all zero there might be all-NA + # columns + feats.fillna(0, inplace=True) + origin = np.zeros_like(feats.columns) + feats['importance'] = feats.apply(euclidean, axis=1, + args=(origin,)) + feats.sort_values('importance', inplace=True, ascending=False) + feats.drop(['importance'], inplace=True, axis=1) + ordination.features = feats[:number_of_features].copy() + # These two lines fetch the JS files for both apps directly from the # installation directory - this makes testing/developing easier empress_resources = pkg_resources.resource_filename('empress', From de01d93f22712160eaf6fb5a550e4a7df370709b Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 25 Mar 2021 13:01:56 -0700 Subject: [PATCH 15/63] fixed test issue --- empress/support_files/css/empress.css | 2 +- empress/support_files/js/empress.js | 39 +++---- .../js/{shear-panel.js => shearer.js} | 102 +++++++++--------- .../support_files/js/side-panel-handler.js | 9 +- .../templates/empress-template.html | 12 +-- tests/python/make-dev-page.py | 5 +- tests/test-legend.js | 6 -- 7 files changed, 79 insertions(+), 96 deletions(-) rename empress/support_files/js/{shear-panel.js => shearer.js} (69%) diff --git a/empress/support_files/css/empress.css b/empress/support_files/css/empress.css index 4971d1fdb..bdf91b521 100644 --- a/empress/support_files/css/empress.css +++ b/empress/support_files/css/empress.css @@ -585,7 +585,7 @@ p.side-header button:hover, white-space: nowrap; } -.shear-layer-legend div{ +.shear-layer-legend div { max-height: 15em; /* The side panel width is 450px */ max-width: 430px; diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index c35ff6a86..64dfc9fd7 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -390,18 +390,17 @@ define([ this._tree.getTree() ); - // Rectangular if (this._currentLayout === "Rectangular") { - // tree is just root + // tree is just root if (this._tree.currentSize == 1) { data = { xCoord: [null, 0], yCoord: [null, 0], highestChildYr: [null, 0], lowestChildYr: [null, 0], - yScalingFactor: [null, 0] - } + yScalingFactor: [null, 0], + }; } else { data = LayoutsUtil.rectangularLayout( this._tree.getTree(), @@ -446,7 +445,7 @@ define([ arcy0: [null, 0], arcStartAngle: [null, 0], arcEndAngle: [null, 0], - } + }; } else { data = LayoutsUtil.circularLayout( this._tree.getTree(), @@ -482,7 +481,7 @@ define([ // store new layout information xCoord: [null, 0], yCoord: [null, 0], - } + }; } else { data = LayoutsUtil.unrootedLayout( this._tree.getTree(), @@ -2181,11 +2180,11 @@ define([ /** * */ - Empress.prototype.getUniqueSampleMetadataInfo = function(cat) { + Empress.prototype.getUniqueSampleMetadataInfo = function (cat) { var obs = this._biom.getObsBy(cat); var nodes = new Set([...this._tree.postorderTraversal()]); _.each(obs, function (observations, key) { - obs[key] = observations.filter(x => nodes.has(x)); + obs[key] = observations.filter((x) => nodes.has(x)); }); return obs; }; @@ -2321,7 +2320,9 @@ define([ } // need to convert to integer node = parseInt(node); - if (!nodes.has(node)) {return}; + if (!nodes.has(node)) { + return; + } uniqueValueToFeatures[fmVal].push(node); }); }); @@ -3625,7 +3626,7 @@ define([ return this._tree.length(this._tree.postorderselect(nodeKey)); } }; - + /** * This will shear/unshear */ @@ -3634,29 +3635,29 @@ define([ var scope = this; var removeNodes = new Set(); - shearMap.forEach(function(values, cat) { + shearMap.forEach(function (values, cat) { var fmInfo = scope.getUniqueFeatureMetadataInfo(cat, "tip"); var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; - console.log("Test",uniqueValueToFeatures) - _.each(values, function(val) { + _.each(values, function (val) { var obs = uniqueValueToFeatures[val]; - for(var node of obs) { + for (var node of obs) { removeNodes.add(node); } }); }); - // remove removeNodes var allNodes = Array.from(Array(this._tree.size + 1).keys()); allNodes.shift(); allNodes = new Set(allNodes); - var keepNodes = new Set([...allNodes].filter(x => !removeNodes.has(x))); + var keepNodes = new Set( + [...allNodes].filter((x) => !removeNodes.has(x)) + ); - var keepNames = [] - for(var node of keepNodes) { - var name = this._tree.name(this._tree.postorderselect(node)) + var keepNames = []; + for (var node of keepNodes) { + var name = this._tree.name(this._tree.postorderselect(node)); keepNames.push(name); } diff --git a/empress/support_files/js/shear-panel.js b/empress/support_files/js/shearer.js similarity index 69% rename from empress/support_files/js/shear-panel.js rename to empress/support_files/js/shearer.js index c3aff0e84..dd7f1c5df 100644 --- a/empress/support_files/js/shear-panel.js +++ b/empress/support_files/js/shearer.js @@ -1,8 +1,4 @@ -define([ - "underscore", - "util", - "TreeController", -], function ( +define(["underscore", "util", "TreeController"], function ( _, util, TreeController @@ -37,31 +33,27 @@ define([ legendTitle.innerText = this.fCol; legendTitle.classList.add("legend-title"); - // create chcbox div + // create chcbox div var legendChkBoxs = document.createElement("div"); chkBoxLegendDiv.appendChild(legendChkBoxs); // create checkboxes var table = document.createElement("table"); legendChkBoxs.appendChild(table); - _.each(this.fVals, function(val) { + _.each(this.fVals, function (val) { var row = document.createElement("tr"); - + // add checkbox var dataCheck = document.createElement("td"); var input = document.createElement("INPUT"); input.setAttribute("type", "checkbox"); input.checked = true; - input.onchange = function(){ - chkBxClickFunction( - !input.checked, - scope.fCol, - val - ); - }; + input.onchange = function () { + chkBxClickFunction(!input.checked, scope.fCol, val); + }; dataCheck.appendChild(input); row.appendChild(dataCheck); - + // add checkbox label var dataLabel = document.createElement("td"); dataLabel.innerText = val; @@ -83,15 +75,15 @@ define([ // create remove button var removeButton = document.createElement("button"); removeButton.innerText = "-"; - removeButton.onclick = function() { + removeButton.onclick = function () { removeClickFunction(scope.fCol); scope.layerDiv.remove(); scope.layerDiv = null; - } + }; removeContainer.appendChild(removeButton); // create border line - this.layerDiv.appendChild(document.createElement("hr")) + this.layerDiv.appendChild(document.createElement("hr")); } function Model(empress, container) { @@ -102,58 +94,59 @@ define([ this.observers = []; } - Model.prototype.addLayer = function(fCol) { - var fVals = this.empress - .getUniqueFeatureMetadataInfo(fCol, "tip") + Model.prototype.addLayer = function (fCol) { + var fVals = this.empress.getUniqueFeatureMetadataInfo(fCol, "tip") .sortedUniqueValues; var layer = new Layer( fCol, fVals, this.container, - (add, col, val) => {Model.addRemoveShearItem(this, add,col, val);}, - (col) => {Model.removeLayer(this, col);} - ) + (add, col, val) => { + Model.addRemoveShearItem(this, add, col, val); + }, + (col) => { + Model.removeLayer(this, col); + } + ); this.layers.set(fCol, layer); }; - Model.prototype.getShearItem = function(fCol) { + Model.prototype.getShearItem = function (fCol) { return this.shearMap.get(fCol); }; - Model.prototype.hasLayer = function(fCol) { - return this.layers.has(fCol); + Model.prototype.hasLayer = function (fCol) { + return this.layers.has(fCol); }; - Model.prototype.notify = function() { - console.log("this", this); + Model.prototype.notify = function () { this.empress.shear(this.shearMap); this.empress.drawTree(); - _.each(this.observers, function(obs) { + _.each(this.observers, function (obs) { obs.shearUpdate(); }); }; - Model.prototype.registerObserver = function(obs) { + Model.prototype.registerObserver = function (obs) { this.observers.push(obs); }; - Model.addRemoveShearItem = function(model, remove, col, val) { + Model.addRemoveShearItem = function (model, remove, col, val) { if (remove) { Model.addShearItem(model, col, val); } else { Model.removeShearItem(model, col, val); } - } + }; - Model.removeLayer = function(model, fCol) { + Model.removeLayer = function (model, fCol) { model.layers.delete(fCol); model.shearMap.delete(fCol); model.notify(); - console.log(model) }; - Model.addShearItem = function(model, fCol, fVal) { - if(model.shearMap.has(fCol)) { + Model.addShearItem = function (model, fCol, fVal) { + if (model.shearMap.has(fCol)) { model.shearMap.get(fCol).push(fVal); } else { model.shearMap.set(fCol, [fVal]); @@ -161,28 +154,29 @@ define([ model.notify(); }; - Model.removeShearItem = function(model, fCol, fVal) { + Model.removeShearItem = function (model, fCol, fVal) { var items = model.getShearItem(fCol); - if (items === undefined) {return;} - var index = items.indexOf(fVal) + if (items === undefined) { + return; + } + var index = items.indexOf(fVal); if (index > -1) { items.splice(index, 1); } - model.notify() + model.notify(); }; - function Controller(empress, container) { - this.model = new Model(empress, container) + this.model = new Model(empress, container); } - Controller.prototype.addLayer = function(fCol) { - if(!this.model.hasLayer(fCol)) { + Controller.prototype.addLayer = function (fCol) { + if (!this.model.hasLayer(fCol)) { this.model.addLayer(fCol); } - } + }; - Controller.prototype.registerObserver = function(obs) { + Controller.prototype.registerObserver = function (obs) { this.model.registerObserver(obs); }; @@ -194,21 +188,21 @@ define([ this.controller = new Controller(empress, this.shearContainer); var scope = this; - _.each(this.fCols, function(col) { + _.each(this.fCols, function (col) { var opt = document.createElement("option"); opt.innerText = col; opt.value = col; scope.shearSelect.appendChild(opt); }); - this.addLayerButton.onclick = function() { - scope.controller.addLayer(scope.shearSelect.value) - } + this.addLayerButton.onclick = function () { + scope.controller.addLayer(scope.shearSelect.value); + }; } - Shearer.prototype.registerObserver = function(obs) { + Shearer.prototype.registerObserver = function (obs) { this.controller.registerObserver(obs); }; return Shearer; -}); \ No newline at end of file +}); diff --git a/empress/support_files/js/side-panel-handler.js b/empress/support_files/js/side-panel-handler.js index 0b3e226a5..d21447b90 100644 --- a/empress/support_files/js/side-panel-handler.js +++ b/empress/support_files/js/side-panel-handler.js @@ -316,7 +316,6 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { var col = this.sColor.value; var reverse = this.sReverseColor.checked; var keyInfo = this.empress.colorBySampleCat(colBy, col, reverse); - console.log("Mother Fuck", keyInfo) if (keyInfo === null) { util.toastMsg( @@ -341,7 +340,6 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { coloringMethod, reverse ); - console.log("Mother Fuck", keyInfo) if (_.isEmpty(keyInfo)) { util.toastMsg( "No unique branches found for this metadata category" @@ -460,16 +458,15 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { } }; - SidePanel.prototype.shearUpdate = function() { - console.log("????") + SidePanel.prototype.shearUpdate = function () { if (this.sChk.checked) { - this.sUpdateBtn.click() + this.sUpdateBtn.click(); } if (this.fChk.checked) { this.fUpdateBtn.click(); } - } + }; /** * Initializes exporting options. diff --git a/empress/support_files/templates/empress-template.html b/empress/support_files/templates/empress-template.html index c8ccabebc..acbf6b92d 100644 --- a/empress/support_files/templates/empress-template.html +++ b/empress/support_files/templates/empress-template.html @@ -116,7 +116,7 @@ 'LayoutsUtil': './js/layouts-util', 'ExportUtil': './js/export-util', 'TreeController': './js/tree-controller', - 'ShearPanel': './js/shear-panel' + 'Shearer': './js/shearer' } }); @@ -125,12 +125,12 @@ 'SidePanel', 'AnimationPanel', 'Animator', 'BarplotLayer', 'BarplotPanel', 'BIOMTable', 'Empress', 'Legend', 'Colorer', 'VectorOps', 'CanvasEvents', 'SelectedNodeMenu', - 'util', 'LayoutsUtil', 'ExportUtil', 'ShearPanel'], + 'util', 'LayoutsUtil', 'ExportUtil', 'Shearer'], function($, gl, chroma, underscore, spectrum, filesaver, ByteArray, BPTree, Camera, Drawer, SidePanel, AnimationPanel, Animator, BarplotLayer, BarplotPanel, BIOMTable, Empress, Legend, Colorer, VectorOps, CanvasEvents, SelectedNodeMenu, util, - LayoutsUtil, ExportUtil, ShearPanel) { + LayoutsUtil, ExportUtil, Shearer) { // initialze the tree and model var tree = new BPTree( {{ tree }}, @@ -175,13 +175,11 @@ sPanel.addLayoutTab(); sPanel.addExportTab(); - // this.empress.getFeatureMetadataCategories(); - var TESTSHEAR = new ShearPanel( + var shearer = new Shearer( empress, empress.getFeatureMetadataCategories() ); - TESTSHEAR.registerObserver(sPanel); - // TESTSHEAR.addLayer("Level 1"); + shearer.registerObserver(sPanel); // Only show the sample metadata coloring / animation panels if a // feature table and sample metadata file were provided diff --git a/tests/python/make-dev-page.py b/tests/python/make-dev-page.py index e5b0a907d..2f7341079 100755 --- a/tests/python/make-dev-page.py +++ b/tests/python/make-dev-page.py @@ -69,8 +69,7 @@ def main(tree, table, sample_metadata, feature_metadata, ordination, table = table.view(biom.Table) sample_metadata = sample_metadata.to_dataframe() feature_metadata = feature_metadata.to_dataframe() - - ordination = None + if ordination is not None: ordination = ordination.view(OrdinationResults) @@ -87,7 +86,7 @@ def main(tree, table, sample_metadata, feature_metadata, ordination, feats.sort_values('importance', inplace=True, ascending=False) feats.drop(['importance'], inplace=True, axis=1) ordination.features = feats[:number_of_features].copy() - + # These two lines fetch the JS files for both apps directly from the # installation directory - this makes testing/developing easier empress_resources = pkg_resources.resource_filename('empress', diff --git a/tests/test-legend.js b/tests/test-legend.js index 81759b730..9eb88c2f2 100644 --- a/tests/test-legend.js +++ b/tests/test-legend.js @@ -156,12 +156,6 @@ require([ deepEqual(legend._sortedCategories, ["hjkl"]); deepEqual(legend._category2color, colorInfo); }); - test("addCategoricalKey (error: no categories)", function () { - var legend = new Legend(this.containerEle); - throws(function () { - legend.addCategoricalKey("oops", {}); - }, /Can't create a categorical legend when there are no categories in the info/); - }); test("addContinuousKey", function () { var legend = new Legend(this.containerEle); var colorer = new Colorer("Viridis", ["0", "4"], true); From fcb6b5b27a7d842491fc2a49567789382a87827c Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 25 Mar 2021 13:05:48 -0700 Subject: [PATCH 16/63] removed old comments --- empress/support_files/css/empress.css | 11 ----------- empress/support_files/js/empress.js | 3 --- 2 files changed, 14 deletions(-) diff --git a/empress/support_files/css/empress.css b/empress/support_files/css/empress.css index bdf91b521..8532d2b7f 100644 --- a/empress/support_files/css/empress.css +++ b/empress/support_files/css/empress.css @@ -592,17 +592,6 @@ p.side-header button:hover, overflow: auto; } -/*.shear-layer-legend label { - display: inline-block; - vertical-align: top; - width: 200px; -}*/ - -/*.shear-layer-legend p { - margin:0; - padding:0; -}*/ - .barplot-layer-legend { max-height: 15em; /* The side panel width is 450px */ diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 64dfc9fd7..72093dc46 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -3670,9 +3670,6 @@ define([ this.getLayoutInfo(); - // if (this._currentLayout !== "Unrooted") { - // this.drawBarplots(); - // } // Undraw or redraw barplots as needed (assuming barplots are supported // in the first place, of course; if no feature or sample metadata at // all was passed then barplots are not available :() From b3acbd18ee784ead1e5457661605b0e5434d7397 Mon Sep 17 00:00:00 2001 From: kwcantrell Date: Tue, 30 Mar 2021 13:27:30 -0700 Subject: [PATCH 17/63] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit added some suggestions by @ElDeveloper and @fedarko Co-authored-by: Marcus Fedarko Co-authored-by: Yoshiki Vázquez Baeza --- empress/support_files/js/empress.js | 5 ++--- empress/support_files/js/shearer.js | 2 +- empress/support_files/templates/side-panel.html | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index b22646853..a7cc391b9 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -2182,8 +2182,8 @@ define([ Empress.prototype.getUniqueSampleMetadataInfo = function (cat) { var obs = this._biom.getObsBy(cat); var nodes = new Set([...this._tree.postorderTraversal()]); - _.each(obs, function (observations, key) { - obs[key] = observations.filter((x) => nodes.has(x)); + _.each(obs, function (featuresWithSMValue, smValue) { + obs[smValue] = featuresWithSMValue.filter((x) => nodes.has(x)); }); return obs; }; @@ -2208,7 +2208,6 @@ define([ reverse = false ) { var tree = this._tree; - // var obs = this._biom.getObsBy(cat); var obs = this.getUniqueSampleMetadataInfo(cat); var categories = Object.keys(obs); diff --git a/empress/support_files/js/shearer.js b/empress/support_files/js/shearer.js index dd7f1c5df..f45dbfd87 100644 --- a/empress/support_files/js/shearer.js +++ b/empress/support_files/js/shearer.js @@ -27,7 +27,7 @@ define(["underscore", "util", "TreeController"], function ( chkBoxLegendDiv.classList.add("shear-layer-legend"); chkBoxLegendDiv.classList.add("legend"); - // create chckbox legend title + // create checkbox legend title var legendTitle = document.createElement("div"); chkBoxLegendDiv.appendChild(legendTitle); legendTitle.innerText = this.fCol; diff --git a/empress/support_files/templates/side-panel.html b/empress/support_files/templates/side-panel.html index 86772df46..1fa67a89c 100644 --- a/empress/support_files/templates/side-panel.html +++ b/empress/support_files/templates/side-panel.html @@ -258,11 +258,11 @@ -

- Shear tree using feature metadata + Shear the tree to tips with certain feature metadata values.

@@ -272,7 +272,7 @@

- +

From 263dd6aaa0eb8041aad706f2073b9531b1401f11 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Tue, 30 Mar 2021 13:30:05 -0700 Subject: [PATCH 18/63] started addressing PR suggestions --- empress/support_files/js/bp-tree.js | 2 +- empress/support_files/js/canvas-events.js | 2 +- empress/support_files/js/empress.js | 2 +- empress/support_files/templates/side-panel.html | 12 +++++------- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index a9077da94..be81c0e69 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -702,7 +702,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { // find first and last preorder positions of the subtree spanned // by the current internal node var n = this.postorderselect(nodeKey); - if (this.isleaf(n)) { + if (this.isleaf(n) && nodeKey !== this.postorder(this.root())) { throw "Error: " + nodeKey + " is a tip!"; } var start = this.preorder(this.fchild(n)); diff --git a/empress/support_files/js/canvas-events.js b/empress/support_files/js/canvas-events.js index 0dc484059..a161dd35c 100644 --- a/empress/support_files/js/canvas-events.js +++ b/empress/support_files/js/canvas-events.js @@ -164,7 +164,7 @@ define(["underscore", "glMatrix", "SelectedNodeMenu"], function ( // Go through all the nodes in the tree and find the node // closest to the (x, y) point that was clicked - for (var node = 1; node <= empress._tree.size; node++) { + for (var node of empress._tree.postorderTraversal(includeRoot = true)) { if (!empress.getNodeInfo(node, "visible")) continue; var nodeX = empress.getX(node); var nodeY = empress.getY(node); diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index b22646853..08dedc7ad 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -2743,7 +2743,7 @@ define([ var x = 0, y = 0, zoomAmount = 0; - for (var node of this._tree.postorderTraversal((includeRoot = true))) { + for (var node of this._tree.postorderTraversal(includeRoot = true)) { // node = this._treeData[node]; x += this.getX(node); y += this.getY(node); diff --git a/empress/support_files/templates/side-panel.html b/empress/support_files/templates/side-panel.html index 86772df46..f9a6d5ebd 100644 --- a/empress/support_files/templates/side-panel.html +++ b/empress/support_files/templates/side-panel.html @@ -260,7 +260,7 @@ -
+ -
-
-
-
-
-
+
+
+
+
From 68d0d6372300613a38e26aa766b65a46bf20aeb4 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Tue, 30 Mar 2021 13:30:46 -0700 Subject: [PATCH 19/63] fixed style --- empress/support_files/js/canvas-events.js | 4 +++- empress/support_files/js/empress.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/empress/support_files/js/canvas-events.js b/empress/support_files/js/canvas-events.js index a161dd35c..6b264b834 100644 --- a/empress/support_files/js/canvas-events.js +++ b/empress/support_files/js/canvas-events.js @@ -164,7 +164,9 @@ define(["underscore", "glMatrix", "SelectedNodeMenu"], function ( // Go through all the nodes in the tree and find the node // closest to the (x, y) point that was clicked - for (var node of empress._tree.postorderTraversal(includeRoot = true)) { + for (var node of empress._tree.postorderTraversal( + (includeRoot = true) + )) { if (!empress.getNodeInfo(node, "visible")) continue; var nodeX = empress.getX(node); var nodeY = empress.getY(node); diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index a12b0ed07..a7cc391b9 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -2742,7 +2742,7 @@ define([ var x = 0, y = 0, zoomAmount = 0; - for (var node of this._tree.postorderTraversal(includeRoot = true)) { + for (var node of this._tree.postorderTraversal((includeRoot = true))) { // node = this._treeData[node]; x += this.getX(node); y += this.getY(node); From 603bf971c7ea876a355b5a15861f85005b6d04f8 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Wed, 31 Mar 2021 20:14:22 -0700 Subject: [PATCH 20/63] still addressing comments --- empress/support_files/js/biom-table.js | 9 +++ empress/support_files/js/empress.js | 67 +++++++++++-------- empress/support_files/js/shearer.js | 66 +++++++++++++++--- .../support_files/js/side-panel-handler.js | 2 +- .../support_files/templates/side-panel.html | 2 +- 5 files changed, 108 insertions(+), 38 deletions(-) diff --git a/empress/support_files/js/biom-table.js b/empress/support_files/js/biom-table.js index 800529d88..949f75692 100644 --- a/empress/support_files/js/biom-table.js +++ b/empress/support_files/js/biom-table.js @@ -86,6 +86,7 @@ define(["underscore", "util"], function (_, util) { this._tbl = tbl; this._smCols = smCols; this._sm = sm; + this.ignorefIDs = new Set(); } /** @@ -582,6 +583,9 @@ define(["underscore", "util"], function (_, util) { var fID2Freqs = {}; var totalSampleCount; _.each(this._fIDs, function (fID, fIdx) { + if (scope.ignorefIDs.has(fID)) { + return; + } totalSampleCount = fIdx2SampleCt[fIdx]; fID2Freqs[fID] = {}; _.each(fIdx2Counts[fIdx], function (count, smValIdx) { @@ -591,8 +595,13 @@ define(["underscore", "util"], function (_, util) { } }); }); + return fID2Freqs; }; + BIOMTable.prototype.setIngnoreNodes = function(nodes) { + this.ignorefIDs = nodes; + }; + return BIOMTable; }); diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index a7cc391b9..4376e510a 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -389,17 +389,24 @@ define([ branchMethod, this._tree.getTree() ); + var dataForOnlyRoot = function(coordKeys) { + var rootCoordData = {}; + _.each(coordKeys, function(key) { + rootCoordData[key] = [null, 0]; + }); + return rootCoordData; + }; // Rectangular if (this._currentLayout === "Rectangular") { // tree is just root if (this._tree.currentSize == 1) { - data = { - xCoord: [null, 0], - yCoord: [null, 0], - highestChildYr: [null, 0], - lowestChildYr: [null, 0], - yScalingFactor: [null, 0], - }; + data = dataForOnlyRoot([ + "xCoord", + "yCoord", + "highestChildYr", + "lowestChildYr", + "yScalingFactor" + ]); } else { data = LayoutsUtil.rectangularLayout( this._tree.getTree(), @@ -418,7 +425,7 @@ define([ ); } this._yrscf = data.yScalingFactor; - for (i of this._tree.postorderTraversal((includeRoot = true))) { + for (i of this._tree.postorderTraversal(includeRoot = true)) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; @@ -433,18 +440,16 @@ define([ } } else if (this._currentLayout === "Circular") { if (this._tree.currentSize == 1) { - data = { - // store new layout information - x0: [null, 0], - y0: [null, 0], - x1: [null, 0], - y1: [null, 0], - angle: [null, 0], - arcx0: [null, 0], - arcy0: [null, 0], - arcStartAngle: [null, 0], - arcEndAngle: [null, 0], - }; + data = dataForOnlyRoot([ + "x0", + "y0", + "x1", + "y1", + "angle", + "arcx0", + "arcStartAngle", + "arcendangle" + ]); } else { data = LayoutsUtil.circularLayout( this._tree.getTree(), @@ -456,7 +461,7 @@ define([ checkLengthsChange ); } - for (i of this._tree.postorderTraversal((includeRoot = true))) { + for (i of this._tree.postorderTraversal(includeRoot = true)) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; @@ -476,11 +481,7 @@ define([ } } else { if (this._tree.currentSize == 1) { - data = { - // store new layout information - xCoord: [null, 0], - yCoord: [null, 0], - }; + data = dataForOnlyRoot(["xCoord", "yCoord"]); } else { data = LayoutsUtil.unrootedLayout( this._tree.getTree(), @@ -491,7 +492,7 @@ define([ checkLengthsChange ); } - for (i of this._tree.postorderTraversal((includeRoot = true))) { + for (i of this._tree.postorderTraversal(includeRoot = true)) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; @@ -1729,6 +1730,13 @@ define([ // Do most of the hard work: compute the frequencies for each tip (only // the tips present in the BIOM table, that is) var feature2freqs = this._biom.getFrequencyMap(layer.colorBySMField); + // var nodes = new Set([...this._tree.postorderTraversal()]); + // _.each(feature2freqs, function (blah, node) { + // if (!nodes.has(parseInt(node))) { + // delete feature2freqs[node]; + // } + // }); + // Only bother computing the halfyrscf / halfAngleRange value we need. // (this._tree.numleaves() does iterate over the full tree, at least @@ -3645,7 +3653,7 @@ define([ }); // remove removeNodes - var allNodes = Array.from(Array(this._tree.size + 1).keys()); + var allNodes = _.range(1, this._tree.size + 1); allNodes.shift(); allNodes = new Set(allNodes); @@ -3659,6 +3667,9 @@ define([ keepNames.push(name); } + this._biom.setIngnoreNodes(new Set(removeNodes)); + + this._tree.shear(new Set(keepNames)); var nodeNames = this._tree.getAllNames(); // Don't include nodes with the name null (i.e. nodes without a diff --git a/empress/support_files/js/shearer.js b/empress/support_files/js/shearer.js index f45dbfd87..4bb45d0eb 100644 --- a/empress/support_files/js/shearer.js +++ b/empress/support_files/js/shearer.js @@ -3,17 +3,21 @@ define(["underscore", "util", "TreeController"], function ( util, TreeController ) { - function Layer( + function Filter ( fCol, fVals, container, chkBxClickFunction, - removeClickFunction + removeClickFunction, + selectAllFunction, + unselectAllFuntion ) { this.fCol = fCol; this.fVals = fVals; this.container = container; this.layerDiv = null; + this.inputs = []; + this.values = []; var scope = this; @@ -33,6 +37,25 @@ define(["underscore", "util", "TreeController"], function ( legendTitle.innerText = this.fCol; legendTitle.classList.add("legend-title"); + var button = document.createElement("button"); + button.innerText = "Select all"; + button.onclick = function() { + _.each(scope.inputs, function(input) { + input.select(); + }); + selectAllFunction(scope.fCol); + }; + chkBoxLegendDiv.appendChild(button); + + button = document.createElement("button"); + button.innerText = "Unselect all"; + button.onclick = function() { + _.each(scope.inputs, function(input) { + input.unselect(); + }); + unselectAllFuntion(scope.fCol, scope.values) + }; + chkBoxLegendDiv.appendChild(button); // create chcbox div var legendChkBoxs = document.createElement("div"); chkBoxLegendDiv.appendChild(legendChkBoxs); @@ -41,22 +64,33 @@ define(["underscore", "util", "TreeController"], function ( var table = document.createElement("table"); legendChkBoxs.appendChild(table); _.each(this.fVals, function (val) { + scope.values.push(val); var row = document.createElement("tr"); + var id = scope.fCol.replace(" ", "-") + "-" + val; + // add checkbox var dataCheck = document.createElement("td"); - var input = document.createElement("INPUT"); + var input = document.createElement("input"); input.setAttribute("type", "checkbox"); input.checked = true; input.onchange = function () { chkBxClickFunction(!input.checked, scope.fCol, val); }; + input.select = function() { + input.checked = true; + }; + input.unselect = function() { + input.checked = false; + }; + scope.inputs.push(input); dataCheck.appendChild(input); row.appendChild(dataCheck); // add checkbox label var dataLabel = document.createElement("td"); dataLabel.innerText = val; + dataLabel.onclick = function() {input.click();}; row.appendChild(dataLabel); // add row to table @@ -88,7 +122,7 @@ define(["underscore", "util", "TreeController"], function ( function Model(empress, container) { this.empress = empress; - this.layers = new Map(); + this.filters = new Map(); this.shearMap = new Map(); this.container = container; this.observers = []; @@ -97,7 +131,7 @@ define(["underscore", "util", "TreeController"], function ( Model.prototype.addLayer = function (fCol) { var fVals = this.empress.getUniqueFeatureMetadataInfo(fCol, "tip") .sortedUniqueValues; - var layer = new Layer( + var layer = new Filter ( fCol, fVals, this.container, @@ -106,9 +140,15 @@ define(["underscore", "util", "TreeController"], function ( }, (col) => { Model.removeLayer(this, col); + }, + (col) => { + Model.clearShearMaplFilter(this, col); + }, + (filter, values) => { + Model.setShearMapFilter(this, filter, values); } ); - this.layers.set(fCol, layer); + this.filters.set(fCol, layer); }; Model.prototype.getShearItem = function (fCol) { @@ -116,7 +156,7 @@ define(["underscore", "util", "TreeController"], function ( }; Model.prototype.hasLayer = function (fCol) { - return this.layers.has(fCol); + return this.filters.has(fCol); }; Model.prototype.notify = function () { @@ -131,6 +171,16 @@ define(["underscore", "util", "TreeController"], function ( this.observers.push(obs); }; + Model.clearShearMaplFilter = function(model, filter) { + model.shearMap.set(filter, []); + model.notify(); + } + + Model.setShearMapFilter = function(model, filter, values) { + model.shearMap.set(filter, values); + model.notify(); + } + Model.addRemoveShearItem = function (model, remove, col, val) { if (remove) { Model.addShearItem(model, col, val); @@ -140,7 +190,7 @@ define(["underscore", "util", "TreeController"], function ( }; Model.removeLayer = function (model, fCol) { - model.layers.delete(fCol); + model.filters.delete(fCol); model.shearMap.delete(fCol); model.notify(); }; diff --git a/empress/support_files/js/side-panel-handler.js b/empress/support_files/js/side-panel-handler.js index d21447b90..c7080335f 100644 --- a/empress/support_files/js/side-panel-handler.js +++ b/empress/support_files/js/side-panel-handler.js @@ -342,7 +342,7 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { ); if (_.isEmpty(keyInfo)) { util.toastMsg( - "No unique branches found for this metadata category" + "No nodes with feature metadata are visible due to shearing" ); this.fUpdateBtn.classList.remove("hidden"); return; diff --git a/empress/support_files/templates/side-panel.html b/empress/support_files/templates/side-panel.html index b773453f5..e7c318aa9 100644 --- a/empress/support_files/templates/side-panel.html +++ b/empress/support_files/templates/side-panel.html @@ -276,7 +276,7 @@

-
+

From 31b5af31d3c2793ce58f436f8c555a8072733e0d Mon Sep 17 00:00:00 2001 From: kcantrel Date: Wed, 31 Mar 2021 20:16:27 -0700 Subject: [PATCH 21/63] fixed style --- empress/support_files/js/biom-table.js | 2 +- empress/support_files/js/empress.js | 16 ++++++-------- empress/support_files/js/shearer.js | 30 ++++++++++++++------------ 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/empress/support_files/js/biom-table.js b/empress/support_files/js/biom-table.js index 949f75692..b822de74a 100644 --- a/empress/support_files/js/biom-table.js +++ b/empress/support_files/js/biom-table.js @@ -599,7 +599,7 @@ define(["underscore", "util"], function (_, util) { return fID2Freqs; }; - BIOMTable.prototype.setIngnoreNodes = function(nodes) { + BIOMTable.prototype.setIngnoreNodes = function (nodes) { this.ignorefIDs = nodes; }; diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 4376e510a..482b9a4fc 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -389,9 +389,9 @@ define([ branchMethod, this._tree.getTree() ); - var dataForOnlyRoot = function(coordKeys) { + var dataForOnlyRoot = function (coordKeys) { var rootCoordData = {}; - _.each(coordKeys, function(key) { + _.each(coordKeys, function (key) { rootCoordData[key] = [null, 0]; }); return rootCoordData; @@ -405,7 +405,7 @@ define([ "yCoord", "highestChildYr", "lowestChildYr", - "yScalingFactor" + "yScalingFactor", ]); } else { data = LayoutsUtil.rectangularLayout( @@ -425,7 +425,7 @@ define([ ); } this._yrscf = data.yScalingFactor; - for (i of this._tree.postorderTraversal(includeRoot = true)) { + for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; @@ -448,7 +448,7 @@ define([ "angle", "arcx0", "arcStartAngle", - "arcendangle" + "arcendangle", ]); } else { data = LayoutsUtil.circularLayout( @@ -461,7 +461,7 @@ define([ checkLengthsChange ); } - for (i of this._tree.postorderTraversal(includeRoot = true)) { + for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; @@ -492,7 +492,7 @@ define([ checkLengthsChange ); } - for (i of this._tree.postorderTraversal(includeRoot = true)) { + for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; @@ -1737,7 +1737,6 @@ define([ // } // }); - // Only bother computing the halfyrscf / halfAngleRange value we need. // (this._tree.numleaves() does iterate over the full tree, at least // as of writing, so avoiding calling it if possible is a good idea.) @@ -3668,7 +3667,6 @@ define([ } this._biom.setIngnoreNodes(new Set(removeNodes)); - this._tree.shear(new Set(keepNames)); var nodeNames = this._tree.getAllNames(); diff --git a/empress/support_files/js/shearer.js b/empress/support_files/js/shearer.js index 4bb45d0eb..e038b25fe 100644 --- a/empress/support_files/js/shearer.js +++ b/empress/support_files/js/shearer.js @@ -3,7 +3,7 @@ define(["underscore", "util", "TreeController"], function ( util, TreeController ) { - function Filter ( + function Filter( fCol, fVals, container, @@ -39,8 +39,8 @@ define(["underscore", "util", "TreeController"], function ( var button = document.createElement("button"); button.innerText = "Select all"; - button.onclick = function() { - _.each(scope.inputs, function(input) { + button.onclick = function () { + _.each(scope.inputs, function (input) { input.select(); }); selectAllFunction(scope.fCol); @@ -49,11 +49,11 @@ define(["underscore", "util", "TreeController"], function ( button = document.createElement("button"); button.innerText = "Unselect all"; - button.onclick = function() { - _.each(scope.inputs, function(input) { + button.onclick = function () { + _.each(scope.inputs, function (input) { input.unselect(); }); - unselectAllFuntion(scope.fCol, scope.values) + unselectAllFuntion(scope.fCol, scope.values); }; chkBoxLegendDiv.appendChild(button); // create chcbox div @@ -77,10 +77,10 @@ define(["underscore", "util", "TreeController"], function ( input.onchange = function () { chkBxClickFunction(!input.checked, scope.fCol, val); }; - input.select = function() { + input.select = function () { input.checked = true; }; - input.unselect = function() { + input.unselect = function () { input.checked = false; }; scope.inputs.push(input); @@ -90,7 +90,9 @@ define(["underscore", "util", "TreeController"], function ( // add checkbox label var dataLabel = document.createElement("td"); dataLabel.innerText = val; - dataLabel.onclick = function() {input.click();}; + dataLabel.onclick = function () { + input.click(); + }; row.appendChild(dataLabel); // add row to table @@ -131,7 +133,7 @@ define(["underscore", "util", "TreeController"], function ( Model.prototype.addLayer = function (fCol) { var fVals = this.empress.getUniqueFeatureMetadataInfo(fCol, "tip") .sortedUniqueValues; - var layer = new Filter ( + var layer = new Filter( fCol, fVals, this.container, @@ -171,15 +173,15 @@ define(["underscore", "util", "TreeController"], function ( this.observers.push(obs); }; - Model.clearShearMaplFilter = function(model, filter) { + Model.clearShearMaplFilter = function (model, filter) { model.shearMap.set(filter, []); model.notify(); - } + }; - Model.setShearMapFilter = function(model, filter, values) { + Model.setShearMapFilter = function (model, filter, values) { model.shearMap.set(filter, values); model.notify(); - } + }; Model.addRemoveShearItem = function (model, remove, col, val) { if (remove) { From 7f56e68b00939bb1a858a71cc82ac49ddeb99e41 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Wed, 31 Mar 2021 20:30:33 -0700 Subject: [PATCH 22/63] fix unselect all --- empress/support_files/js/shearer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/empress/support_files/js/shearer.js b/empress/support_files/js/shearer.js index e038b25fe..4dc49cc46 100644 --- a/empress/support_files/js/shearer.js +++ b/empress/support_files/js/shearer.js @@ -53,7 +53,7 @@ define(["underscore", "util", "TreeController"], function ( _.each(scope.inputs, function (input) { input.unselect(); }); - unselectAllFuntion(scope.fCol, scope.values); + unselectAllFuntion(scope.fCol, _.clone(scope.values)); }; chkBoxLegendDiv.appendChild(button); // create chcbox div From 9a9bb75234eaeb678ec90a21478a7e029de37fba Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 1 Apr 2021 08:33:58 -0700 Subject: [PATCH 23/63] Fixed unselect all for circ layout --- empress/support_files/js/empress.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 482b9a4fc..09ad1b8e2 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -447,8 +447,9 @@ define([ "y1", "angle", "arcx0", + "arcy0", "arcStartAngle", - "arcendangle", + "arcEndAngle", ]); } else { data = LayoutsUtil.circularLayout( From 9dc26bcd1671b022a0a78cd9d29eb67a982035b2 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 1 Apr 2021 08:42:45 -0700 Subject: [PATCH 24/63] fixed issue were tree disappears if all tips sheared --- empress/support_files/js/drawer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/empress/support_files/js/drawer.js b/empress/support_files/js/drawer.js index 3afd66647..8b304f99d 100644 --- a/empress/support_files/js/drawer.js +++ b/empress/support_files/js/drawer.js @@ -526,6 +526,8 @@ define(["underscore", "glMatrix", "Camera", "Colorer"], function ( * @param{Number} zoomAmount The amout to zoom in or out. */ Drawer.prototype.zoom = function (x, y, zoomIn, zoomAmount = this.scaleBy) { + // zoomAmount can be zero if all tips in the tree were sheared. + if (zoomAmount === 0) zoomAmount = this.scaleBy; var zoomAt = gl.vec4.fromValues(x, y, 0, 1); // move tree var transVec = gl.vec3.create(); From 4445ebd7ae99ca15e3148b34adbf05bc188e9b68 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 1 Apr 2021 12:00:03 -0700 Subject: [PATCH 25/63] check point --- empress/support_files/js/empress.js | 2 ++ empress/support_files/templates/empress-template.html | 1 + 2 files changed, 3 insertions(+) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 09ad1b8e2..8afaa8ad4 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -1892,6 +1892,7 @@ define([ coords, prevLayerMaxD ) { + console.log("Barplot Feature Metadata!!!", window.counter++); var maxD = prevLayerMaxD; var colorer = null; var fm2color, colorFMIdx; @@ -2370,6 +2371,7 @@ define([ method, reverse = false ) { + console.log("Color By Feature Metadata!!!", window.counter++); var fmInfo = this.getUniqueFeatureMetadataInfo(cat, method); var sortedUniqueValues = fmInfo.sortedUniqueValues; var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; diff --git a/empress/support_files/templates/empress-template.html b/empress/support_files/templates/empress-template.html index acbf6b92d..6783113c9 100644 --- a/empress/support_files/templates/empress-template.html +++ b/empress/support_files/templates/empress-template.html @@ -131,6 +131,7 @@ BarplotLayer, BarplotPanel, BIOMTable, Empress, Legend, Colorer, VectorOps, CanvasEvents, SelectedNodeMenu, util, LayoutsUtil, ExportUtil, Shearer) { + window.counter = 0; // initialze the tree and model var tree = new BPTree( {{ tree }}, From 15f81ab41b92ed4b76e3755d09c8d1ecd00af47e Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 1 Apr 2021 16:53:47 -0700 Subject: [PATCH 26/63] fixed clade collapse --- empress/support_files/js/empress.js | 48 ++----------------- empress/support_files/js/select-node-menu.js | 2 - empress/support_files/js/tree-controller.js | 43 +++++++++++++++++ .../templates/empress-template.html | 1 + 4 files changed, 48 insertions(+), 46 deletions(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 8afaa8ad4..f677c27f7 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -932,7 +932,6 @@ define([ } else { throw new Error("getNodeCoords() drawNodeCircles is out of range"); } - for (var node of this._tree.postorderTraversal((includeRoot = true))) { if (!comp(node)) { continue; @@ -1892,7 +1891,6 @@ define([ coords, prevLayerMaxD ) { - console.log("Barplot Feature Metadata!!!", window.counter++); var maxD = prevLayerMaxD; var colorer = null; var fm2color, colorFMIdx; @@ -2371,7 +2369,6 @@ define([ method, reverse = false ) { - console.log("Color By Feature Metadata!!!", window.counter++); var fmInfo = this.getUniqueFeatureMetadataInfo(cat, method); var sortedUniqueValues = fmInfo.sortedUniqueValues; var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; @@ -2836,7 +2833,7 @@ define([ */ Empress.prototype.dontCollapseClade = function (clade) { var scope = this; - var nodes = this.getCladeNodes(parseInt(clade)); + var nodes = this._tree.getCladeNodes(parseInt(clade)); nodes.forEach(function (node) { scope._dontCollapse.add(node); }); @@ -3116,7 +3113,7 @@ define([ // step 1: find all nodes in the clade. // Note: cladeNodes is an array of nodes arranged in postorder fashion - var cladeNodes = this.getCladeNodes(rootNode); + var cladeNodes = this._tree.getCladeNodes(rootNode); // use the left most child in the clade to initialize currentCladeInfo var currentCladeInfo = { @@ -3131,6 +3128,7 @@ define([ color: this.getNodeInfo(rootNode, "color"), }; + // step 2: find the following clade information and // step 3: make all descendants of rootNode invisible for (var i in cladeNodes) { @@ -3214,44 +3212,6 @@ define([ this.drawTree(); }; - /** - * Returns all nodes in the clade whose root is node. - * - * Note: elements in the returned array are keys in this._treeData - * also, the returned array is sorted in a postorder fashion - * - * @param {Number} cladeRoot The root of the clade. An error is thrown if - * cladeRoot is not a valid node. - * - * @return {Array} The nodes in the clade - */ - Empress.prototype.getCladeNodes = function (cladeRoot) { - if (!this._treeData.hasOwnProperty(cladeRoot)) { - throw cladeRoot + " is not a valid node."; - } - // stores the clade nodes - var cladeNodes = []; - - // Nodes in the clade are found by performing a postorder traversal - // starting at the left most child of the clade and ending on cladeRoot - - // find left most child - // Note: initializing lchild as cladeRoot incase cladeRoot is a tip - var lchild = cladeRoot; - var fchild = this._tree.fchild(this._tree.postorderselect(cladeRoot)); - while (fchild !== 0) { - lchild = this._tree.postorder(fchild); - fchild = this._tree.fchild(this._tree.postorderselect(lchild)); - } - - // perform post order traversal until cladeRoot is reached. - for (var i = lchild; i <= cladeRoot; i++) { - cladeNodes.push(i); - } - - return cladeNodes; - }; - /** * Checks if the point (x, y) is within the bounds of the collapsed clade. * @@ -3427,7 +3387,7 @@ define([ for (var clade in this._collapsedClades) { if (this._isPointInClade(clade, point)) { var cladeNode = this._treeData[clade]; - return clade; + return parseInt(clade); } } return -1; diff --git a/empress/support_files/js/select-node-menu.js b/empress/support_files/js/select-node-menu.js index 635929ed2..c9487bceb 100644 --- a/empress/support_files/js/select-node-menu.js +++ b/empress/support_files/js/select-node-menu.js @@ -343,7 +343,6 @@ define(["underscore", "util"], function (_, util) { this.nodeNameLabel.textContent = "Name: " + name; } hide(this.nodeNameWarning); - // show either leaf or internal node var t = emp._tree; if (t.isleaf(t.postorderselect(this.nodeKeys[0]))) { @@ -431,7 +430,6 @@ define(["underscore", "util"], function (_, util) { } var name = this.empress.getNodeInfo(this.nodeKeys[0], "name"); - // Figure out whether or not we know the actual node in the tree (for // example, if the user searched for a node with a duplicate name, then // we don't know which node the user was referring to). This impacts diff --git a/empress/support_files/js/tree-controller.js b/empress/support_files/js/tree-controller.js index aca8716c7..8ab270cfd 100644 --- a/empress/support_files/js/tree-controller.js +++ b/empress/support_files/js/tree-controller.js @@ -44,6 +44,45 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { yield* nodes; }; + /** + * Returns all nodes in the clade whose root is node. + * + * Note: elements in the returned array are keys in this._treeData + * also, the returned array is sorted in a postorder fashion + * + * @param {Number} cladeRoot The root of the clade. An error is thrown if + * cladeRoot is not a valid node. + * + * @return {Array} The nodes in the clade + */ + TreeModel.prototype.getCladeNodes = function(cladeRoot) { + cladeRoot = this.fullToSheared.get(cladeRoot) + if (cladeRoot === undefined) { + throw cladeRoot + " is not a valid node."; + } + // stores the clade nodes + var cladeNodes = []; + + // Nodes in the clade are found by performing a postorder traversal + // starting at the left most child of the clade and ending on cladeRoot + + // find left most child + // Note: initializing lchild as cladeRoot incase cladeRoot is a tip + var lchild = cladeRoot; + var fchild = this.shearedTree.fchild(this.shearedTree.postorderselect(cladeRoot)); + while (fchild !== 0) { + lchild = this.shearedTree.postorder(fchild); + fchild = this.shearedTree.fchild(this.shearedTree.postorderselect(lchild)); + } + + // perform post order traversal until cladeRoot is reached. + for (var i = lchild; i <= cladeRoot; i++) { + cladeNodes.push(this.shearedToFull.get(i)); + } + return cladeNodes; + } + + function TreeController(tree) { /** * @@ -474,5 +513,9 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { return nodes; }; + TreeController.prototype.getCladeNodes = function(cladeRoot) { + return this.model.getCladeNodes(cladeRoot); + } + return TreeController; }); diff --git a/empress/support_files/templates/empress-template.html b/empress/support_files/templates/empress-template.html index 6783113c9..cfc321a4f 100644 --- a/empress/support_files/templates/empress-template.html +++ b/empress/support_files/templates/empress-template.html @@ -181,6 +181,7 @@ empress.getFeatureMetadataCategories() ); shearer.registerObserver(sPanel); + // shearer.registerObserver(empress._barplotPanel); // Only show the sample metadata coloring / animation panels if a // feature table and sample metadata file were provided From 46306890f8a6d9fa3d3952d4ac2d2a2a4d1d835e Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 1 Apr 2021 16:56:04 -0700 Subject: [PATCH 27/63] fixed style --- empress/support_files/js/empress.js | 1 - empress/support_files/js/tree-controller.js | 19 +++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index f677c27f7..b49e43466 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -3128,7 +3128,6 @@ define([ color: this.getNodeInfo(rootNode, "color"), }; - // step 2: find the following clade information and // step 3: make all descendants of rootNode invisible for (var i in cladeNodes) { diff --git a/empress/support_files/js/tree-controller.js b/empress/support_files/js/tree-controller.js index 8ab270cfd..20a887049 100644 --- a/empress/support_files/js/tree-controller.js +++ b/empress/support_files/js/tree-controller.js @@ -55,8 +55,8 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { * * @return {Array} The nodes in the clade */ - TreeModel.prototype.getCladeNodes = function(cladeRoot) { - cladeRoot = this.fullToSheared.get(cladeRoot) + TreeModel.prototype.getCladeNodes = function (cladeRoot) { + cladeRoot = this.fullToSheared.get(cladeRoot); if (cladeRoot === undefined) { throw cladeRoot + " is not a valid node."; } @@ -69,10 +69,14 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { // find left most child // Note: initializing lchild as cladeRoot incase cladeRoot is a tip var lchild = cladeRoot; - var fchild = this.shearedTree.fchild(this.shearedTree.postorderselect(cladeRoot)); + var fchild = this.shearedTree.fchild( + this.shearedTree.postorderselect(cladeRoot) + ); while (fchild !== 0) { lchild = this.shearedTree.postorder(fchild); - fchild = this.shearedTree.fchild(this.shearedTree.postorderselect(lchild)); + fchild = this.shearedTree.fchild( + this.shearedTree.postorderselect(lchild) + ); } // perform post order traversal until cladeRoot is reached. @@ -80,8 +84,7 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { cladeNodes.push(this.shearedToFull.get(i)); } return cladeNodes; - } - + }; function TreeController(tree) { /** @@ -513,9 +516,9 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { return nodes; }; - TreeController.prototype.getCladeNodes = function(cladeRoot) { + TreeController.prototype.getCladeNodes = function (cladeRoot) { return this.model.getCladeNodes(cladeRoot); - } + }; return TreeController; }); From 73892fd24a6fad1632fc702b619244051e54c0be Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 1 Apr 2021 16:57:53 -0700 Subject: [PATCH 28/63] fixed test --- tests/test-empress.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test-empress.js b/tests/test-empress.js index d9722ace7..e3e7eba29 100644 --- a/tests/test-empress.js +++ b/tests/test-empress.js @@ -911,13 +911,13 @@ require([ test("Test getCladeNodes", function () { deepEqual( - this.empress.getCladeNodes(5), + this.empress._tree.getCladeNodes(5), [1, 2, 3, 4, 5], "valid node" ); throws(function () { - this.empress.getCladeNodes(-1); + this.empress._tree.getCladeNodes(-1); }); }); From 3ca595eb52fb32d7cfc49e0b7f18f41d8af711cd Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 1 Apr 2021 17:47:53 -0700 Subject: [PATCH 29/63] fixed no biom error --- empress/support_files/js/empress.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index b49e43466..bf6e6672f 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -3628,7 +3628,9 @@ define([ keepNames.push(name); } - this._biom.setIngnoreNodes(new Set(removeNodes)); + if (this.isCommunityPlot) { + this._biom.setIngnoreNodes(new Set(removeNodes)); + } this._tree.shear(new Set(keepNames)); var nodeNames = this._tree.getAllNames(); From 66e5dedfc85636a785cefd4e3b6abb6f6c7b1736 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Mon, 5 Apr 2021 18:34:30 -0700 Subject: [PATCH 30/63] checkpoint --- empress/support_files/js/bp-tree.js | 125 +++++++++++++++++- empress/support_files/js/empress.js | 35 ++++- .../support_files/js/side-panel-handler.js | 3 + 3 files changed, 155 insertions(+), 8 deletions(-) diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index be81c0e69..be1c3e052 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -1006,27 +1006,51 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { // iterate over bp tree in post order and add all tips that are in // keepTips plus their ancestors + // var i; + // for (i = 1; i <= this.size; i++) { + // var node = this.postorderselect(i); + // var name = this.name(node); + // if (this.isleaf(node) && keepTips.has(name)) { + // // set open/close bits for tip + // set_bits(node); + + // // set open/close bits for tips ancestors + // var parent = this.parent(node); + // while (parent !== this.root() || mask[parent] !== 1) { + // set_bits(parent); + // parent = this.parent(parent); + // } + // } + // } + + var d = new Date(); var i; for (i = 1; i <= this.size; i++) { var node = this.postorderselect(i); - var name = this.name(node); - if (this.isleaf(node) && keepTips.has(name)) { + if (this.isleaf(node) && keepTips.has(i)) { // set open/close bits for tip - set_bits(node); + // set_bits(node); + mask[node] = 1; + mask[scope.close(node)] = 0; // set open/close bits for tips ancestors var parent = this.parent(node); while (parent !== this.root() || mask[parent] !== 1) { - set_bits(parent); + // set_bits(parent); + mask[parent] = 1; + mask[scope.close(parent)] = 0; parent = this.parent(parent); } } } + var dt = new Date(); + console.log("Set bits ", dt.getTime() - d.getTime()); var newBitArray = []; var shearedToFull = new Map(); var fullToSheared = new Map(); var postorderPos = 1; + d = new Date(); for (i = 0; i < mask.length; i++) { if (mask[i] !== undefined) { newBitArray.push(mask[i]); @@ -1043,6 +1067,8 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { postorderPos += 1; } } + dt = new Date(); + console.log("Create tree info ", dt.getTime() - d.getTime()); return { shearedToFull: shearedToFull, fullToSheared: fullToSheared, @@ -1050,5 +1076,96 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { }; }; + // BPTree.prototype.shear = function (removeTips) { + // // closure + // var scope = this; + + // // create new names and lengths array + // var names = [null]; + // var lengths = [null]; + + // // create new bit array + // var mask = _.clone(this.b_); + // console.log(mask, mask.length) + + // // function to that will set open/close bits for a node + // var set_bits = (node) => { + // mask[node] = 1; + // mask[scope.close(node)] = 0; + // }; + + // // // set root open/close bits + // // set_bits(this.root()); + + // var d = new Date(); + // var i; + // // remove tips + // for (i of removeTips) { + // var node = this.postorderselect(i); + // mask[node] = undefined; + // mask[node + 1] = undefined; + // } + + // // // remove internal + // var nodeStack = []; + // for (i = mask.length - 1; i > 0; i--) { + // // internal node + // if (mask[i] === 0 && mask[i - 1] === 0) { + // nodeStack.push([i, 'u']); + // } else if (mask[i] === 0 && mask[i - 1] === 1) { + // // mark parent as keep + // if (nodeStack.length > 0) { + // nodeStack[nodeStack.length - 1][1] = 'k'; + // } + // nodeStack.push([i, 'k']); + // } else if (mask[i] === 0 && mask[i - 1] === undefined){ + // if (nodeStack.length > 0 && nodeStack[nodeStack.length - 1][1] === 'u') { + // nodeStack[nodeStack.length - 1][1] = 'r'; + // } + // nodeStack.push([i, 'r']) + // } else if (mask[i] === 1) { + // var parent = nodeStack.pop(); + // if (parent[1] === 'r') { + // mask[i] = undefined; + // mask[parent[0]] = undefined; + // } + // } + // } + // console.log("WTF", nodeStack) + // var dt = new Date(); + // console.log("Set bits ", dt.getTime() - d.getTime()); + + // var newBitArray = []; + // var shearedToFull = new Map(); + // var fullToSheared = new Map(); + // var postorderPos = 1; + // d = new Date(); + // for (i = 0; i < mask.length; i++) { + // if (mask[i] !== undefined) { + // newBitArray.push(mask[i]); + // } + + // // get name and length of node + // // Note: names and lengths of nodes are stored in postorder + + // if (mask[i] === 0) { + // names.push(this.name(i)); + // lengths.push(this.length(i)); + // shearedToFull.set(postorderPos, this.postorder(i)); + // fullToSheared.set(this.postorder(i), postorderPos); + // postorderPos += 1; + // } + // } + // console.log(mask, mask.length) + + // dt = new Date(); + // console.log("Create tree info ", dt.getTime() - d.getTime()); + // return { + // shearedToFull: shearedToFull, + // fullToSheared: fullToSheared, + // tree: new BPTree(newBitArray, names, lengths, null), + // }; + // }; + return BPTree; }); diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index bf6e6672f..8be3a57ca 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -3560,7 +3560,7 @@ define([ */ Empress.prototype.getTreeStats = function () { // Compute node counts - var allCt = this._tree.size; + var allCt = this._tree.currentSize; var tipCt = this._tree.getNumTips(this._tree.size); var intCt = allCt - tipCt; // Get length statistics @@ -3601,7 +3601,7 @@ define([ this._tree.unshear(); var scope = this; var removeNodes = new Set(); - + var d = new Date() shearMap.forEach(function (values, cat) { var fmInfo = scope.getUniqueFeatureMetadataInfo(cat, "tip"); var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; @@ -3612,8 +3612,11 @@ define([ } }); }); + var dt = new Date(); + console.log("Create `removeNodes` ", dt.getTime() - d.getTime()); // remove removeNodes + d = new Date(); var allNodes = _.range(1, this._tree.size + 1); allNodes.shift(); @@ -3621,6 +3624,8 @@ define([ var keepNodes = new Set( [...allNodes].filter((x) => !removeNodes.has(x)) ); + dt = new Date(); + console.log("Create `keepNodes` ", dt.getTime() - d.getTime()); var keepNames = []; for (var node of keepNodes) { @@ -3629,17 +3634,28 @@ define([ } if (this.isCommunityPlot) { - this._biom.setIngnoreNodes(new Set(removeNodes)); + this._biom.setIngnoreNodes(new Set(keepNodes)); } - this._tree.shear(new Set(keepNames)); + // this._tree.shear(new Set(keepNames)); + d = new Date(); + this._tree.shear(new Set(removeNodes)); + dt = new Date(); + console.log("Shear tree ", dt.getTime() - d.getTime()); + + d = new Date(); var nodeNames = this._tree.getAllNames(); // Don't include nodes with the name null (i.e. nodes without a // specified name in the Newick file) in the auto-complete. nodeNames = nodeNames.filter((n) => n !== null); this._events.autocomplete(nodeNames); + dt = new Date(); + console.log("Extract names for filter ", dt.getTime() - d.getTime()); + d = new Date(); this.getLayoutInfo(); + dt = new Date(); + console.log("Calc coords ", dt.getTime() - d.getTime()); // Undraw or redraw barplots as needed (assuming barplots are supported // in the first place, of course; if no feature or sample metadata at @@ -3658,3 +3674,14 @@ define([ return Empress; }); + +var d = new Date(); +var test = []; +for (i = 0; i < 1512754; i++) { + test.push(i) +} +for (i = 0; i < 1512754; i++) { + test.pop() +} +var dt = new Date(); +console.log(dt.getTime() - d.getTime()); \ No newline at end of file diff --git a/empress/support_files/js/side-panel-handler.js b/empress/support_files/js/side-panel-handler.js index c7080335f..e840159e5 100644 --- a/empress/support_files/js/side-panel-handler.js +++ b/empress/support_files/js/side-panel-handler.js @@ -459,6 +459,7 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { }; SidePanel.prototype.shearUpdate = function () { + var d = new Date(); if (this.sChk.checked) { this.sUpdateBtn.click(); } @@ -466,6 +467,8 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { if (this.fChk.checked) { this.fUpdateBtn.click(); } + var dt = new Date(); + console.log("Shear update ", dt.getTime() - d.getTime()); }; /** From 9eb4243f471cdbd16cc60ddf53df2e9fa409acc2 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Wed, 7 Apr 2021 13:14:34 -0700 Subject: [PATCH 31/63] performance improvemens --- empress/support_files/js/bp-tree.js | 201 +++++------------- empress/support_files/js/empress.js | 98 ++------- .../support_files/js/side-panel-handler.js | 4 +- tests/test-bp-tree.js | 20 +- tests/test-tree-controller.js | 56 +++-- tests/utilities-for-testing.js | 2 +- 6 files changed, 124 insertions(+), 257 deletions(-) diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index be1c3e052..f91a7a196 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -131,7 +131,8 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { * performance boost */ var eCache = []; - for (var i = 0; i < this.b_.length; i++) { + var i; + for (i = 0; i < this.b_.length; i++) { eCache.push(2 * this.r1Cache_[i] - i - 1); } this.eCache_ = new Uint32Array(eCache); @@ -190,6 +191,27 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { * that have the same name. */ this._nameToNodes = {}; + + this._pCache = []; + var pStack = []; + for (i = 0; i < this.b_.length - 1; i++) { + if (this.b_[i] === 1) { + if (pStack.length === 0) { + pStack.push(i); + this._pCache[i] = 0; + } else if (this.b_[i + 1] === 1) { + this._pCache[i] = pStack[pStack.length - 1]; + pStack.push(i); + } else if (this.b_[i + 1] === 0) { + this._pCache[i] = pStack[pStack.length - 1]; + } + } else if (this.b_[i] === 0) { + this._pCache[i] = pStack[pStack.length - 1]; + if (this.b_[i + 1] === 0) { + pStack.pop(); + } + } + } } /** @@ -450,6 +472,8 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { // i represents the root node if (i === this.root() || i === this.b_.length - 1) { return -1; + } else { + return this._pCache[i]; } return this.enclose(i); @@ -984,7 +1008,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { * convert the original postorder positions to the sheared * tree postorder positions ("newToOld") and vice-versa ("oldToNew"). */ - BPTree.prototype.shear = function (keepTips) { + BPTree.prototype.shear = function (removeTips) { // closure var scope = this; @@ -993,64 +1017,49 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { var lengths = [null]; // create new bit array - var mask = []; - - // function to that will set open/close bits for a node - var set_bits = (node) => { - mask[node] = 1; - mask[scope.close(node)] = 0; - }; + var mask = _.clone(this.b_); - // set root open/close bits - set_bits(this.root()); - - // iterate over bp tree in post order and add all tips that are in - // keepTips plus their ancestors - // var i; - // for (i = 1; i <= this.size; i++) { - // var node = this.postorderselect(i); - // var name = this.name(node); - // if (this.isleaf(node) && keepTips.has(name)) { - // // set open/close bits for tip - // set_bits(node); - - // // set open/close bits for tips ancestors - // var parent = this.parent(node); - // while (parent !== this.root() || mask[parent] !== 1) { - // set_bits(parent); - // parent = this.parent(parent); - // } - // } - // } - - var d = new Date(); var i; - for (i = 1; i <= this.size; i++) { + + // remove tips + for (i of removeTips) { var node = this.postorderselect(i); - if (this.isleaf(node) && keepTips.has(i)) { - // set open/close bits for tip - // set_bits(node); - mask[node] = 1; - mask[scope.close(node)] = 0; - - // set open/close bits for tips ancestors - var parent = this.parent(node); - while (parent !== this.root() || mask[parent] !== 1) { - // set_bits(parent); - mask[parent] = 1; - mask[scope.close(parent)] = 0; - parent = this.parent(parent); + mask[node] = undefined; + mask[node + 1] = undefined; + } + + // remove internal + var nodeStack = []; + for (i = mask.length - 1; i > 0; i--) { + if (mask[i] === 0) { + // close par + if (mask[i - 1] === 0) { + nodeStack.push([i, null]); + } else if (mask[i - 1] === 1) { + nodeStack.push([i, false]); + } else if (mask[i - 1] === undefined) { + nodeStack.push([i, true]); + } + } else if (mask[i] === 1) { + var parent = nodeStack.pop(); + if (parent[1] === true) { + mask[i] = undefined; + mask[parent[0]] = undefined; + } else if (parent[1] === false) { + // need explicitly check false since it can be null + nodeStack[nodeStack.length - 1][1] = false; + } + + if (nodeStack[nodeStack.length - 1][1] === null) { + nodeStack[nodeStack.length - 1][1] = parent[1]; } } } - var dt = new Date(); - console.log("Set bits ", dt.getTime() - d.getTime()); var newBitArray = []; var shearedToFull = new Map(); var fullToSheared = new Map(); var postorderPos = 1; - d = new Date(); for (i = 0; i < mask.length; i++) { if (mask[i] !== undefined) { newBitArray.push(mask[i]); @@ -1067,8 +1076,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { postorderPos += 1; } } - dt = new Date(); - console.log("Create tree info ", dt.getTime() - d.getTime()); + return { shearedToFull: shearedToFull, fullToSheared: fullToSheared, @@ -1076,96 +1084,5 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { }; }; - // BPTree.prototype.shear = function (removeTips) { - // // closure - // var scope = this; - - // // create new names and lengths array - // var names = [null]; - // var lengths = [null]; - - // // create new bit array - // var mask = _.clone(this.b_); - // console.log(mask, mask.length) - - // // function to that will set open/close bits for a node - // var set_bits = (node) => { - // mask[node] = 1; - // mask[scope.close(node)] = 0; - // }; - - // // // set root open/close bits - // // set_bits(this.root()); - - // var d = new Date(); - // var i; - // // remove tips - // for (i of removeTips) { - // var node = this.postorderselect(i); - // mask[node] = undefined; - // mask[node + 1] = undefined; - // } - - // // // remove internal - // var nodeStack = []; - // for (i = mask.length - 1; i > 0; i--) { - // // internal node - // if (mask[i] === 0 && mask[i - 1] === 0) { - // nodeStack.push([i, 'u']); - // } else if (mask[i] === 0 && mask[i - 1] === 1) { - // // mark parent as keep - // if (nodeStack.length > 0) { - // nodeStack[nodeStack.length - 1][1] = 'k'; - // } - // nodeStack.push([i, 'k']); - // } else if (mask[i] === 0 && mask[i - 1] === undefined){ - // if (nodeStack.length > 0 && nodeStack[nodeStack.length - 1][1] === 'u') { - // nodeStack[nodeStack.length - 1][1] = 'r'; - // } - // nodeStack.push([i, 'r']) - // } else if (mask[i] === 1) { - // var parent = nodeStack.pop(); - // if (parent[1] === 'r') { - // mask[i] = undefined; - // mask[parent[0]] = undefined; - // } - // } - // } - // console.log("WTF", nodeStack) - // var dt = new Date(); - // console.log("Set bits ", dt.getTime() - d.getTime()); - - // var newBitArray = []; - // var shearedToFull = new Map(); - // var fullToSheared = new Map(); - // var postorderPos = 1; - // d = new Date(); - // for (i = 0; i < mask.length; i++) { - // if (mask[i] !== undefined) { - // newBitArray.push(mask[i]); - // } - - // // get name and length of node - // // Note: names and lengths of nodes are stored in postorder - - // if (mask[i] === 0) { - // names.push(this.name(i)); - // lengths.push(this.length(i)); - // shearedToFull.set(postorderPos, this.postorder(i)); - // fullToSheared.set(this.postorder(i), postorderPos); - // postorderPos += 1; - // } - // } - // console.log(mask, mask.length) - - // dt = new Date(); - // console.log("Create tree info ", dt.getTime() - d.getTime()); - // return { - // shearedToFull: shearedToFull, - // fullToSheared: fullToSheared, - // tree: new BPTree(newBitArray, names, lengths, null), - // }; - // }; - return BPTree; }); diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 8be3a57ca..522dbdeca 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -2374,7 +2374,6 @@ define([ var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; // convert observation IDs to _treeData keys. Notably, this includes // converting the values of uniqueValueToFeatures from Arrays to Sets. - var obs = {}; _.each(sortedUniqueValues, function (uniqueVal, i) { uniqueVal = sortedUniqueValues[i]; @@ -2389,6 +2388,7 @@ define([ undefined, reverse ); + // colors for drawing the tree var cm = colorer.getMapRGB(); @@ -2446,44 +2446,30 @@ define([ i, j; - if (!ignoreAbsentTips) { - // find "non-represented" tips - // Note: the following uses postorder traversal - for (i of this._tree.postorderTraversal()) { - if (tree.isleaf(tree.postorderselect(i))) { - var represented = false; - for (j = 0; j < categories.length; j++) { - if (obs[categories[j]].has(i)) { - represented = true; - break; - } - } - if (!represented) notRepresented.add(i); - } - } - } - - // assign internal nodes to appropriate category based on children - // iterate using postorder - // Note that, although we don't explicitly iterate over the - // root (at index tree.size) in this loop, we iterate over all its - // descendants; so in the event that all leaves are unique, - // the root can still get assigned to a group. + // Note: the following uses postorder traversal for (i of this._tree.postorderTraversal()) { var node = i; - var parent = tree.postorder(tree.parent(tree.postorderselect(i))); - + var parent = tree.postorder( + tree.parent(tree.postorderselect(node)) + ); + var represented = false; for (j = 0; j < categories.length; j++) { - category = categories[j]; - + var category = categories[j]; // add internal nodes to groups if (obs[category].has(node)) { obs[category].add(parent); + represented = true; } - if (notRepresented.has(node)) { - notRepresented.add(parent); + } + + if (tree.isleaf(tree.postorderselect(node))) { + if (!represented && !ignoreAbsentTips) { + notRepresented.add(node); } } + if (notRepresented.has(node)) { + notRepresented.add(parent); + } } var result = util.keepUniqueKeys(obs, notRepresented); @@ -2534,13 +2520,15 @@ define([ this.setNodeInfo(node, "isColored", false); this.setNodeInfo(node, "visible", true); } - this._collapsedClades = {}; this._dontCollapse = new Set(); this._collapsedCladeBuffer = []; this._drawer.loadThickNodeBuff([]); this._drawer.loadCladeBuff([]); this._group = new Array(this._tree.size + 1).fill(-1); - this._drawer.loadTreeCoordsBuff(this.getTreeCoords()); + if (Object.keys(this._collapsedClades).length > 0) { + this._drawer.loadTreeCoordsBuff(this.getTreeCoords()); + } + this._collapsedClades = {}; }; /** @@ -3601,7 +3589,6 @@ define([ this._tree.unshear(); var scope = this; var removeNodes = new Set(); - var d = new Date() shearMap.forEach(function (values, cat) { var fmInfo = scope.getUniqueFeatureMetadataInfo(cat, "tip"); var uniqueValueToFeatures = fmInfo.uniqueValueToFeatures; @@ -3612,50 +3599,20 @@ define([ } }); }); - var dt = new Date(); - console.log("Create `removeNodes` ", dt.getTime() - d.getTime()); - - // remove removeNodes - d = new Date(); - var allNodes = _.range(1, this._tree.size + 1); - allNodes.shift(); - - allNodes = new Set(allNodes); - var keepNodes = new Set( - [...allNodes].filter((x) => !removeNodes.has(x)) - ); - dt = new Date(); - console.log("Create `keepNodes` ", dt.getTime() - d.getTime()); - - var keepNames = []; - for (var node of keepNodes) { - var name = this._tree.name(this._tree.postorderselect(node)); - keepNames.push(name); - } if (this.isCommunityPlot) { - this._biom.setIngnoreNodes(new Set(keepNodes)); + this._biom.setIngnoreNodes(removeNodes); } - // this._tree.shear(new Set(keepNames)); - d = new Date(); - this._tree.shear(new Set(removeNodes)); - dt = new Date(); - console.log("Shear tree ", dt.getTime() - d.getTime()); + this._tree.shear(removeNodes); - d = new Date(); var nodeNames = this._tree.getAllNames(); // Don't include nodes with the name null (i.e. nodes without a // specified name in the Newick file) in the auto-complete. nodeNames = nodeNames.filter((n) => n !== null); this._events.autocomplete(nodeNames); - dt = new Date(); - console.log("Extract names for filter ", dt.getTime() - d.getTime()); - d = new Date(); this.getLayoutInfo(); - dt = new Date(); - console.log("Calc coords ", dt.getTime() - d.getTime()); // Undraw or redraw barplots as needed (assuming barplots are supported // in the first place, of course; if no feature or sample metadata at @@ -3674,14 +3631,3 @@ define([ return Empress; }); - -var d = new Date(); -var test = []; -for (i = 0; i < 1512754; i++) { - test.push(i) -} -for (i = 0; i < 1512754; i++) { - test.pop() -} -var dt = new Date(); -console.log(dt.getTime() - d.getTime()); \ No newline at end of file diff --git a/empress/support_files/js/side-panel-handler.js b/empress/support_files/js/side-panel-handler.js index e840159e5..ec55729f4 100644 --- a/empress/support_files/js/side-panel-handler.js +++ b/empress/support_files/js/side-panel-handler.js @@ -302,6 +302,7 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { if (collapseChk.checked) { this.empress.collapseClades(); } + var lw = util.parseAndValidateNum(lwInput); this.empress.thickenColoredNodes(lw); @@ -459,7 +460,6 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { }; SidePanel.prototype.shearUpdate = function () { - var d = new Date(); if (this.sChk.checked) { this.sUpdateBtn.click(); } @@ -467,8 +467,6 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { if (this.fChk.checked) { this.fUpdateBtn.click(); } - var dt = new Date(); - console.log("Shear update ", dt.getTime() - d.getTime()); }; /** diff --git a/tests/test-bp-tree.js b/tests/test-bp-tree.js index 2d8c56eaf..c588347b0 100644 --- a/tests/test-bp-tree.js +++ b/tests/test-bp-tree.js @@ -928,7 +928,7 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { null ); - var keep = new Set(["4", "6", "7", "10", "11"]); + var remove = new Set([1]); var shearedToFull = new Map([ [1, 2], [2, 3], @@ -953,7 +953,7 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { [10, 9], [11, 10], ]); - var result = preShearBPTree.shear(keep); + var result = preShearBPTree.shear(remove); deepEqual(result.tree.b_, [ 1, 1, @@ -1005,7 +1005,7 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { deepEqual(result.shearedToFull, shearedToFull); deepEqual(result.fullToSheared, fullToSheared); - keep = new Set(["7", "10", "11"]); + remove = new Set([1, 2, 3]); shearedToFull = new Map([ [1, 6], [2, 7], @@ -1022,7 +1022,7 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { [10, 5], [11, 6], ]); - result = preShearBPTree.shear(keep); + result = preShearBPTree.shear(remove); deepEqual(result.tree.b_, [1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0]); deepEqual(result.tree.names_, [ null, @@ -1037,15 +1037,23 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { deepEqual(result.shearedToFull, shearedToFull); deepEqual(result.fullToSheared, fullToSheared); - keep = new Set([]); + remove = new Set([1, 2, 3, 6, 7, 8]); shearedToFull = new Map([[1, 11]]); fullToSheared = new Map([[11, 1]]); - result = preShearBPTree.shear(keep); + result = preShearBPTree.shear(remove); deepEqual(result.tree.b_, [1, 0]); deepEqual(result.tree.names_, [null, "r"]); deepEqual(result.tree.lengths_, [null, 11]); deepEqual(result.shearedToFull, shearedToFull); deepEqual(result.fullToSheared, fullToSheared); + + remove = new Set([3, 6, 7, 8]); + result = preShearBPTree.shear(remove); + deepEqual( + result.tree.names_, + [null, "3", "4", "2", "r"] + // [null, "3", "4", "2", "7", "10", "11", "9", "8", "r"] + ); }); }); }); diff --git a/tests/test-tree-controller.js b/tests/test-tree-controller.js index c511a6260..48d8ceabf 100644 --- a/tests/test-tree-controller.js +++ b/tests/test-tree-controller.js @@ -25,37 +25,35 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( }); test("Test shear", function () { - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([2, 3])); // checks to make sure correct names are kept - var shearNames = [null, "t2", "t3", "i4", "i5", "r"]; + var shearNames = [null, "t1", "i5", "t6", "r"]; var resutlNames = this.treeController.model.shearedTree.names_; deepEqual(resutlNames, shearNames); - var shearLengths = [null, 2, 3, 4, 5, null]; + var shearLengths = [null, 1, 5, 6, null]; var resultLengts = this.treeController.model.shearedTree.lengths_; deepEqual(resultLengts, shearLengths); // checks to make sure structre of tree is correct - var shearTree = [1, 1, 1, 1, 0, 1, 0, 0, 0, 0]; + var shearTree = [1, 1, 1, 0, 0, 1, 0, 0]; var resultTree = this.treeController.model.shearedTree.b_; deepEqual(resultTree, shearTree); // checks to make sure the mappings from orignal tree to shear tree // is correct and vice-versa var fullToSheared = new Map([ - [2, 1], - [3, 2], - [4, 3], - [5, 4], - [7, 5], + [1, 1], + [5, 2], + [6, 3], + [7, 4], ]); var shearedToFull = new Map([ - [1, 2], - [2, 3], - [3, 4], - [4, 5], - [5, 7], + [1, 1], + [2, 5], + [3, 6], + [4, 7], ]); var resultOrigToCur = this.treeController.model.fullToSheared; var resultCurToOrig = this.treeController.model.shearedToFull; @@ -64,7 +62,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( }); test("Test unshear", function () { - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); this.treeController.unshear(); deepEqual(this.treeController.model.shearedTree.names_, this.names); @@ -87,7 +85,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( }); test("Test postorderTraversal", function () { - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var nodes = [2, 3, 4, 5, 7]; var result = [ ...this.treeController.postorderTraversal((includeRoot = true)), @@ -111,7 +109,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( }); test("Test getLengthStats", function () { - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var stats = { avg: 3.5, min: 2, @@ -139,7 +137,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( }); test("Test getAllNames", function () { - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var shearNames = ["t2", "t3", "i4", "i5", "r"]; var resutlNames = this.treeController.getAllNames(); deepEqual(resutlNames, shearNames); @@ -150,7 +148,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( }); test("Test numleaves", function () { - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); equal(this.treeController.numleaves(), 2); this.treeController.unshear(); @@ -195,7 +193,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( test("Test fchild", function () { // fchild's input/output is in respect to the original tree. // However, fchild will use the topology of the sheared tree. - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var index = this.treeController.postorderselect(5); var fchild = this.treeController.fchild(index); var expected = this.treeController.postorderselect(4); @@ -211,7 +209,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( test("Test lchild", function () { // lchild's input/output is in respect to the original tree. // However, lchild will use the topology of the sheared tree. - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var index = this.treeController.postorderselect(7); var lchild = this.treeController.lchild(index); var expected = this.treeController.postorderselect(5); @@ -227,7 +225,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( test("Test nsibling", function () { // nsibling's input/output is in respect to the original tree. // However, nsibling will use the topology of the sheared tree. - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var index = this.treeController.postorderselect(5); var nsibling = this.treeController.nsibling(index); var expected = 0; // doesn't have a next sibling @@ -243,7 +241,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( test("Test psibling", function () { // psibling's input/output is in respect to the original tree. // However, psibling will use the topology of the sheared tree. - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var index = this.treeController.postorderselect(4); var psibling = this.treeController.psibling(index); var expected = 0; // doesn't have a next sibling @@ -289,7 +287,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( test("Test inOrderTraversal", function () { // inOrderTraversal's input/output is in respect to the original tree. // However, inOrderTraversal will use the topology of the sheared tree. - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var expected = [7, 5, 4, 2, 3]; var result = [ ...this.treeController.inOrderTraversal((includeRoot = true)), @@ -317,7 +315,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( test("Test getTotalLength", function () { // getTotalLength's input/output is in respect to the original tree. // However, getTotalLength will use the topology of the sheared tree. - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var result = this.treeController.getTotalLength(2, 7); equal(result, 11); @@ -329,7 +327,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( test("Test findTips", function () { // findTips's input/output is in respect to the original tree. // However, findTips will use the topology of the sheared tree. - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var result = this.treeController.findTips(5); deepEqual(result, [2, 3]); @@ -341,7 +339,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( test("Test getNumTips", function () { // getNumTips's input/output is in respect to the original tree. // However, getNumTips will use the topology of the sheared tree. - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var result = this.treeController.getNumTips(5); deepEqual(result, 2); @@ -351,7 +349,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( }); test("Test containsNode", function () { - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var result = this.treeController.containsNode("t1"); equal(result, false); @@ -363,7 +361,7 @@ require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( test("Test getNodesWithName", function () { // getNodesWithName's input/output is in respect to the original tree. // However, getNodesWithName will use the topology of the sheared tree. - this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.shear(new Set([1, 6])); var result = this.treeController.getNodesWithName("t2"); deepEqual(result, [2]); result = this.treeController.getNodesWithName("t1"); diff --git a/tests/utilities-for-testing.js b/tests/utilities-for-testing.js index 866f8b76c..b86aab243 100644 --- a/tests/utilities-for-testing.js +++ b/tests/utilities-for-testing.js @@ -23,7 +23,7 @@ define(["Empress", "BPTree", "BiomTable"], function ( // tree comes from the following newick string // ((1,(2,3)4)5,6)7; var tree = new BPTree( - new Uint8Array([1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0]), + [1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0], // see https://github.com/biocore/empress/issues/311 ["", "1", "2", "3", "internal", "internal", null, "root"], [0, 1, 2, 3, 4, 5, 6, 7], From 4b4d8e32d9be36a58d28dcc9d4eb0ab420273423 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 8 Apr 2021 18:13:04 -0700 Subject: [PATCH 32/63] finished documenting --- empress/support_files/js/biom-table.js | 29 ++- empress/support_files/js/bp-tree.js | 50 ++-- empress/support_files/js/empress.js | 86 ++++--- empress/support_files/js/shearer.js | 214 +++++++++++++----- .../support_files/js/side-panel-handler.js | 3 + empress/support_files/js/tree-controller.js | 11 + .../templates/empress-template.html | 12 +- 7 files changed, 287 insertions(+), 118 deletions(-) diff --git a/empress/support_files/js/biom-table.js b/empress/support_files/js/biom-table.js index b822de74a..0614c31d4 100644 --- a/empress/support_files/js/biom-table.js +++ b/empress/support_files/js/biom-table.js @@ -86,7 +86,13 @@ define(["underscore", "util"], function (_, util) { this._tbl = tbl; this._smCols = smCols; this._sm = sm; - this.ignorefIDs = new Set(); + + /** + * A set of feature ids to ignore. This is whenever the tree is sheared + * and will contain the features id (tips) that were removed. + * @ type {Set} + */ + this.ignorefIdx = new Set(); } /** @@ -279,7 +285,9 @@ define(["underscore", "util"], function (_, util) { var cVal; var addSampleFeatures = function (sIdx, cVal) { _.each(scope._tbl[sIdx], function (fIdx) { - valueToFeatureIdxs[cVal].add(fIdx); + if (!scope.ignorefIdx.has(fIdx)) { + valueToFeatureIdxs[cVal].add(fIdx); + } }); }; // For each sample... @@ -583,7 +591,8 @@ define(["underscore", "util"], function (_, util) { var fID2Freqs = {}; var totalSampleCount; _.each(this._fIDs, function (fID, fIdx) { - if (scope.ignorefIDs.has(fID)) { + // we dont want to consider features that have been marked as ignore + if (scope.ignorefIdx.has(fIdx)) { return; } totalSampleCount = fIdx2SampleCt[fIdx]; @@ -599,8 +608,20 @@ define(["underscore", "util"], function (_, util) { return fID2Freqs; }; + /** + * Set which features to ignore. Features in this set will not be + * considered in funcitons such as getObsBy() or getFrequencyMap() + * + * @param {Set} nodes A set of feature ids to ignore + */ BIOMTable.prototype.setIngnoreNodes = function (nodes) { - this.ignorefIDs = nodes; + var scope = this; + + // convert feature ids to feature indices + var nodeIdx = _.map([...nodes], (fId) => { + return scope._getFeatureIndexFromID(fId); + }); + this.ignorefIdx = new Set(nodeIdx); }; return BIOMTable; diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index f91a7a196..3723b73ca 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -192,6 +192,12 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { */ this._nameToNodes = {}; + /** + * @type {Array} + * @private + * Cache Parent nodes. This will help speed up a lot of methods that + * make calls to parent. + */ this._pCache = []; var pStack = []; for (i = 0; i < this.b_.length - 1; i++) { @@ -472,11 +478,9 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { // i represents the root node if (i === this.root() || i === this.b_.length - 1) { return -1; - } else { - return this._pCache[i]; } - return this.enclose(i); + return this._pCache[i]; }; /** @@ -726,7 +730,9 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { // find first and last preorder positions of the subtree spanned // by the current internal node var n = this.postorderselect(nodeKey); - if (this.isleaf(n) && nodeKey !== this.postorder(this.root())) { + + // check for root to account for cases when all tips have been sheared + if (this.isleaf(n) && nodeKey !== this.postorder(this.root()) ) { throw "Error: " + nodeKey + " is a tip!"; } var start = this.preorder(this.fchild(n)); @@ -1012,18 +1018,14 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { // closure var scope = this; - // create new names and lengths array - var names = [null]; - var lengths = [null]; - // create new bit array var mask = _.clone(this.b_); - var i; + var i, node; // remove tips for (i of removeTips) { - var node = this.postorderselect(i); + node = this.postorderselect(i); mask[node] = undefined; mask[node + 1] = undefined; } @@ -1032,26 +1034,38 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { var nodeStack = []; for (i = mask.length - 1; i > 0; i--) { if (mask[i] === 0) { - // close par + // close parentheses if (mask[i - 1] === 0) { + // close parentheses represents internal node nodeStack.push([i, null]); } else if (mask[i - 1] === 1) { + // close parentheses represents non-removed tip + // thus we mark it as false so it is not removed nodeStack.push([i, false]); } else if (mask[i - 1] === undefined) { + // close paretheses represents an internal node + // with at least one removed tip so we temporarly mark it + // as true to remove nodeStack.push([i, true]); } } else if (mask[i] === 1) { - var parent = nodeStack.pop(); - if (parent[1] === true) { + //open parentheses + node = nodeStack.pop(); + if (node[1] === true) { + // remove internal node mask[i] = undefined; - mask[parent[0]] = undefined; - } else if (parent[1] === false) { + mask[node[0]] = undefined; + } else if (node[1] === false) { // need explicitly check false since it can be null + // if node[1] is false that means it contains a tip and thus + // we need to mark its parent as false so it is not removed nodeStack[nodeStack.length - 1][1] = false; } if (nodeStack[nodeStack.length - 1][1] === null) { - nodeStack[nodeStack.length - 1][1] = parent[1]; + // if null then we set its remove status to whatever node + // was + nodeStack[nodeStack.length - 1][1] = node[1]; } } } @@ -1060,6 +1074,9 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { var shearedToFull = new Map(); var fullToSheared = new Map(); var postorderPos = 1; + // create new names and lengths array + var names = [null]; + var lengths = [null]; for (i = 0; i < mask.length; i++) { if (mask[i] !== undefined) { newBitArray.push(mask[i]); @@ -1067,7 +1084,6 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { // get name and length of node // Note: names and lengths of nodes are stored in postorder - if (mask[i] === 0) { names.push(this.name(i)); lengths.push(this.length(i)); diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 47ec3cb9f..6cf3bdb76 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -531,11 +531,7 @@ define([ Empress.prototype.initialize = function () { this._drawer.initialize(); this._events.setMouseEvents(); - var nodeNames = this._tree.getAllNames(); - // Don't include nodes with the name null (i.e. nodes without a - // specified name in the Newick file) in the auto-complete. - nodeNames = nodeNames.filter((n) => n !== null); - this._events.autocomplete(nodeNames); + this.setAutoCompleteNames(); this.getLayoutInfo(); this.centerLayoutAvgPoint(); @@ -1748,12 +1744,6 @@ define([ // Do most of the hard work: compute the frequencies for each tip (only // the tips present in the BIOM table, that is) var feature2freqs = this._biom.getFrequencyMap(layer.colorBySMField); - // var nodes = new Set([...this._tree.postorderTraversal()]); - // _.each(feature2freqs, function (blah, node) { - // if (!nodes.has(parseInt(node))) { - // delete feature2freqs[node]; - // } - // }); // Only bother computing the halfyrscf / halfAngleRange value we need. // (this._tree.numleaves() does iterate over the full tree, at least @@ -2230,18 +2220,6 @@ define([ this.drawTree(); }; - /** - * - */ - Empress.prototype.getUniqueSampleMetadataInfo = function (cat) { - var obs = this._biom.getObsBy(cat); - var nodes = new Set([...this._tree.postorderTraversal()]); - _.each(obs, function (featuresWithSMValue, smValue) { - obs[smValue] = featuresWithSMValue.filter((x) => nodes.has(x)); - }); - return obs; - }; - /** * Color the tree using sample metadata * @@ -2262,7 +2240,7 @@ define([ reverse = false ) { var tree = this._tree; - var obs = this.getUniqueSampleMetadataInfo(cat); + var obs = this._biom.getObsBy(cat); var categories = Object.keys(obs); // Assign colors to categories @@ -2277,6 +2255,10 @@ define([ var cm = colorer.getMapRGB(); // colors for the legend var keyInfo = colorer.getMapHex(); + + // if the tree has been sheared then categories in obs maybe empty. + // getObsBy() does not filter out those categories so that the same + // color can be assigned to each value in obs. for (var key in keyInfo) { if (obs[key].length === 0) { delete keyInfo[key]; @@ -2460,8 +2442,11 @@ define([ if (!_.has(uniqueValueToFeatures, fmVal)) { uniqueValueToFeatures[fmVal] = []; } + // need to convert to integer node = parseInt(node); + + // ignore nodes that have been sheared if (!nodes.has(node)) { return; } @@ -2531,6 +2516,10 @@ define([ // colors for the legend var keyInfo = colorer.getMapHex(); + + // if the tree has been sheared then categories in obs maybe empty. + // getUniqueFeatureMetadataInfo() does not filter out those categories + // so that the same color can be assigned to each value in obs. for (var key in keyInfo) { if (uniqueValueToFeatures[key].length === 0) { delete keyInfo[key]; @@ -2662,6 +2651,10 @@ define([ this._drawer.loadThickNodeBuff([]); this._drawer.loadCladeBuff([]); this._group = new Array(this._tree.size + 1).fill(-1); + + // if _collapsedClades is empty then there is no need to call + // getTreeCoords which can save a fair amount of time depending on the + // size of the tree if (Object.keys(this._collapsedClades).length > 0) { this._drawer.loadTreeCoordsBuff(this.getTreeCoords()); } @@ -2736,6 +2729,16 @@ define([ // stuff to only change whenever the tree is redrawn. this.thickenColoredNodes(this._currentLineWidth); + this.redrawBarPlotsToMatchLayout(); + this.centerLayoutAvgPoint(); + }; + + /** + * Redraw the barplot to match the current layout. If the current layout is + * "Unrooted" then this will remove the barplots from canvas. + * + */ + Empress.prototype.redrawBarPlotsToMatchLayout = function() { // Undraw or redraw barplots as needed (assuming barplots are supported // in the first place, of course; if no feature or sample metadata at // all was passed then barplots are not available :() @@ -2749,7 +2752,6 @@ define([ this.drawBarplots(); } } - this.centerLayoutAvgPoint(); }; /** @@ -3238,7 +3240,7 @@ define([ // step 1: find all nodes in the clade. // Note: cladeNodes is an array of nodes arranged in postorder fashion - var cladeNodes = this._tree.getCladeNodes(rootNode); + var cladeNodes = this._tree.getCladeNodes(parseInt(rootNode)); // use the left most child in the clade to initialize currentCladeInfo var currentCladeInfo = { @@ -3719,6 +3721,18 @@ define([ } }; + /** + * This will fill the autocomplete search bar with the names of the current + * tree. + */ + Empress.prototype.setAutoCompleteNames = function() { + var nodeNames = this._tree.getAllNames(); + // Don't include nodes with the name null (i.e. nodes without a + // specified name in the Newick file) in the auto-complete. + nodeNames = nodeNames.filter((n) => n !== null); + this._events.autocomplete(nodeNames); + }; + /** * This will shear/unshear */ @@ -3743,27 +3757,11 @@ define([ this._tree.shear(removeNodes); - var nodeNames = this._tree.getAllNames(); - // Don't include nodes with the name null (i.e. nodes without a - // specified name in the Newick file) in the auto-complete. - nodeNames = nodeNames.filter((n) => n !== null); - this._events.autocomplete(nodeNames); + this.setAutoCompleteNames(); this.getLayoutInfo(); - // Undraw or redraw barplots as needed (assuming barplots are supported - // in the first place, of course; if no feature or sample metadata at - // all was passed then barplots are not available :() - if (!_.isNull(this._barplotPanel)) { - var supported = this._barplotPanel.updateLayoutAvailability( - this._currentLayout - ); - if (!supported && this._barplotsDrawn) { - this.undrawBarplots(); - } else if (supported && this._barplotPanel.enabled) { - this.drawBarplots(); - } - } + this.redrawBarPlotsToMatchLayout(); }; return Empress; diff --git a/empress/support_files/js/shearer.js b/empress/support_files/js/shearer.js index 4dc49cc46..e15c4aa5d 100644 --- a/empress/support_files/js/shearer.js +++ b/empress/support_files/js/shearer.js @@ -1,9 +1,14 @@ -define(["underscore", "util", "TreeController"], function ( + define(["underscore", "util", "TreeController"], function ( _, util, TreeController ) { - function Filter( + /** + * @class ShearLayer + * + * Create a new shear layer and adds it to the shear panel + */ + function ShearLayer( fCol, fVals, container, @@ -37,6 +42,7 @@ define(["underscore", "util", "TreeController"], function ( legendTitle.innerText = this.fCol; legendTitle.classList.add("legend-title"); + // create the select all button var button = document.createElement("button"); button.innerText = "Select all"; button.onclick = function () { @@ -47,6 +53,7 @@ define(["underscore", "util", "TreeController"], function ( }; chkBoxLegendDiv.appendChild(button); + // create the unselect all button button = document.createElement("button"); button.innerText = "Unselect all"; button.onclick = function () { @@ -56,6 +63,7 @@ define(["underscore", "util", "TreeController"], function ( unselectAllFuntion(scope.fCol, _.clone(scope.values)); }; chkBoxLegendDiv.appendChild(button); + // create chcbox div var legendChkBoxs = document.createElement("div"); chkBoxLegendDiv.appendChild(legendChkBoxs); @@ -66,7 +74,6 @@ define(["underscore", "util", "TreeController"], function ( _.each(this.fVals, function (val) { scope.values.push(val); var row = document.createElement("tr"); - var id = scope.fCol.replace(" ", "-") + "-" + val; // add checkbox @@ -77,12 +84,16 @@ define(["underscore", "util", "TreeController"], function ( input.onchange = function () { chkBxClickFunction(!input.checked, scope.fCol, val); }; + + // the select/unselect functions that the "Select all" and + // "Unselect all" buttons will call input.select = function () { input.checked = true; }; input.unselect = function () { input.checked = false; }; + scope.inputs.push(input); dataCheck.appendChild(input); row.appendChild(dataCheck); @@ -122,46 +133,75 @@ define(["underscore", "util", "TreeController"], function ( this.layerDiv.appendChild(document.createElement("hr")); } - function Model(empress, container) { + /** + * @class ShearModel + * + * The model for Shearer. This model is responsible for maintaining updating + * empress whenever a user clicks on a shear option in one of the shear + * layers. This model is also responsible for notifying its observers + * whenever the shear status of the tree has changed. + */ + function ShearModel(empress, container) { this.empress = empress; - this.filters = new Map(); + this.layers = new Map(); this.shearMap = new Map(); this.container = container; this.observers = []; } - Model.prototype.addLayer = function (fCol) { - var fVals = this.empress.getUniqueFeatureMetadataInfo(fCol, "tip") + /** + * Adds a shear layer to the shear panel. + * + * @param{String} layer The feateure metadata column to create a shear layer + * from. + */ + ShearModel.prototype.addLayer = function (layer) { + var fVals = this.empress.getUniqueFeatureMetadataInfo(layer, "tip") .sortedUniqueValues; - var layer = new Filter( - fCol, + var layer = new ShearLayer( + layer, fVals, this.container, - (add, col, val) => { - Model.addRemoveShearItem(this, add, col, val); + (add, layer, val) => { + ShearModel.addRemoveShearItem(this, add, layer, val); }, - (col) => { - Model.removeLayer(this, col); + (layer) => { + ShearModel.removeLayer(this, layer); }, - (col) => { - Model.clearShearMaplFilter(this, col); + (layer) => { + ShearModel.clearShearMapLayer(this, layer); }, - (filter, values) => { - Model.setShearMapFilter(this, filter, values); + (layer, values) => { + ShearModel.setShearMapLayer(this, layer, values); } ); - this.filters.set(fCol, layer); + this.layers.set(layer, layer); }; - Model.prototype.getShearItem = function (fCol) { - return this.shearMap.get(fCol); + /** + * Returns the feature values the have been unselected (i.e. sheared) from + * a particular shear layer. + * + * @param{String} layer The name of shear layer + */ + ShearModel.prototype.getShearLayer = function (layer) { + return this.shearMap.get(layer); }; - Model.prototype.hasLayer = function (fCol) { - return this.filters.has(fCol); + /** + * Checks if a shear layer has been create for a particular feature metadata + * column. + * + * @param{String} layer The feature metadata column to check + */ + ShearModel.prototype.hasLayer = function (layer) { + return this.layers.has(layer); }; - Model.prototype.notify = function () { + /** + * Notifies all observers whenever the model has changed. + */ + ShearModel.prototype.notify = function () { this.empress.shear(this.shearMap); this.empress.drawTree(); _.each(this.observers, function (obs) { @@ -169,75 +209,142 @@ define(["underscore", "util", "TreeController"], function ( }); }; - Model.prototype.registerObserver = function (obs) { + /** + * Registers an observer to the model which will then be notified whenever + * the model is updated. Note this object must implement a shearUpdate() + * method. + * + * @param{Object} obs The object to register. + */ + ShearModel.prototype.registerObserver = function (obs) { this.observers.push(obs); }; - Model.clearShearMaplFilter = function (model, filter) { - model.shearMap.set(filter, []); + /** + * Removes a shear layer from a ShearModel + * @param{ShearModel} model The ShearModel to use + * @param{String} layer The name of layer to remove. + */ + ShearModel.removeLayer = function (model, layer) { + model.layers.delete(layer); + model.shearMap.delete(layer); + model.notify(); + }; + + /** + * Clears the shearMap. + * + * @param{ShearModel} model The ShearModel to use + * @param{String} layer The feature metadata column name of the shear layer + */ + ShearModel.clearShearMapLayer = function (model, layer) { + model.shearMap.set(layer, []); model.notify(); }; - Model.setShearMapFilter = function (model, filter, values) { - model.shearMap.set(filter, values); + /** + * sets a shear layer within the shearMap. + * + * @param{ShearModel} model The ShearModel to use. + * @param{String} layer The feature metadata column name of the shear layer + * @param{Array} values An array of feature metadata value + */ + ShearModel.setShearMapLayer = function (model, layer, values) { + model.shearMap.set(layer, values); model.notify(); }; - Model.addRemoveShearItem = function (model, remove, col, val) { + /** + * Adds or removes a shear value from a shear layer. + * @param{ShearModel} model The ShearModel to use. + * @param{Boolean} remove Whether or not to remove val from the shear layer + * @param{String} layer The name of feature metadata column of shear layer + * @param{String} val The feature metadata column value to add or remove + * from layer. + */ + ShearModel.addRemoveShearItem = function (model, remove, layer, val) { if (remove) { - Model.addShearItem(model, col, val); + ShearModel.addShearItem(model, layer, val); } else { - Model.removeShearItem(model, col, val); + ShearModel.removeShearItem(model, layer, val); } }; - Model.removeLayer = function (model, fCol) { - model.filters.delete(fCol); - model.shearMap.delete(fCol); - model.notify(); - }; - - Model.addShearItem = function (model, fCol, fVal) { - if (model.shearMap.has(fCol)) { - model.shearMap.get(fCol).push(fVal); + /** + * Adds a shear value from a shear layer. + * @param{ShearModel} model The ShearModel to use. + * @param{String} layer The name of feature metadata column of shear layer + * @param{String} val The feature metadata column value to add or remove + * from layer. + */ + ShearModel.addShearItem = function (model, layer, val) { + if (model.shearMap.has(layer)) { + model.shearMap.get(layer).push(val); } else { - model.shearMap.set(fCol, [fVal]); + model.shearMap.set(layer, [val]); } model.notify(); }; - Model.removeShearItem = function (model, fCol, fVal) { - var items = model.getShearItem(fCol); + /** + * Removes a shear value from a shear layer. + * @param{ShearModel} model The ShearModel to use. + * @param{String} layer The name of feature metadata column of shear layer + * @param{String} val The feature metadata column value to add or remove + * from layer. + */ + ShearModel.removeShearItem = function (model, layer, val) { + var items = model.getShearLayer(layer); if (items === undefined) { return; } - var index = items.indexOf(fVal); + var index = items.indexOf(val); if (index > -1) { items.splice(index, 1); } model.notify(); }; - function Controller(empress, container) { - this.model = new Model(empress, container); + /** + * @class ShearController + * + * The controller for a ShearModel. + */ + function ShearController(empress, container) { + this.model = new ShearModel(empress, container); } - Controller.prototype.addLayer = function (fCol) { - if (!this.model.hasLayer(fCol)) { - this.model.addLayer(fCol); + /** + * Adds a layer to the model. + * @param{String} layer A feature metadata column name + */ + ShearController.prototype.addLayer = function (layer) { + if (!this.model.hasLayer(layer)) { + this.model.addLayer(layer); } }; - Controller.prototype.registerObserver = function (obs) { + /** + * Registers an observer to the model. + * + * @oaram{Object} obs The object to register to the model + */ + ShearController.prototype.registerObserver = function (obs) { this.model.registerObserver(obs); }; + /** + * @class Shearer + * + * This is the exposed only exposed class of this closure and the one that + * the rest of the empress code base will interact with. + */ function Shearer(empress, fCols) { this.fCols = fCols; this.shearSelect = document.getElementById("shear-feature-select"); this.addLayerButton = document.getElementById("shear-add-btn"); this.shearContainer = document.getElementById("shear-legends"); - this.controller = new Controller(empress, this.shearContainer); + this.controller = new ShearController(empress, this.shearContainer); var scope = this; _.each(this.fCols, function (col) { @@ -252,6 +359,11 @@ define(["underscore", "util", "TreeController"], function ( }; } + /** + * Registers an observer to the model. + * + * @oaram{Object} obs The object to register to the model + */ Shearer.prototype.registerObserver = function (obs) { this.controller.registerObserver(obs); }; diff --git a/empress/support_files/js/side-panel-handler.js b/empress/support_files/js/side-panel-handler.js index ec55729f4..6304e8e19 100644 --- a/empress/support_files/js/side-panel-handler.js +++ b/empress/support_files/js/side-panel-handler.js @@ -459,6 +459,9 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { } }; + /** + * This method is called whenever the empress tree is sheared + */ SidePanel.prototype.shearUpdate = function () { if (this.sChk.checked) { this.sUpdateBtn.click(); diff --git a/empress/support_files/js/tree-controller.js b/empress/support_files/js/tree-controller.js index 20a887049..dbd9d0119 100644 --- a/empress/support_files/js/tree-controller.js +++ b/empress/support_files/js/tree-controller.js @@ -516,6 +516,17 @@ define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { return nodes; }; + /** + * Returns all nodes in the clade whose root is node. + * + * Note: elements in the returned array are keys in this._treeData + * also, the returned array is sorted in a postorder fashion + * + * @param {Number} cladeRoot The root of the clade. An error is thrown if + * cladeRoot is not a valid node. + * + * @return {Array} The nodes in the clade + */ TreeController.prototype.getCladeNodes = function (cladeRoot) { return this.model.getCladeNodes(cladeRoot); }; diff --git a/empress/support_files/templates/empress-template.html b/empress/support_files/templates/empress-template.html index 03bd61c99..b2233c155 100644 --- a/empress/support_files/templates/empress-template.html +++ b/empress/support_files/templates/empress-template.html @@ -131,7 +131,6 @@ BarplotLayer, BarplotPanel, BIOMTable, Empress, Legend, Colorer, VectorOps, CanvasEvents, SelectedNodeMenu, util, LayoutsUtil, ExportUtil, Shearer) { - window.counter = 0; // initialze the tree and model var tree = new BPTree( {{ tree }}, @@ -184,7 +183,6 @@ empress.getFeatureMetadataCategories() ); shearer.registerObserver(sPanel); - // shearer.registerObserver(empress._barplotPanel); // Only show the sample metadata coloring / animation panels if a // feature table and sample metadata file were provided @@ -217,6 +215,16 @@ $(".needs-feature-metadata").addClass("hidden"); } + var statsButton = document.getElementById("stats-btn"); + statsButton.shearUpdate = () => { + statsButton.classList.remove("unpopulated"); + if (!statsButton.classList.contains("unpopulated")) { + sPanel.populateTreeStats(); + } + }; + shearer.registerObserver(statsButton); + + // make all tabs collapsable document.querySelectorAll(".collapsible").forEach(function(btn) { btn.addEventListener("click", function() { From 3d05f17151d2c9a1fabba67553f48c98711f600b Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 8 Apr 2021 18:16:55 -0700 Subject: [PATCH 33/63] style fix --- empress/support_files/js/biom-table.js | 6 ++--- empress/support_files/js/bp-tree.js | 2 +- empress/support_files/js/empress.js | 8 +++---- empress/support_files/js/shearer.js | 31 +++++++++++++------------- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/empress/support_files/js/biom-table.js b/empress/support_files/js/biom-table.js index 0614c31d4..4a1e50dc0 100644 --- a/empress/support_files/js/biom-table.js +++ b/empress/support_files/js/biom-table.js @@ -90,7 +90,7 @@ define(["underscore", "util"], function (_, util) { /** * A set of feature ids to ignore. This is whenever the tree is sheared * and will contain the features id (tips) that were removed. - * @ type {Set} + * @ type {Set} */ this.ignorefIdx = new Set(); } @@ -286,8 +286,8 @@ define(["underscore", "util"], function (_, util) { var addSampleFeatures = function (sIdx, cVal) { _.each(scope._tbl[sIdx], function (fIdx) { if (!scope.ignorefIdx.has(fIdx)) { - valueToFeatureIdxs[cVal].add(fIdx); - } + valueToFeatureIdxs[cVal].add(fIdx); + } }); }; // For each sample... diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index 3723b73ca..ff2f7869b 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -732,7 +732,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { var n = this.postorderselect(nodeKey); // check for root to account for cases when all tips have been sheared - if (this.isleaf(n) && nodeKey !== this.postorder(this.root()) ) { + if (this.isleaf(n) && nodeKey !== this.postorder(this.root())) { throw "Error: " + nodeKey + " is a tip!"; } var start = this.preorder(this.fchild(n)); diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 6cf3bdb76..a4c2f2a02 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -2257,7 +2257,7 @@ define([ var keyInfo = colorer.getMapHex(); // if the tree has been sheared then categories in obs maybe empty. - // getObsBy() does not filter out those categories so that the same + // getObsBy() does not filter out those categories so that the same // color can be assigned to each value in obs. for (var key in keyInfo) { if (obs[key].length === 0) { @@ -2736,9 +2736,9 @@ define([ /** * Redraw the barplot to match the current layout. If the current layout is * "Unrooted" then this will remove the barplots from canvas. - * + * */ - Empress.prototype.redrawBarPlotsToMatchLayout = function() { + Empress.prototype.redrawBarPlotsToMatchLayout = function () { // Undraw or redraw barplots as needed (assuming barplots are supported // in the first place, of course; if no feature or sample metadata at // all was passed then barplots are not available :() @@ -3725,7 +3725,7 @@ define([ * This will fill the autocomplete search bar with the names of the current * tree. */ - Empress.prototype.setAutoCompleteNames = function() { + Empress.prototype.setAutoCompleteNames = function () { var nodeNames = this._tree.getAllNames(); // Don't include nodes with the name null (i.e. nodes without a // specified name in the Newick file) in the auto-complete. diff --git a/empress/support_files/js/shearer.js b/empress/support_files/js/shearer.js index e15c4aa5d..6ee799842 100644 --- a/empress/support_files/js/shearer.js +++ b/empress/support_files/js/shearer.js @@ -1,11 +1,11 @@ - define(["underscore", "util", "TreeController"], function ( +define(["underscore", "util", "TreeController"], function ( _, util, TreeController ) { /** * @class ShearLayer - * + * * Create a new shear layer and adds it to the shear panel */ function ShearLayer( @@ -139,7 +139,7 @@ * The model for Shearer. This model is responsible for maintaining updating * empress whenever a user clicks on a shear option in one of the shear * layers. This model is also responsible for notifying its observers - * whenever the shear status of the tree has changed. + * whenever the shear status of the tree has changed. */ function ShearModel(empress, container) { this.empress = empress; @@ -158,24 +158,24 @@ ShearModel.prototype.addLayer = function (layer) { var fVals = this.empress.getUniqueFeatureMetadataInfo(layer, "tip") .sortedUniqueValues; - var layer = new ShearLayer( + var layerObj = new ShearLayer( layer, fVals, this.container, - (add, layer, val) => { - ShearModel.addRemoveShearItem(this, add, layer, val); + (add, lyr, val) => { + ShearModel.addRemoveShearItem(this, add, lyr, val); }, - (layer) => { - ShearModel.removeLayer(this, layer); + (lyr) => { + ShearModel.removeLayer(this, lyr); }, - (layer) => { - ShearModel.clearShearMapLayer(this, layer); + (lyr) => { + ShearModel.clearShearMapLayer(this, lyr); }, - (layer, values) => { - ShearModel.setShearMapLayer(this, layer, values); + (lyr, values) => { + ShearModel.setShearMapLayer(this, lyr, values); } ); - this.layers.set(layer, layer); + this.layers.set(layer, layerObj); }; /** @@ -257,7 +257,7 @@ /** * Adds or removes a shear value from a shear layer. * @param{ShearModel} model The ShearModel to use. - * @param{Boolean} remove Whether or not to remove val from the shear layer + * @param{Boolean} remove Whether or not to remove val from the shear layer * @param{String} layer The name of feature metadata column of shear layer * @param{String} val The feature metadata column value to add or remove * from layer. @@ -338,7 +338,8 @@ * * This is the exposed only exposed class of this closure and the one that * the rest of the empress code base will interact with. - */ + */ + function Shearer(empress, fCols) { this.fCols = fCols; this.shearSelect = document.getElementById("shear-feature-select"); From d909e83e14f9fa9389239ff14245db6c2ca9d305 Mon Sep 17 00:00:00 2001 From: kcantrel Date: Thu, 8 Apr 2021 18:41:41 -0700 Subject: [PATCH 34/63] added warning to shear --- empress/support_files/templates/side-panel.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/empress/support_files/templates/side-panel.html b/empress/support_files/templates/side-panel.html index e7c318aa9..e05d0dc7a 100644 --- a/empress/support_files/templates/side-panel.html +++ b/empress/support_files/templates/side-panel.html @@ -262,7 +262,10 @@ Shear Tree