Skip to content
Open
20 changes: 20 additions & 0 deletions empress/support_files/js/canvas-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ define(["underscore", "glMatrix", "SelectedNodeMenu"], function (
var x = treeSpace.x;
var y = treeSpace.y;

// If the clicked point is within the barplot area, show a menu
// for the corresponding tip node.
if (empress.isPointWithinBarplotRange(x, y)) {
var tipKey = scope.empress.getTipByBarplotClickPoint(x, y);
scope.placeBarplotNodeSelectionMenu(tipKey, x, y);
return;
}

// check if mouse is in a clade
var clade = empress.getRootNodeForPointInClade([x, y]);
if (clade !== -1) {
Expand Down Expand Up @@ -464,5 +472,17 @@ define(["underscore", "glMatrix", "SelectedNodeMenu"], function (
}
};

CanvasEvents.prototype.placeBarplotNodeSelectionMenu = function (
tipKey,
x,
y
) {
this.selectedNodeMenu.setSelectedNodes([tipKey]);
// TODO: store customx and customy here, and use when calling
// updatemenuposition in this class...
this.selectedNodeMenu.showNodeMenu(x, y);
this.empress.drawTree();
};

return CanvasEvents;
});
107 changes: 106 additions & 1 deletion empress/support_files/js/empress.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,15 @@ define([
*/
this._barplotUnit = null;

/**
* @type {Number}
* Analogous to this._maxDisplacement, but for the nearest and farthest
* edges of barplots. Used for determining if a click event falls
* within a barplot range.
*/
this._barplotMinDisplacement = null;
this._barplotMaxDisplacement = null;

/**
* @type{Boolean}
* Indicates whether or not barplots are currently drawn.
Expand Down Expand Up @@ -1558,6 +1567,10 @@ define([
this._maxDisplacement +
this._barplotPanel.distBtwnTreeAndBarplots * this._barplotUnit;

// This is the "nearest" displacement where barplots start, so store
// this for later
this._barplotMinDisplacement = maxD;

// As we iterate through the layers, we'll store the "previous layer
// max D" as a separate variable. This will help us easily work with
// layers of varying lengths.
Expand Down Expand Up @@ -1607,8 +1620,14 @@ define([
});
// Add a border on the outside of the outermost layer
if (this._barplotPanel.useBorders) {
this.addBorderBarplotLayerCoords(barplotBuffer, prevLayerMaxD);
prevLayerMaxD = scope.addBorderBarplotLayerCoords(
barplotBuffer,
prevLayerMaxD
);
}
// Update data on the farthest barplot point
this._barplotMaxDisplacement = prevLayerMaxD;

return {
coords: barplotBuffer,
colorers: colorers,
Expand Down Expand Up @@ -3717,5 +3736,91 @@ define([
}
};

Empress.prototype.isPointWithinBarplotRange = function (x, y) {
var scope = this;
if (this._barplotsDrawn) {
var inRange = function (d) {
return (
d > scope._barplotMinDisplacement &&
d <= scope._barplotMaxDisplacement
);
};

if (this._currentLayout === "Circular") {
var r = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
return inRange(r);
} else if (this._currentLayout === "Rectangular") {
return inRange(x);
} else {
// barplots unsupported for the Unrooted layout
return false;
}
} else {
return false;
}
};

Empress.prototype.getTipByBarplotClickPoint = function (x, y) {
var node;
if (this._barplotsDrawn) {
var closestTip;
if (this._currentLayout === "Rectangular") {
// Find the tip with the closest y
var closestYDist = Infinity;
// Omit this._tree.size since the root is not a tip
for (node = 1; node < this._tree.size; node++) {
if (this._tree.isleaf(this._tree.postorderselect(node))) {
var newYDist = Math.abs(this.getY(node) - y);
if (newYDist < closestYDist) {
closestYDist = newYDist;
closestTip = node;
}
}
}
} else if (this._currentLayout === "Circular") {
// We use atan2() to convert from Cartesian coordinates to
// the angle used for Polar coordinates. However, atan2 treats
// angles greater than pi (180 degrees) as being negative,
// whereas angles in Empress are stored in the range [0, 2pi].
// Basically, there are two different unit circles:
//
// Math.atan2() Empress
//
// pi/2 pi/2
// | |
//-pi or pi --+-- 0 pi --+-- 0 or 2pi
// | |
// -pi/2 3pi/2
//
// To address this, we just call atan2() and then -- if the
// angle returned is negative -- add 2pi to it. References:
// https://en.wikipedia.org/wiki/Atan2#endnote_a,
// https://stackoverflow.com/a/16614914/10730311,
// https://www.mathsisfun.com/polar-cartesian-coordinates.html.
var ptAngle = Math.atan2(y, x);
if (ptAngle < 0) {
ptAngle += Math.PI * 2;
}
var closestAngleDist = Infinity;
for (node = 1; node < this._tree.size; node++) {
if (this._tree.isleaf(this._tree.postorderselect(node))) {
var newAngleDist = Math.abs(
this.getNodeInfo(node, "angle") - ptAngle
);
if (newAngleDist < closestAngleDist) {
closestAngleDist = newAngleDist;
closestTip = node;
}
}
}
} else {
throw new Error("Unclear layout for barplots?");
}
return closestTip;
} else {
throw new Error("Barplots not drawn?");
}
};

return Empress;
});
31 changes: 20 additions & 11 deletions empress/support_files/js/select-node-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ define(["underscore", "util"], function (_, util) {
* Displays the node selection menu. nodeKeys must be set in order to use
* this method.
*/
SelectedNodeMenu.prototype.showNodeMenu = function () {
SelectedNodeMenu.prototype.showNodeMenu = function (customX, customY) {
// make sure the state machine is set
if (this.nodeKeys === null) {
throw "showNodeMenu(): Nodes have not been selected.";
Expand All @@ -352,10 +352,11 @@ define(["underscore", "util"], function (_, util) {
this.showInternalNode();
}

// place menu-node menu next to the selected node
// (if multiple nodes are selected, updateMenuPosition() positions the
// menu next to an arbitrary one)
this.updateMenuPosition();
// place menu-node menu next to:
// -position of the only selected node, if only 1 node is selected
// -position of an arbitrary selected node, if multiple are selected
// -custom position, if specified
this.updateMenuPosition(customX, customY);

show(this.box);

Expand Down Expand Up @@ -605,16 +606,24 @@ define(["underscore", "util"], function (_, util) {
* 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 () {
SelectedNodeMenu.prototype.updateMenuPosition = function (
customX,
customY
) {
if (this.nodeKeys === null) {
return;
}

var nodeToPositionAt = this.nodeKeys[0];
// get table coords
var x = this.empress.getX(nodeToPositionAt);
var y = this.empress.getY(nodeToPositionAt);
var tableLoc = this.drawer.toScreenSpace(x, y);
var x, y, tableLoc;
if (_.isUndefined(customX) && _.isUndefined(customY)) {
var nodeToPositionAt = this.nodeKeys[0];
x = this.empress.getX(nodeToPositionAt);
y = this.empress.getY(nodeToPositionAt);
} else {
x = customX;
y = customY;
}
tableLoc = this.drawer.toScreenSpace(x, y);

// set table location. add slight offset to location so menu appears
// next to the node instead of on top of it.
Expand Down