Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "leaf sorting"; reorganize side panel a bit #394

Merged
merged 32 commits into from
Sep 28, 2020
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
50373a8
ENH: Move layout options to layout tab: close #379
fedarko Sep 24, 2020
95d59f9
ENH: initial draft of leaf sorting in js
fedarko Sep 24, 2020
c9416b8
BUG: fix child sorting
fedarko Sep 24, 2020
b7a8814
ENH: Enable JS leaf sorting (close #170)
fedarko Sep 24, 2020
339eda0
ENH: update leaf sorting desc dynamically
fedarko Sep 24, 2020
3a000a8
MNT: temporarily? remove layout avg point cache
fedarko Sep 24, 2020
34dcf1a
ENH: disable leaf sorting sel for unrooted layout
fedarko Sep 24, 2020
a839f18
ENH: Just hide leaf sorting rather than disabling
fedarko Sep 24, 2020
8b7af2d
DOC/PERF/MNT: cache l.s. results; imprv docs/code
fedarko Sep 24, 2020
c424913
STY: prettify
fedarko Sep 24, 2020
9b3ded2
MNT: rm old todo
fedarko Sep 24, 2020
5a35226
DOC/PERF: Document new bptree funcs; speed up desc
fedarko Sep 24, 2020
0aebb18
DOC: clean up BPTree.getSortedChildren()
fedarko Sep 25, 2020
01329e1
DOC: clarify postorederLeafSortedNodes docs
fedarko Sep 25, 2020
db0fbaa
TST: Test postorder leafsorted (basic tests)
fedarko Sep 25, 2020
2494ccd
BUG: ignore root during circ layout
fedarko Sep 25, 2020
6b96b05
TST: unbreak (most of) the js tests
fedarko Sep 25, 2020
d4e4da5
STY/TST/DOC: tidy up centerLayoutAvgPoint &fix tst
fedarko Sep 25, 2020
0263f1a
STY: prettify layout tests
fedarko Sep 25, 2020
95e45fd
DOC/TST: doc & tst LayoutsUtil.getPostOrderNodes()
fedarko Sep 25, 2020
a775241
TST: expand leaf sorting test
fedarko Sep 25, 2020
5d4066a
TST/DOC: Add tests+beef up docs for get*Children
fedarko Sep 25, 2020
7444001
STY: prettify
fedarko Sep 25, 2020
ec19b7c
TST: Add rect layout leaf sorting test
fedarko Sep 25, 2020
f55dc53
TST: Test desc leaf sorting via layout also kinda
fedarko Sep 25, 2020
c060786
STY: prettify
fedarko Sep 25, 2020
b6403b0
TST: kinda test leaf sorting for circ layouts
fedarko Sep 25, 2020
fcfc7e8
DOC/BUG: doc some new sp funcs; fix desc text
fedarko Sep 25, 2020
cc5ad5e
DOC: update README barplot sec now that #170 done
fedarko Sep 25, 2020
a669f04
TST: make centerLayoutAvgPt tests use util func
fedarko Sep 25, 2020
cc8a94c
Merge branch 'master' of https://github.com/biocore/empress into js-l…
fedarko Sep 26, 2020
dd073f4
BUG: add back accidentally-rm'd node circles txt
fedarko Sep 26, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,15 @@ Similarly to other tree visualization tools like [iTOL](https://itol.embl.de/),
#### First: a small warning about barplots

Although barplots are very useful for identifying patterns, be wary of
reading too much into them! The ordering of tips / clades on the same level of
the tree is [currently arbitrary](https://github.com/biocore/empress/issues/170),
and this can impact the way barplots look in ways that might not be immediately
reading too much into them! The way the rectangular and circular layouts work
means that a tip that looks "next" to another tip may actually be somewhat far
away from that tip (e.g. in the rectangular layout if one tip is at the top of
its clade, and another tip just "above" it is at the bottom of its clade). An
example of this is shown below with the mustard and lavender clades:

![Example of this phenomenon on the moving pictures dataset](docs/moving-pictures/img/empress_funky_barplot_example.png)

This can impact the way barplots look in ways that might not be immediately
obvious. To quote "Inferring Phylogenies" (Felsenstein 2004), pages 573–574:

> It is worth noting that by reordering tips, you can change the viewer's impression of the closeness of relationships. [...] A little judicious flipping may create a Great Chain of Being marching nicely along the sequence of names, even though the tree supports no such thing.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
199 changes: 192 additions & 7 deletions empress/support_files/js/bp-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,24 @@ define(["ByteArray", "underscore"], function (ByteArray, _) {
* Stores the order of nodes in an in-order traversal. Elements in this
* array are node ids
*
* Note: In-order is stored because bp-tree doesn't not have
* Note: In-order is stored because bp-tree doesn't have
* an efficient way of convert a nodes in-order position to tree
* index and vice versa like it does with post order through
* the use of postorderselect() and postorder(). So it is more
* efficient to cache an in-order tree traversal.
*/
this._inorder = null;

/**
* @type{Array}
* @private
*
* Stores the order of nodes in ascending and descending-leaf-sorted
* postorder traversals. Elements in this array are node IDs.
*/
this._ascendingLeafSorted = null;
this._descendingLeafSorted = null;

/**
* @type {Object}
* @private
Expand Down Expand Up @@ -588,7 +598,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) {
};

/**
* Returns an array of nodes sorted by their inoder position.
* 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
Expand All @@ -610,11 +620,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) {
this._inorder.push(this.postorder(curNode));

// append children to stack
var child = this.fchild(curNode);
while (child !== 0) {
nodeStack.push(child);
child = this.nsibling(child);
}
nodeStack = nodeStack.concat(this.getChildren(curNode));
}
return this._inorder;
};
Expand Down Expand Up @@ -708,6 +714,185 @@ define(["ByteArray", "underscore"], function (ByteArray, _) {
return numTips;
};

/**
* Returns a list of nodes' postorder positions in the tree, using "leaf
* sorting" -- clades at the same level are sorted by the number of leaves
* they contain.
*
* This (usually) makes the tree look pretty.
*
* This code behaves similarly to this.inOrderNodes(), in that it caches
* the result once computed. Therefore, this function should be faster the
* second time it's called (assuming the sortingMethod is the same).
*
* @param {String} sortingMethod Either "ascending" or "descending". Any
* other values will trigger an error.
* -"ascending": Order clades starting with
* the clade with the fewest tips and ending
* with the clade with the most tips (ties
* broken arbitrarily).
* -"descending": Opposite of "ascending"
* (start with most-tip clades then go down;
* ties broken arbitrarily).
* @return {Array} Array of nodes' postorder positions in the tree, sorted
* as specified.
* @throws {Error} if sortingMethod is not "ascending" or "descending".
*
* REFERENCES
* ----------
* This was translated from this Python code to do leaf-sorted postorder
* traversal over a scikit-bio TreeNode:
* https://github.com/biocore/empress/commit/2b3d90f52fc3118b3641ddc6378d3659abdb0d05
*
* That code was in turn a slightly modified version of scikit-bio's
* postorder tree traversal code:
* https://github.com/biocore/scikit-bio/blob/6ccba4076f1b96843fa2428804cc5a91bf4b76b8/skbio/tree/_tree.py#L1085-L1154
*
* And this functionality was inspired by iTOL's use of it, of course:
* https://itol.embl.de/help.cgi (see the "Leaf sorting:" text)
*/
BPTree.prototype.postorderLeafSortedNodes = function (sortingMethod) {
if (sortingMethod === "ascending") {
if (this._ascendingLeafSorted !== null) {
return this._ascendingLeafSorted;
}
} else if (sortingMethod === "descending") {
if (this._descendingLeafSorted !== null) {
return this._descendingLeafSorted;
}
} else {
throw new Error(
"Unrecognized leaf sorting method " + sortingMethod
);
}
var outputNodes = [];
var childIdxStack = [0];
var rootNode = this.preorderselect(1);
var currNode = rootNode;
var currChildren = this.getSortedChildren(currNode, sortingMethod);
var currChildrenLen = currChildren.length;
var currIdx, currChild;
while (true) {
currIdx = childIdxStack[childIdxStack.length - 1];
// If children left, process them.
if (currIdx < currChildrenLen) {
currChild = currChildren[currIdx];
// If currChild has children, go there.
if (!this.isleaf(currChild)) {
childIdxStack.push(0);
currNode = currChild;
currChildren = this.getSortedChildren(
currNode,
sortingMethod
);
currChildrenLen = currChildren.length;
currIdx = 0;
}
// Otherwise, add this child.
else {
outputNodes.push(this.postorder(currChild));
childIdxStack[childIdxStack.length - 1]++;
}
}
// If no children left, add self and move on to self's parent
else {
outputNodes.push(this.postorder(currNode));
if (currNode === rootNode) {
break;
}
currNode = this.parent(currNode);
currChildren = this.getSortedChildren(currNode, sortingMethod);
currChildrenLen = currChildren.length;
childIdxStack.pop();
childIdxStack[childIdxStack.length - 1]++;
}
}
// Cache the results so we don't have to do all that again
if (sortingMethod === "ascending") {
this._ascendingLeafSorted = outputNodes;
} else {
this._descendingLeafSorted = outputNodes;
}
return outputNodes;
};

/**
* Returns an array containing the children of a node.
*
* The order of the array is based on fchild and nsibling, which I think
* should match what done in the input Newick file.
*
* If the input node has no children (i.e. it's a leaf / tip), this will
* return an empty Array.
*
* @param {Number} node Index of the node. "Index" here refers to the
* 0-indexed position in the balanced parentheses of
* the opening paren for the node in question, e.g.
* 0 1 2 3 4 5
* ( () () ( () () ) )
* @return {Array} children Array of child indices, specified analogously
* to the node index above. As an example, the
* children of node 3 in the tree above would be
* 4 and 5.
*/
BPTree.prototype.getChildren = function (node) {
var children = [];
var child = this.fchild(node);
while (child !== 0) {
children.push(child);
child = this.nsibling(child);
}
return children;
};

/**
* Returns an array containing the children of a node, sorted by the number
* of tips their subtrees contain.
*
* If the input node has no children (i.e. it's a leaf / tip), this will
* return an empty Array.
*
* Ties (e.g. when an internal node's children are all tips, and thus
* "contain" 1 tip) should respect the ordering in the initial Newick file,
* since _.sortBy() is a stable sort: https://underscorejs.org/#sortBy
*
* (That said, we don't make any claims in the UI about this [at least not
* for the ascending/descending leaf sorting options], so it's not a huge
* deal.)
*
* @param {Number} node
* @param {String} sortingMethod Should be one of "ascending" or
* "descending". We don't bother validating
* it at this point -- if another string is
* passed in then the behavior of this
* function is undefined (realistically it'll
* probably just sort things in ascending
* order, which is what it currently does in
* that case, but we can't guarantee that
* won't change).
* @return {Array} children
*/
BPTree.prototype.getSortedChildren = function (node, sortingMethod) {
var scope = this;

var children = this.getChildren(node);
// Define a function mapping a node index to the number of tips its
// subtree contains. This function will be used to sort the array
// of children.
var child2numTips = function (childIdx) {
var numTips = scope.getNumTips(scope.postorder(childIdx));
if (sortingMethod === "descending") {
// Flip things around; sort in descending order. This should be
// quicker than sorting in ascending order and then reversing
// it afterwards.
return -numTips;
} else {
return numTips;
}
};
return _.sortBy(children, child2numTips);
};

/**
* True if name is in the names array for the tree
*
Expand Down
99 changes: 58 additions & 41 deletions empress/support_files/js/empress.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,6 @@ define([
*/
this._barplotsDrawn = false;

/**
* type {Object}
* Maps tree layouts to the average point of each layout
*/
this.layoutAvgPoint = {};

/**
* @type{Number}
* The (not-yet-scaled) line width used for drawing "thick" lines.
Expand Down Expand Up @@ -270,6 +264,12 @@ define([
*/
this.ignoreLengths = false;

/**
* @type{String}
* Leaf sorting method: one of "none", "ascending", or "descending"
*/
this.leafSorting = "descending";

/**
* @type{CanvasEvents}
* Handles user events
Expand Down Expand Up @@ -344,7 +344,8 @@ define([
this._tree,
4020,
4020,
this.ignoreLengths
this.ignoreLengths,
this.leafSorting
);
this._yrscf = data.yScalingFactor;
for (i = 1; i <= this._tree.size; i++) {
Expand All @@ -364,7 +365,8 @@ define([
this._tree,
4020,
4020,
this.ignoreLengths
this.ignoreLengths,
this.leafSorting
);
for (i = 1; i <= this._tree.size; i++) {
// remove old layout information
Expand Down Expand Up @@ -2285,52 +2287,67 @@ define([

/**
* Centers the viewing window at the average of the current layout.
*
* The layout's average point is defined as [x, y, zoomAmount], where:
*
* -x is the average of all x coordinates
* -y is the average of all y coordinates
* -zoomAmount takes the largest x or y coordinate and normalizes it by
* dim / 2 (where dim is the dimension of the canvas).
*
* zoomAmount is defined be a simple heuristic that should allow the
* majority of the tree to be visible in the viewing window.
*
* NOTE: Previously, layoutAvgPoint was cached for each layout. This
* behavior has been removed, because (with the advent of leaf sorting and
* "ignore lengths") a given "layout" (e.g. Rectangular) can now have
* pretty drastically different locations across all the options available.
*
* @return {Array} Contains three elements, in the following order:
* 1. Average x-coordinate
* 2. Average y-coordinate
* 3. zoomAmount
* As of writing, nothing in Empress that I'm aware of
* consumes the output of this function. The main reason we
* return this is to make testing this easier.
*/
Empress.prototype.centerLayoutAvgPoint = function () {
if (!(this._currentLayout in this.layoutAvgPoint)) {
// Add up x and y coordinates of all nodes in the tree (using
// current layout).
var x = 0,
y = 0,
zoomAmount = 0;
for (var node = 1; node <= this._tree.size; node++) {
// node = this._treeData[node];
x += this.getX(node);
y += this.getY(node);
zoomAmount = Math.max(
zoomAmount,
Math.abs(this.getX(node)),
Math.abs(this.getY(node))
);
}

// each layout's avegerage point is define as followed:
// [x, y, zoomAmount] where x is the average of all x coordinates,
// y is the average of all y coordinates, and zoomAmount takes the
// largest x or y coordinate and normaizes it by dim / 2 (where
// dim is the dimension of the canvas).
// Note: zoomAmount is defined be a simple heuristic that should
// allow the majority of the tree to be visible in the viewing
// window.
this.layoutAvgPoint[this._currentLayout] = [
x / this._tree.size,
y / this._tree.size,
(2 * zoomAmount) / this._drawer.dim,
];
var layoutAvgPoint = [];
// Add up x and y coordinates of all nodes in the tree (using
// current layout).
var x = 0,
y = 0,
zoomAmount = 0;
for (var node = 1; node <= this._tree.size; node++) {
// node = this._treeData[node];
x += this.getX(node);
y += this.getY(node);
zoomAmount = Math.max(
zoomAmount,
Math.abs(this.getX(node)),
Math.abs(this.getY(node))
);
}

layoutAvgPoint = [
x / this._tree.size,
y / this._tree.size,
(2 * zoomAmount) / this._drawer.dim,
];

// center the viewing window on the average point of the current layout
// and zoom out so the majority of the tree is visible.
var cX = this.layoutAvgPoint[this._currentLayout][0],
cY = this.layoutAvgPoint[this._currentLayout][1];
var cX = layoutAvgPoint[0],
cY = layoutAvgPoint[1];
this._drawer.centerCameraOn(cX, cY);
this._drawer.zoom(
this._drawer.treeSpaceCenterX,
this._drawer.treeSpaceCenterY,
false,
this.layoutAvgPoint[this._currentLayout][2]
layoutAvgPoint[2]
);
this.drawTree();
return layoutAvgPoint;
};

/**
Expand Down
Loading