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

[WIP] Barplot clicking #504

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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