Skip to content

Commit

Permalink
Merge pull request #240 from fedarko/fancy-schmancy-menu
Browse files Browse the repository at this point in the history
Position the selection menu normally, even for nodes with duplicate names (when clicked on)
  • Loading branch information
kwcantrell authored Jul 8, 2020
2 parents ec613fb + ab2e8bc commit 852e399
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 79 deletions.
2 changes: 1 addition & 1 deletion empress/support_files/js/biom-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ define(["underscore", "util"], function (_, util) {
* @param {Number} num Number to look for the presence of in arr; in
* practice, this will be a feature index to search for
*
* @return {boolean} true if num is present in arr, false otherwise
* @return {Boolean} true if num is present in arr, false otherwise
*/
BIOMTable.prototype._sortedArrayHasNumber = function (arr, num) {
return _.indexOf(arr, num, true) >= 0;
Expand Down
2 changes: 1 addition & 1 deletion empress/support_files/js/bp-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ define(["ByteArray"], function (ByteArray) {
/**
* Return true if i represents a leaf node
*
* @return {boolean}
* @return {Boolean}
*/
BPTree.prototype.isleaf = function (i) {
return this.b_[i] && !this.b_[i + 1];
Expand Down
110 changes: 79 additions & 31 deletions empress/support_files/js/canvas-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ define(["glMatrix", "SelectedNodeMenu"], function (gl, SelectedNodeMenu) {
var closestDist = Infinity;
var closestNode = null;
var xDist, yDist;
var closeNodeKey;
// Go through all the nodes in the tree and find the node
// closest to the (x, y) point that was clicked
for (var i = 1; i <= empress._tree.size; i++) {
var node = empress._treeData[i];
var nodeX = empress.getX(node);
Expand All @@ -158,10 +161,13 @@ define(["glMatrix", "SelectedNodeMenu"], function (gl, SelectedNodeMenu) {
if (squareDist < closestDist) {
closestDist = squareDist;
closeNode = node;
closeNodeKey = i;
}
}

// check if node is within epsilon pixels away from mouse click
// check if the closest-to-the-click node is within epsilon
// pixels away from the mouse click; if so, this node was
// "selected," so we can open the menu
var nX = empress.getX(closeNode);
var nY = empress.getY(closeNode);
var screenSpace = drawer.toScreenSpace(nX, nY);
Expand All @@ -171,7 +177,18 @@ define(["glMatrix", "SelectedNodeMenu"], function (gl, SelectedNodeMenu) {
yDist = e.clientY - nY;
var screenDist = Math.sqrt(xDist * xDist + yDist * yDist);
if (screenDist < epsilon) {
scope.placeNodeSelectionMenu(closeNode.name, false);
// We pass closeNodeKey so that placeNodeSelectionMenu()
// knows which node was selected, even if the selected node
// has a duplicate name.
// (Not all places that call this function will know this
// information, though -- for example, the searching code
// only knows the name of the node the user specified. This
// is why we still have to provide the name here.)
scope.placeNodeSelectionMenu(
closeNode.name,
false,
closeNodeKey
);
}
}
};
Expand Down Expand Up @@ -205,7 +222,7 @@ define(["glMatrix", "SelectedNodeMenu"], function (gl, SelectedNodeMenu) {
quickSearchBar.innerHTML = nodeName;

// show the selected node menu
scope.placeNodeSelectionMenu(nodeName);
scope.placeNodeSelectionMenu(nodeName, true);

// clear possible words menu
removeSuggestionMenu();
Expand Down Expand Up @@ -282,7 +299,7 @@ define(["glMatrix", "SelectedNodeMenu"], function (gl, SelectedNodeMenu) {
// <ENTER> key is pressed
if (key.keyCode === 13) {
removeSuggestionMenu();
scope.placeNodeSelectionMenu(this.value);
scope.placeNodeSelectionMenu(this.value, true);
}
};

Expand All @@ -309,52 +326,83 @@ define(["glMatrix", "SelectedNodeMenu"], function (gl, SelectedNodeMenu) {
*/
var search = function () {
var nodeName = quickSearchBar.value;
scope.placeNodeSelectionMenu(nodeName);
scope.placeNodeSelectionMenu(nodeName, true);
};
searchBtn.onclick = search;
};

/**
* Creates a node selection menu box for nodeName. If nodeName does
* not exist, then this this method will make the background color of the
* not exist, then this method will make the background color of the
* quick search bar red.
* This method is call from the both the quick search btn and when the user
* clicks on the canvas.
* This method is called both from the quick search button and when the
* user clicks on the canvas.
*
* @param{String} nodeName The node name to make a node selection menu box
* for.
* @param{Boolean} moveTree If true, then this method will move the viewing
* window of the tree to the node.
* @param {String} nodeName The name of the node to make a menu box for.
* Since internal nodes can have duplicate names,
* it's expected that this name can be non-unique.
* @param {Boolean} moveTree If true, then this method will center the
* camera on the node in question (for example,
* if the user searched for a node using the
* side panel). If false, this won't be done (for
* example, if the user clicked on a node, there
* shouldn't be a need for the camera to move).
* @param {Number} nodeKey Optional parameter. If specified, this should
* be an index in this.empress._treeData of the
* node that this menu is being shown for. This is
* useful for cases where the selected node was
* clicked on (so we know exactly which node it
* is), but where the node in question has a
* duplicate name (so we can't get its position
* from just its name alone).
*/
CanvasEvents.prototype.placeNodeSelectionMenu = function (
nodeName,
moveTree = true
moveTree,
nodeKey
) {
// multiple nodes can have the same name
var idList = this.empress._nameToKeys[nodeName];

if (idList !== undefined) {
// get first node
var node = this.empress._treeData[idList[0]];

if (idList.length > 1) {
node = this.empress._treeData[this.empress._tree.size - 1];
}

var scope = this;
var node;
/**
* This is a utility function that, given an array of node keys,
* centers the camera on the first node (if moveTree is truthy) and
* then passes the array to this.empress.selectedNodeMenu and opens the
* menu / redraws the tree.
*/
var openMenu = function (nodeKeys) {
// We'll position the camera at whatever the "first" node in
// nodeKeys is. This is an arbitrary decision, but better than
// nothing.
if (moveTree) {
this.drawer.centerCameraOn(
this.empress.getX(node),
this.empress.getY(node)
var nodeToCenterOn = scope.empress._treeData[nodeKeys[0]];
scope.drawer.centerCameraOn(
scope.empress.getX(nodeToCenterOn),
scope.empress.getY(nodeToCenterOn)
);
}

// show menu
this.selectedNodeMenu.setSelectedNodes(idList);
this.selectedNodeMenu.showNodeMenu();
scope.selectedNodeMenu.setSelectedNodes(nodeKeys);
scope.selectedNodeMenu.showNodeMenu();

this.empress.drawTree();
scope.empress.drawTree();
};
if (nodeKey !== undefined) {
// If this parameter was specified, our job is easy -- we know the
// exact node to place the menu at
openMenu([nodeKey]);
} else {
this.quickSearchBar.classList.add("invalid-search");
// We only know the name of the node to select (due to something
// like the user searching for this name). Therefore, if there are
// multiple nodes with this same name, things will be ambiguous.
var nodeKeys = this.empress._nameToKeys[nodeName];
if (nodeKeys !== undefined) {
// At least one node with this name exists
openMenu(nodeKeys);
} else {
// No nodes have this name
this.quickSearchBar.classList.add("invalid-search");
}
}
};

Expand Down
4 changes: 2 additions & 2 deletions empress/support_files/js/legend.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ define(["underscore", "util"], function (_, util) {

/**
* Display a color key in the legend box.
* @param {string} name - key name
* @param {String} name - key name
* @param {Object} info - key information
* @param {Object} container - container DOM
* @param {boolean} gradient - gradient or discrete
* @param {Boolean} gradient - gradient or discrete
*/
Legend.prototype.addColorKey = function (name, info, container, gradient) {
var legendContainer = this.__getLegend(container);
Expand Down
87 changes: 51 additions & 36 deletions empress/support_files/js/select-node-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ define(["underscore", "util"], function (_, util) {
this.drawer = drawer;
this.fields = [];
this.smTable = document.getElementById("menu-sm-table");
this.smSection = document.getElementById("menu-sm-section");
this.box = document.getElementById("menu-box");
this.sel = document.getElementById("menu-select");
this.addBtn = document.getElementById("menu-add-btn");
Expand Down Expand Up @@ -247,6 +248,8 @@ define(["underscore", "util"], function (_, util) {
"This node is a tip in the tree. These values represent the " +
"number of unique samples that contain this node.";
}
this.smSection.classList.remove("hidden");
this.smTable.classList.remove("hidden");
};

/**
Expand All @@ -261,20 +264,33 @@ define(["underscore", "util"], function (_, util) {
throw "showInternalNode(): nodeKeys is not set!";
}

var isDup = false;
if (this.nodeKeys.length > 1) {
var name = this.empress._treeData[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
// whether or not we show the sample presence info for this node.
var isUnambiguous = this.nodeKeys.length === 1;

// This is not necessarily equal to this.nodeKeys. If an internal node
// with a duplicate name was clicked on then this.nodeKeys will only
// have a single entry (the node that was clicked on): but
// keysOfNodesWithThisName will accordingly have multiple entries.
// The reason we try to figure this out here is so that we can
// determine whether or not to show a warning about duplicate names
// in the menu.
var keysOfNodesWithThisName = this.empress._nameToKeys[name];
if (keysOfNodesWithThisName.length > 1) {
this.warning.textContent =
"Warning: " +
this.nodeKeys.length +
keysOfNodesWithThisName.length +
" nodes exist with the " +
"above name.";
isDup = true;
}

// 1. Add feature metadata information (if present) for this node
// (Note that we allow duplicate-name internal nodes to have
// feature metadata; this isn't a problem)
var name = this.empress._treeData[this.nodeKeys[0]].name;
SelectedNodeMenu.makeFeatureMetadataTable(
name,
this.empress._featureMetadataColumns,
Expand Down Expand Up @@ -307,9 +323,9 @@ define(["underscore", "util"], function (_, util) {
}
}

// iterate over all keys
for (i = 0; i < this.nodeKeys.length; i++) {
var nodeKey = this.nodeKeys[i];
if (isUnambiguous) {
// this.nodeKeys has a length of 1
var nodeKey = this.nodeKeys[0];

// find first and last preorder positions of the subtree spanned
// by the current internal node
Expand Down Expand Up @@ -346,24 +362,21 @@ define(["underscore", "util"], function (_, util) {
fieldsMap[field][fieldValue] += result[fieldValue];
}
}
SelectedNodeMenu.makeSampleMetadataTable(fieldsMap, this.smTable);
this.smSection.classList.remove("hidden");
this.smTable.classList.remove("hidden");
} else {
this.smSection.classList.add("hidden");
this.smTable.classList.add("hidden");
}

SelectedNodeMenu.makeSampleMetadataTable(fieldsMap, this.smTable);
if (this.fields.length > 0) {
if (isDup) {
this.notes.textContent =
"This node is an internal node in the tree with a " +
"duplicated name. These values represent the number of " +
"unique samples that contain any of this node's " +
"descendants, aggregated across all nodes with this " +
"name. (This is buggy, so please don't trust these " +
"numbers right now.)";
} else {
this.notes.textContent =
"This node is an internal node in the tree. These " +
"values represent the number of unique samples that " +
"contain any of this node's descendants.";
}
// If isUnambiguous is false, no notes will be shown and the sample
// presence info (including the table and notes) will be hidden
if (this.fields.length > 0 && isUnambiguous) {
this.notes.textContent =
"This node is an internal node in the tree. These " +
"values represent the number of unique samples that " +
"contain any of this node's descendant tips.";
}
};

Expand All @@ -385,8 +398,12 @@ define(["underscore", "util"], function (_, util) {
* Sets the nodeKeys parameter of the state machine. This method will also
* set the buffer to highlight the selected nodes.
*
* @param{Array} nodeKeys An array of node keys. The keys should be the
* post order position of the nodes.
* @param {Array} nodeKeys An array of node keys representing the
* nodes to be selected. The keys should be the
* post order position of the nodes. If this array
* has multiple entries (i.e. multiple nodes are
* selected), the node selection menu will be
* positioned at the first node in this array.
*/
SelectedNodeMenu.prototype.setSelectedNodes = function (nodeKeys) {
// test to make sure nodeKeys represents nodes with the same name
Expand Down Expand Up @@ -430,26 +447,24 @@ define(["underscore", "util"], function (_, util) {
};

/**
* Set the coordinates of the node menu box. If nodeKeys was set to a
* single node, then the box will be placed next to that node.
* Otherwise, the box will be placed next to the root of the tree.
* Set the coordinates of the node menu box at the first node in nodeKeys.
* This means that, if only a single node is selected, the menu will be
* placed at this node's position; if multiple nodes are selected, the menu
* will be placed at the first node's position.
*/
SelectedNodeMenu.prototype.updateMenuPosition = function () {
if (this.nodeKeys === null) {
return;
}

var node = this.empress._treeData[this.nodeKeys[0]];
if (this.nodeKeys.length > 1) {
node = this.empress._treeData[this.empress._tree.size - 1];
}
var nodeToPositionAt = this.empress._treeData[this.nodeKeys[0]];
// get table coords
var x = this.empress.getX(node);
var y = this.empress.getY(node);
var x = this.empress.getX(nodeToPositionAt);
var y = this.empress.getY(nodeToPositionAt);
var tableLoc = this.drawer.toScreenSpace(x, y);

// set table location. add slight offset to location so menu appears
// next to node instead of on top of it.
// next to the node instead of on top of it.
this.box.style.left = Math.floor(tableLoc.x + 23) + "px";
this.box.style.top = Math.floor(tableLoc.y - 43) + "px";
};
Expand Down
19 changes: 11 additions & 8 deletions empress/support_files/templates/empress-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,17 @@ <h1 id="menu-box-node-id"></h1>
<p id="menu-box-warning"></p>
<h3 class="hidden" id="menu-fm-header">Feature Metadata</h3>
<table class="menu-table hidden" id="menu-fm-table"></table>
<h3 class="hidden" id="menu-sm-header">Sample Presence Information</h3>
<p id="menu-box-notes"></p>
<table class="menu-table" id="menu-sm-table"></table>
<br><strong>Select a metadata column to summarize</strong>
<label class="menu-select-container">
<select id="menu-select"></select>
</label>
<button id="menu-add-btn">Add</button>
<div id="menu-sm-section">
<h3 class="hidden" id="menu-sm-header">Sample Presence Information</h3>
<p id="menu-box-notes"></p>
<table class="menu-table" id="menu-sm-table"></table>
<br>
<strong>Select a metadata column to summarize</strong>
<label class="menu-select-container">
<select id="menu-select"></select>
</label>
<button id="menu-add-btn">Add</button>
</div>
</div>

<!-- color keys -->
Expand Down

0 comments on commit 852e399

Please sign in to comment.