diff --git a/empress/support_files/js/canvas-events.js b/empress/support_files/js/canvas-events.js index cdca090d..a03909a0 100644 --- a/empress/support_files/js/canvas-events.js +++ b/empress/support_files/js/canvas-events.js @@ -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) { @@ -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; }); diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 873b6ae1..c48013be 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -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. @@ -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. @@ -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, @@ -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; }); diff --git a/empress/support_files/js/select-node-menu.js b/empress/support_files/js/select-node-menu.js index 635929ed..444bbb22 100644 --- a/empress/support_files/js/select-node-menu.js +++ b/empress/support_files/js/select-node-menu.js @@ -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."; @@ -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); @@ -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.