Skip to content

Commit

Permalink
Fix projection bug #152 (#189)
Browse files Browse the repository at this point in the history
* fixed #152

* fixed js test

* added check for 'non-unique' column name in metadata files

* Removed 'non-unique'

* removed redundant line

* Apply suggestions from code review

removed 'addNonUnique' from _projectObservations' function description and fixed typos/format

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

* added two more tests

Co-authored-by: Marcus Fedarko <[email protected]>
  • Loading branch information
kwcantrell and fedarko authored Jun 22, 2020
1 parent 31883f2 commit c672144
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 61 deletions.
Binary file modified docs/moving-pictures/empress-tree.qzv
Binary file not shown.
2 changes: 1 addition & 1 deletion empress/support_files/js/animation-panel-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ define(["Colorer"], function (Colorer) {
gradient,
cm,
hide,
lWidth
lWidth - 1
);

// start animation
Expand Down
1 change: 0 additions & 1 deletion empress/support_files/js/animator.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ define(["Colorer"], function (Colorer) {
// Retrive list of unique categories to display during the animation.
this.trajectoryCol = trajectory;
var trajectories = this.empress.getUniqueSampleValues(trajectory);

// Assign a color to each unique category
var colorer = new Colorer(cm, trajectories);
this.cm = colorer.getMapRGB();
Expand Down
104 changes: 54 additions & 50 deletions empress/support_files/js/empress.js
Original file line number Diff line number Diff line change
Expand Up @@ -793,22 +793,18 @@ define([
obs[category] = this._namesToKeys(obs[category]);
}

// assign internal nodes to appropriate category based on its children
obs = this._projectObservations(obs);

// assign colors to categories
var colorer = new Colorer(color, categories);
// colors for drawing the tree
var cm = colorer.getMapRGB();
// colors for the legend
var keyInfo = colorer.getMapHex();

// assign internal nodes to appropriate category based on its children
obs = this._projectObservations(obs);

// color tree
this._colorTree(obs, cm);

// get percent of branches belonging to unique category (i.e. just gut)
this.percentColoredBySample(obs, keyInfo);

return keyInfo;
};

Expand Down Expand Up @@ -897,34 +893,73 @@ define([
return keyInfo;
};

/**
* Finds the branches that are unique to each category in obs
/*
* Projects the groups in obs up the tree.
*
* This function performs two distinct operations:
* 1) Removes the non-unique observations from each group in obs
* (i.e. performs an 'exclusive or' between each group).
*
* @param {Object} observations grouped by categories
* 2) Assigns each internal node to a group if all of its children belong
* to the same group.
*
* @return {Object} the branches of the tree that are unique to category in
obs
*/
* Note: All tips that are not passed into obs are considered to belong to
* a "not-represented" group, which will be omitted from the
* returned version of obs.
*
* @param {Object} obs Maps categories to a set of observations (i.e. tips)
* @return {Object} returns A Map with the same group names that maps groups
to a set of keys (i.e. tree nodes) that are unique to
each group.
*/
Empress.prototype._projectObservations = function (obs) {
var tree = this._tree;
var categories = Object.keys(obs);
var tree = this._tree,
categories = Object.keys(obs),
notRepresented = new Set(),
i,
j;

// find "non-represented" tips
// Note: the following uses postorder traversal
for (i = 1; i < tree.size; i++) {
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
for (var i = 1; i < tree.size; i++) {
// 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.
for (i = 1; i < tree.size; i++) {
var node = i;
var parent = tree.postorder(tree.parent(tree.postorderselect(i)));

for (var j = 0; j < categories.length; j++) {
for (j = 0; j < categories.length; j++) {
category = categories[j];

// add internal nodes to groups
if (obs[category].has(node)) {
this._treeData[node].inSample = true;
obs[category].add(parent);
}
if (notRepresented.has(node)) {
notRepresented.add(parent);
}
}
}
obs = util.keepUniqueKeys(obs);
return obs;
var result = util.keepUniqueKeys(obs, notRepresented);

return result;
};

/**
Expand All @@ -948,37 +983,6 @@ define([
}
};

/**
* Cacluates the total and relative pertange of the tree that was colored by
* each category in sampleObs
*
* @param {Object} sampleObs The object containing which tree branches are
* colored by which sample category
* @param {Object} keyInfo The object containing the information to be
* displayed in the sample legend
*/
Empress.prototype.percentColoredBySample = function (sampleObs, keyInfo) {
// calculate relative tree size i.e. the subtree spanned by the samples
// iterate over tree using postorder

var i,
relativeTreeSize = 0;
for (i = 1; i <= this._tree.size; i++) {
if (this._treeData[i].inSample) {
relativeTreeSize++;
}
}

// calculate total and relative percentages in each group
var sampleCategies = Object.keys(sampleObs);
for (i = 0; i < sampleCategies.length; i++) {
var category = sampleCategies[i];
var branchesInCategory = sampleObs[category].length;
keyInfo[category].tPercent = branchesInCategory / this._tree.size;
keyInfo[category].rPercent = branchesInCategory / relativeTreeSize;
}
};

/**
* Sets the color of the tree back to default
*/
Expand Down
20 changes: 16 additions & 4 deletions empress/support_files/js/util.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
define(["underscore"], function (_) {
/**
* Remove all non unique keys
* Note: keys are referring to empress._treeData (i.e. postorder position of
* tree nodes starting at 1)
*
* @param {Object} keys An object containing multiple lists of keys
* @param {Object} keys Maps groups to sets of keys.
* @param {Set} removeAll Set of keys to remove regardless of whether or not
* they are unique.
*
* @return {Object} A new object with the non unique keys removed
*/
function keepUniqueKeys(keys) {
function keepUniqueKeys(keys, removeAll) {
// get unique keys
var items = Object.keys(keys);
var i;
Expand All @@ -30,7 +34,7 @@ define(["underscore"], function (_) {
.flatten()
.value();
var uniqueKeys = new Set(uniqueKeysArray);
var hasKey = function (key) {
var isUnique = function (key) {
return uniqueKeys.has(key);
};

Expand All @@ -39,7 +43,15 @@ define(["underscore"], function (_) {
items = Object.keys(keys);
for (i = 0; i < items.length; i++) {
var itemKeys = [...keys[items[i]]];
result[items[i]] = _.filter(itemKeys, hasKey);
var keep = new Set();
for (var j = 0; j < itemKeys.length; j++) {
if (removeAll.has(itemKeys[j])) continue;

if (isUnique(itemKeys[j])) {
keep.add(itemKeys[j]);
}
}
result[items[i]] = keep;
}

return result;
Expand Down
4 changes: 2 additions & 2 deletions empress/support_files/templates/side-panel.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<!-- The Header bar -->
<div id="autocomplete-container">
<p class="side-header">
<input type="text" id="quick-search" placeholder="Search by node ID..."
style="flex-grow: 1;">
<input type="text" id="quick-search" placeholder="Search by node ID..."
style="flex-grow: 1;">
<button id="side-header-search-btn" style="flex-grow: 1;">Search</button>
<button id="hide-ctrl" title="Hide control panel"
style="font-size: 10pt; padding: 0 0 3px 3px;">&#9701;</button>
Expand Down
7 changes: 4 additions & 3 deletions tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
'testSummaryHelper': './../tests/test-summary-helper',
'testUtil' : './../tests/test-util',
'testCircularLayoutComputation' : './../tests/test-circular-layout-computation',
'testVectorOps' : './../tests/test-vector-ops'
'testVectorOps' : './../tests/test-vector-ops',
'testEmpress' : './../tests/test-empress'
}
});

Expand All @@ -66,13 +67,13 @@
'Camera', 'Colorer', 'BiomTable', 'SummaryHelper', 'util', 'Empress',
'testBPTree', 'testByteTree', 'testBIOMTable', 'testCamera',
'testColorer', 'testSummaryHelper', 'testUtil',
'testCircularLayoutComputation', 'testVectorOps'],
'testCircularLayoutComputation', 'testVectorOps', 'testEmpress'],

// start tests
function ($, gl, chroma, underscore, ByteArray, BPTree, Camera,
testBIOMTable, Colorer, BiomTable, SummaryHelper, util, Empress,
testBPTree, testByteTree, testCamera, testColorer, testSummaryHelper,
testUtil, testCircularLayoutComputation, testVectorOps) {
testUtil, testCircularLayoutComputation, testVectorOps, testEmpress) {
$(document).ready(function() {
QUnit.start();
});
Expand Down
148 changes: 148 additions & 0 deletions tests/test-empress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
require(["jquery", "BPTree", "Empress", "util"], function($, BPTree, Empress, util) {
$(document).ready(function() {
// Setup test variables
// Note: This is ran for each test() so tests can modify bpArray without
// effecting other test
module('Empress' , {
setup: 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]));
var treeData = {
7: {
"color":[1.0, 1.0, 1.0],
"inSample": false
},
6: {
"color":[1.0, 1.0, 1.0],
"inSample": false
},
5: {
"color":[1.0, 1.0, 1.0],
"inSample": false
},
4: {
"color":[1.0, 1.0, 1.0],
"inSample": false
},
2: {
"color":[1.0, 1.0, 1.0],
"inSample": false
},
3: {
"color":[1.0, 1.0, 1.0],
"inSample": false
},
1: {
"color":[1.0, 1.0, 1.0],
"inSample": false
}

}
this.empress = new Empress(tree, treeData, null,
null, null, null, null, null, null, null);
},

teardown: function() {
this.empress = null;
}
});

test("Test _projectObservations, all tips in obs", function() {
var obs = {
"g1" : new Set([2, 3]),
"g2" : new Set([1]),
"g3" : new Set([6])
};
var expectedResult = {
"g1" : new Set([2,3,4]),
"g2" : new Set([1]),
"g3" : new Set([6]),
};
var result = this.empress._projectObservations(obs);

var groups = ["g1", "g2", "g3"];
for (var i = 0; i < groups.length; i++) {
var group = groups[i];
var expectedArray = Array.from(expectedResult[group]);
var resultArray = util.naturalSort(Array.from(result[group]));
deepEqual(resultArray, expectedArray);
}

var columns = Object.keys(result);
deepEqual(columns, groups);
});

test("Test _projectObservations, missing tips in obs", function() {
var obs = {
"g1" : new Set([2, 3]),
"g2" : new Set([]),
"g3" : new Set([6])
};
var expectedResult = {
"g1" : new Set([2, 3, 4]),
"g2" : new Set([]),
"g3" : new Set([6])
};
var result = this.empress._projectObservations(obs);

var groups = ["g1", "g2", "g3"];
for (var i = 0; i < groups.length; i++) {
var group = groups[i];
var expectedArray = Array.from(expectedResult[group]);
var resultArray = util.naturalSort(Array.from(result[group]));
deepEqual(resultArray, expectedArray);
}

var columns = Object.keys(result);
deepEqual(columns, groups);
});

test("Test _projectObservations, all tips are unique to group", function() {
var obs = {
"g1": new Set([1, 2, 3, 6]),
"g2": new Set([])
};
var expectedResult = {
"g1": new Set([1, 2, 3, 4, 5, 6, 7]),
"g2": new Set([])
};
var result = this.empress._projectObservations(obs);

var groups = ["g1", "g2"];
for (var i = 0; i < groups.length; i++) {
var group = groups[i];
var expectedArray = Array.from(expectedResult[group]);
var resultArray = util.naturalSort(Array.from(result[group]));
deepEqual(resultArray, expectedArray);
}

var columns = Object.keys(result);
deepEqual(columns, groups);
});

test("Test _projectObservations, no tips are present in any group", function() {
var obs = {
"g1": new Set([]),
"g2": new Set([])
};
var expectedResult = {
"g1": new Set([]),
"g2": new Set([])
};
var result = this.empress._projectObservations(obs);

var groups = ["g1", "g2"];
for (var i = 0; i < groups.length; i++) {
var group = groups[i];
var expectedArray = Array.from(expectedResult[group]);
var resultArray = util.naturalSort(Array.from(result[group]));
deepEqual(resultArray, expectedArray);
}

var columns = Object.keys(result);
deepEqual(columns, groups);
});
});
});
Loading

0 comments on commit c672144

Please sign in to comment.