Skip to content

Commit

Permalink
Tree statistics (#398)
Browse files Browse the repository at this point in the history
* ENH: Show node lengths in selection menu #179

Also computes min/max/avg node lengths; gotta show that somewhere...

* DOC: note abt root len

* STY: prettify

* ENH: show tree stats! Closes #179.

* STY: prettify

* DOC: clarify & upd8 bptree stat code

* TST: unbreak tests

* ENH: hide the root node's "length" in the menu

since it's not even validated

* DOC: buff disclaimer

* DOC: separate stats disclaimers

splits up ugly paragraph, and allows us to selectively hide part
of it if tree-plot done

* DOC: simplify tree stats header txt

* BUG: Ignore root len in unrooted layout: fix #374

* TST: hide stats table in test html

* TST/BUG/MNT: Make getLengthStats own func&fix/test

(Found a bug where the min was getting set to 0 even if real min
was > 0; due to setting min to 0 by default. Now it's +inf and max
starts as -inf, so we should be solid

* STY/TST: test error len stats case; prettify

* TST: test Empress.getNodeLength

* TST/MNT: mv code to spanel;refact/tst getTreeStats

* DOC: clarify ambiguous language in ignore len secn

* MNT: rename a few things from tree props->settings

* MNT: rm extraneous unrooted layout op

* ENH: round node length stats to 4 decimal pts

addresses @ElDeveloper comment

* PERF: only compute tree stats when requested

addresses @ElDeveloper comment

* PERF: only compute tree stats once

* STY: tidy some old html
  • Loading branch information
fedarko authored Sep 30, 2020
1 parent 6dd1085 commit 4ab2c8b
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 40 deletions.
46 changes: 41 additions & 5 deletions empress/support_files/js/bp-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,12 @@ define(["ByteArray", "underscore"], function (ByteArray, _) {
this._numTips = new Array(this.size + 1).fill(0);

/**
* @type{Float32Array}
* @type {Array}
* @private
* stores the length of the nodes in preorder. If lengths are not
* provided then lengths will be set to 0.
* Note: lengths are assumed to be smaller that 3.4 * 10^38
* Stores the length of the nodes in preorder. If lengths are not
* provided then lengths will be set to null.
*/
this.lengths_ = lengths ? new Float32Array(lengths) : null;
this.lengths_ = lengths ? lengths : null;

/**
* @type {Uint32Array}
Expand Down Expand Up @@ -193,6 +192,43 @@ define(["ByteArray", "underscore"], function (ByteArray, _) {
this._nameToNodes = {};
}

/**
* Returns an Object describing the minimum, maximum, and average of all
* non-root node lengths.
*
* @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.
* @throws {Error} If this tree does not have length information (i.e.
* this.lengths_ is null; this should only happen during
* testing).
*/
BPTree.prototype.getLengthStats = function () {
if (this.lengths_ !== null) {
var min = Number.POSITIVE_INFINITY,
max = Number.NEGATIVE_INFINITY,
sum = 0,
avg = 0;
// non-root lengths should be guaranteed to be nonnegative, and
// at least one non-root length should be positive.
//
// The x = 1, skips the first element (lengths_ is 1-indexed);
// the x < this.lengths_.length - 1 skips the last element
// (corresponding to the root length, which we ignore on purpose)
for (var x = 1; x < this.lengths_.length - 1; x++) {
min = Math.min(min, this.lengths_[x]);
max = Math.max(max, this.lengths_[x]);
sum += this.lengths_[x];
}
min = min;
max = max;
avg = sum / (this.size - 1);
return { min: min, max: max, avg: avg };
} else {
throw new Error("No length information defined for this tree.");
}
};

/**
*
* Returns the number of times bit t was observed leading upto bit t
Expand Down
54 changes: 53 additions & 1 deletion empress/support_files/js/empress.js
Original file line number Diff line number Diff line change
Expand Up @@ -3095,7 +3095,8 @@ define([
return samplePresence;
};

/** Show the node menu for a node name
/**
* Show the node menu for a node name
*
* @param {String} nodeName The name of the node to show.
*/
Expand All @@ -3111,5 +3112,56 @@ define([
this._events.placeNodeSelectionMenu(nodeName, this.focusOnSelectedNode);
};

/**
* Returns an Object describing various tree-level statistics.
*
* @return {Object} Contains six keys:
* -min: Minimum non-root node length
* -max: Maximum non-root node length
* -avg: Average non-root node length
* -tipCt: Number of tips in the tree
* -intCt: Number of internal nodes in the tree (incl.
* root)
* -allCt: Number of all nodes in the tree (incl. root)
* @throws {Error} If the tree does not have length information, this will
* be unable to call BPTree.getLengthStats() and will thus
* fail.
*/
Empress.prototype.getTreeStats = function () {
// Compute node counts
var allCt = this._tree.size;
var tipCt = this._tree.getNumTips(this._tree.size);
var intCt = allCt - tipCt;
// Get length statistics
var lenStats = this._tree.getLengthStats();
return {
min: lenStats.min,
max: lenStats.max,
avg: lenStats.avg,
tipCt: tipCt,
intCt: intCt,
allCt: allCt,
};
};

/**
* Returns the length corresponding to a node key, or null if the node key
* corresponds to the root of the tree.
*
* (The reason for the null thing is that the root node's length is not
* currently validated, so we don't want to show whatever the value
* there is stored as internally to the user.)
*
* @param {Number} nodeKey Postorder position of a node in the tree.
* @return {Number} The length of the node.
*/
Empress.prototype.getNodeLength = function (nodeKey) {
if (nodeKey === this._tree.size) {
return null;
} else {
return this._tree.length(this._tree.postorderselect(nodeKey));
}
};

return Empress;
});
30 changes: 12 additions & 18 deletions empress/support_files/js/layouts-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -502,27 +502,23 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
ignoreLengths,
normalize = true
) {
var angle = (2 * Math.PI) / tree.numleaves();
var da = (2 * Math.PI) / tree.numleaves();
var x1Arr = new Array(tree.size + 1);
var x2Arr = new Array(tree.size + 1).fill(0);
var y1Arr = new Array(tree.size + 1);
var y2Arr = new Array(tree.size + 1).fill(0);
var aArr = new Array(tree.size + 1);

var n = tree.postorderselect(tree.size);
var x1 = 0,
y1 = 0,
a = 0,
da = angle;
// NOTE: x2 will always be 0, since sin(0) = 0.
var rootLen = ignoreLengths ? 1 : tree.length(n);
var x2 = x1 + rootLen * Math.sin(a);
var y2 = y1 + rootLen * Math.cos(a);
x1Arr[tree.size] = x1;
x2Arr[tree.size] = x2;
y1Arr[tree.size] = y1;
y2Arr[tree.size] = y2;
aArr[tree.size] = a;
var x1, y1, a;
// Position the root at (0, 0) and ignore any length it might
// ostensibly have in the tree:
// https://github.com/biocore/empress/issues/374
x1Arr[tree.size] = 0;
x2Arr[tree.size] = 0;
y1Arr[tree.size] = 0;
y2Arr[tree.size] = 0;
aArr[tree.size] = 0;
var maxX = x2Arr[tree.size],
minX = x2Arr[tree.size];
var maxY = y2Arr[tree.size],
Expand Down Expand Up @@ -558,8 +554,6 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
maxY = Math.max(maxY, y2Arr[node]);
minY = Math.min(minY, y2Arr[node]);
}
var rX = x2Arr[tree.size];
var rY = y2Arr[tree.size];
var scale;
if (normalize) {
var widthScale = width / (maxX - minX);
Expand All @@ -570,11 +564,11 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
}
// skip the first element since the tree is zero-indexed
for (var i = 1; i <= tree.size - 1; i++) {
x2Arr[i] -= rX;
y2Arr[i] -= rY;
x2Arr[i] *= scale;
y2Arr[i] *= scale;
}
// Don't need to reposition coordinates relative to the root because
// the root is already at (0, 0)

return { xCoord: x2Arr, yCoord: y2Arr };
}
Expand Down
27 changes: 27 additions & 0 deletions empress/support_files/js/select-node-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ define(["underscore", "util"], function (_, util) {
this.nodeNameLabel = document.getElementById("menu-box-node-id");
this.notes = document.getElementById("menu-box-notes");
this.warning = document.getElementById("menu-box-warning");
this.nodeLengthContainer = document.getElementById(
"menu-box-node-length-container"
);
this.nodeLengthLabel = document.getElementById("menu-box-node-length");
this.fmTable = document.getElementById("menu-fm-table");
this.fmHeader = document.getElementById("menu-fm-header");
this.smHeader = document.getElementById("menu-sm-header");
Expand Down Expand Up @@ -247,6 +251,8 @@ define(["underscore", "util"], function (_, util) {
this.fmTable
);

this.setNodeLengthLabel(node);

if (this.empress.isCommunityPlot) {
// 2. Add sample presence information for this tip
// TODO: handle case where tip isn't in table, which happens if
Expand Down Expand Up @@ -348,6 +354,7 @@ define(["underscore", "util"], function (_, util) {
if (isUnambiguous) {
// this.nodeKeys has a length of 1
var nodeKey = this.nodeKeys[0];
this.setNodeLengthLabel(nodeKey);
if (this.empress.isCommunityPlot) {
var tips = this.empress._tree.findTips(nodeKey);

Expand All @@ -374,6 +381,7 @@ define(["underscore", "util"], function (_, util) {
} else {
this.smSection.classList.add("hidden");
this.smTable.classList.add("hidden");
this.nodeLengthContainer.classList.add("hidden");
}

// If isUnambiguous is false, no notes will be shown and the sample
Expand Down Expand Up @@ -403,6 +411,25 @@ define(["underscore", "util"], function (_, util) {
}
};

/**
* Updates and shows the node length UI elements for a given node.
*
* (If the node is the root of the tree, this will actually hide the UI
* elements. See Empress.getNodeLength() for details.)
*
* @param {Number} nodeKey Postorder position of a node in the tree.
*/
SelectedNodeMenu.prototype.setNodeLengthLabel = function (nodeKey) {
var nodeLength = this.empress.getNodeLength(nodeKey);
if (nodeLength !== null) {
this.nodeLengthLabel.textContent = nodeLength;
this.nodeLengthContainer.classList.remove("hidden");
} else {
// Don't show the length for the root node
this.nodeLengthContainer.classList.add("hidden");
}
};

/**
* Resets the state machine.
*/
Expand Down
31 changes: 26 additions & 5 deletions empress/support_files/js/side-panel-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) {
// used to event triggers
this.empress = empress;

// tree properties components
// settings components
this.treeNodesChk = document.getElementById("display-nodes-chk");
this.recenterBtn = document.getElementById("center-tree-btn");
this.focusOnNodeChk = document.getElementById("focus-on-node-chk");
Expand Down Expand Up @@ -117,7 +117,7 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) {
document.getElementById(scope.SHOW_ID).classList.remove("hidden");
};

// // shows the side menu
// shows the side menu
var show = document.getElementById(this.SHOW_ID);
show.onclick = function () {
document.getElementById(scope.SHOW_ID).classList.add("hidden");
Expand Down Expand Up @@ -467,13 +467,13 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) {
};

/**
* Add the callback events for the global tree properties tab. The callback
* events include things like centering the tree and showing tree nodes.
* Add the callback events for the settings tab. The callback events
* include things like centering the tree and showing tree nodes.
*
* Other things such as changing the defualt color of the tree will be
* added.
*/
SidePanel.prototype.addTreePropertiesTab = function () {
SidePanel.prototype.addSettingsTab = function () {
var scope = this;
this.treeNodesChk.onchange = function () {
scope.empress.setTreeNodeVisibility(scope.treeNodesChk.checked);
Expand Down Expand Up @@ -560,5 +560,26 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) {
};
};

/**
* Fills in tree statistics in various HTML elements on the side panel.
*/
SidePanel.prototype.populateTreeStats = function () {
var populate = function (htmlID, val) {
document.getElementById(htmlID).textContent = val;
};
var populateWithFixedPrecision = function (htmlID, val) {
populate(htmlID, val.toFixed(4));
};
var stats = this.empress.getTreeStats();
// only call toFixed on the length stats; the node counts are all
// integers
populate("stats-tip-count", stats.tipCt);
populate("stats-int-count", stats.intCt);
populate("stats-total-count", stats.allCt);
populateWithFixedPrecision("stats-min-length", stats.min);
populateWithFixedPrecision("stats-max-length", stats.max);
populateWithFixedPrecision("stats-avg-length", stats.avg);
};

return SidePanel;
});
26 changes: 21 additions & 5 deletions empress/support_files/templates/empress-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
<div class="hidden" id="menu-box">
<h1 id="menu-box-node-id"></h1>
<p id="menu-box-warning"></p>
<div id="menu-box-node-length-container" class="hidden">
<strong>Node length:</strong>
<span id="menu-box-node-length"></span>
</div>
<h3 class="hidden" id="menu-fm-header">Feature Metadata</h3>
<table class="menu-table hidden" id="menu-fm-table"></table>
<div id="menu-sm-section">
Expand Down Expand Up @@ -157,7 +161,7 @@ <h3 class="hidden" id="menu-sm-header">Sample Presence Information</h3>
// The side menu
var sPanel = new SidePanel(document.getElementById('side-panel'),
empress);
sPanel.addTreePropertiesTab();
sPanel.addSettingsTab();
sPanel.addLayoutTab();
sPanel.addExportTab();

Expand Down Expand Up @@ -195,10 +199,22 @@ <h3 class="hidden" id="menu-sm-header">Sample Presence Information</h3>
// make all tabs collapsable
document.querySelectorAll(".collapsible").forEach(function(btn) {
btn.addEventListener("click", function() {
this.classList.toggle("active");
this.nextElementSibling.classList.toggle("hidden");
document.getElementById("side-panel").classList.toggle("panel-active",
document.querySelector(".side-content:not(.hidden)"));
// Only compute tree stats the first time the user opens up
// the tree statistics tab
if (
this.id === "stats-btn" &&
this.classList.contains("unpopulated")
) {
sPanel.populateTreeStats();
this.classList.remove("unpopulated");
}

this.classList.toggle("active");
this.nextElementSibling.classList.toggle("hidden");
document.getElementById("side-panel").classList.toggle(
"panel-active",
document.querySelector(".side-content:not(.hidden)")
);
});
});

Expand Down
Loading

0 comments on commit 4ab2c8b

Please sign in to comment.