From 8025ecc0ea35fd202f8e5955439b532fe5b24c77 Mon Sep 17 00:00:00 2001 From: Marcus Fedarko Date: Tue, 20 Oct 2020 12:19:38 -0700 Subject: [PATCH 1/6] Support clicking on barplots. Kinda. #308 Bugs: - for some reason this selects internal nodes sometimes? even though it should be filtering to leaves. not sure. - menu isn't positioned correctly (at barplot) - menu doesn't show up until tree is zoomed or panned, implying that the drawtree call somehow isn't working --- empress/support_files/js/canvas-events.js | 14 +++ empress/support_files/js/empress.js | 96 +++++++++++++++++++- empress/support_files/js/select-node-menu.js | 28 ++++-- 3 files changed, 126 insertions(+), 12 deletions(-) diff --git a/empress/support_files/js/canvas-events.js b/empress/support_files/js/canvas-events.js index d8c342bdd..27af8f07c 100644 --- a/empress/support_files/js/canvas-events.js +++ b/empress/support_files/js/canvas-events.js @@ -142,6 +142,14 @@ define(["glMatrix", "SelectedNodeMenu"], function (gl, SelectedNodeMenu) { 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) { @@ -429,5 +437,11 @@ define(["glMatrix", "SelectedNodeMenu"], function (gl, SelectedNodeMenu) { } }; + CanvasEvents.prototype.placeBarplotNodeSelectionMenu = function (tipKey, x, y) { + this.selectedNodeMenu.setSelectedNodes([tipKey]); + 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 8f8a40c2a..2c942af8c 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -236,6 +236,21 @@ define([ */ this._maxDisplacement = null; + /** + * @type {Number} + * Analogous to this._maxDisplacement, but for the farthest edge + * of barplots. Used for determining if a click event falls within + * a barplot range. + */ + this._barplotMaxDisplacement = null; + + /** + * type {Number} + * The gap (in displacement) between the farthest node's endpoint (at + * this._maxDisplacement) and the first barplot layer. + */ + this._INITIAL_BARPLOT_GAP = 100; + /** * @type{Boolean} * Indicates whether or not barplots are currently drawn. @@ -1284,7 +1299,7 @@ define([ // start drawing barplots, and the first barplot layer. This could be // made into a barplot-panel-level configurable thing if desired. // (Note that, as with barplot lengths, the units here are arbitrary.) - var maxD = this._maxDisplacement + 100; + var maxD = this._maxDisplacement + this._INITIAL_BARPLOT_GAP; // 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 @@ -1325,8 +1340,13 @@ define([ }); // Add a border on the outside of the outermost layer if (scope._barplotPanel.useBorders) { - scope.addBorderBarplotLayerCoords(coords, prevLayerMaxD); + prevLayerMaxD = scope.addBorderBarplotLayerCoords( + coords, prevLayerMaxD + ); } + // Update data on the farthest barplot point + this._barplotMaxDisplacement = prevLayerMaxD; + // NOTE that we purposefuly don't clear the barplot buffer until we // know all of the barplots are valid. If we were to call // this.loadBarplotBuff([]) at the start of this function, then if we'd @@ -3238,5 +3258,77 @@ define([ } }; + Empress.prototype.isPointWithinBarplotRange = function (x, y) { + var scope = this; + if (this._barplotsDrawn) { + var inRange = function (d) { + return ( + d > scope._maxDisplacement + scope._INITIAL_BARPLOT_GAP && + 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) { + if (this._barplotsDrawn) { + if (this._currentLayout === "Rectangular") { + // Find the tip with the closest y + var closestTip; + var closestYDist = Infinity; + // Omit this._tree.size since the root is not a tip + for (var node = 1; node < this._tree.size; node++) { + if (this._tree.isleaf(this._tree.postorder(node))) { + var newYDist = Math.abs(this.getY(node) - y); + if (newYDist < closestYDist) { + closestYDist = newYDist; + closestTip = node; + } + } + } + return closestTip; + } else if (this._currentLayout === "Circular") { + // Shoutouts to + // https://www.mathsisfun.com/polar-cartesian-coordinates.html + var ptAngle = Math.atan(y / x); + if (x < 0) { + ptAngle += Math.PI; + } else if (x > 0 && y < 0) { + ptAngle += Math.PI * 2; + } + var closestTip; + var closestAngleDist = Infinity; + for (var node = 1; node < this._tree.size; node++) { + if (this._tree.isleaf(this._tree.postorder(node))) { + var newAngleDist = Math.abs( + this.getNodeInfo(node, "angle") - ptAngle + ); + if (newAngleDist < closestAngleDist) { + closestAngleDist = newAngleDist; + closestTip = node; + } + } + } + return closestTip; + } else { + throw new Error("Unclear layout for barplots?"); + } + } 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 5882eb113..796065512 100644 --- a/empress/support_files/js/select-node-menu.js +++ b/empress/support_files/js/select-node-menu.js @@ -179,7 +179,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 be set in the state machine!"; @@ -208,9 +208,11 @@ define(["underscore", "util"], function (_, util) { this.showInternalNode(); } - // place menu-node menu next to node - // otherwise place the (aggregated) node-menu over the root of the tree - 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 table this.box.classList.remove("hidden"); @@ -507,16 +509,22 @@ 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); + tableLoc = this.drawer.toScreenSpace(x, y); + } else { + x = customX; + y = customY; + tableLoc = {x: x, y: y}; + } // set table location. add slight offset to location so menu appears // next to the node instead of on top of it. From 1e2ee22c5d78270ac0935ef768326dea684110ae Mon Sep 17 00:00:00 2001 From: Marcus Fedarko Date: Tue, 20 Oct 2020 12:29:45 -0700 Subject: [PATCH 2/6] some fixes gotta fix positioning but past that should be ok --- empress/support_files/js/canvas-events.js | 2 ++ empress/support_files/js/empress.js | 4 ++-- empress/support_files/js/select-node-menu.js | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/empress/support_files/js/canvas-events.js b/empress/support_files/js/canvas-events.js index 27af8f07c..623715dc4 100644 --- a/empress/support_files/js/canvas-events.js +++ b/empress/support_files/js/canvas-events.js @@ -439,6 +439,8 @@ define(["glMatrix", "SelectedNodeMenu"], function (gl, SelectedNodeMenu) { 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(); }; diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 2c942af8c..7fd16ed62 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -3290,7 +3290,7 @@ define([ var closestYDist = Infinity; // Omit this._tree.size since the root is not a tip for (var node = 1; node < this._tree.size; node++) { - if (this._tree.isleaf(this._tree.postorder(node))) { + if (this._tree.isleaf(this._tree.postorderselect(node))) { var newYDist = Math.abs(this.getY(node) - y); if (newYDist < closestYDist) { closestYDist = newYDist; @@ -3311,7 +3311,7 @@ define([ var closestTip; var closestAngleDist = Infinity; for (var node = 1; node < this._tree.size; node++) { - if (this._tree.isleaf(this._tree.postorder(node))) { + if (this._tree.isleaf(this._tree.postorderselect(node))) { var newAngleDist = Math.abs( this.getNodeInfo(node, "angle") - ptAngle ); diff --git a/empress/support_files/js/select-node-menu.js b/empress/support_files/js/select-node-menu.js index 796065512..a0f86a00b 100644 --- a/empress/support_files/js/select-node-menu.js +++ b/empress/support_files/js/select-node-menu.js @@ -514,16 +514,16 @@ define(["underscore", "util"], function (_, util) { return; } - var x, y, tableLoc; + var tableLoc; if (_.isUndefined(customX) && _.isUndefined(customY)) { + console.log("case1"); var nodeToPositionAt = this.nodeKeys[0]; - x = this.empress.getX(nodeToPositionAt); - y = this.empress.getY(nodeToPositionAt); + var x = this.empress.getX(nodeToPositionAt); + var y = this.empress.getY(nodeToPositionAt); tableLoc = this.drawer.toScreenSpace(x, y); } else { - x = customX; - y = customY; - tableLoc = {x: x, y: y}; + console.log("case2"); + tableLoc = {x: customX, y: customY}; } // set table location. add slight offset to location so menu appears From a1127d3988a725d7274138a4fd71ecc0828860d1 Mon Sep 17 00:00:00 2001 From: Marcus Fedarko Date: Tue, 20 Oct 2020 16:13:36 -0700 Subject: [PATCH 3/6] Show selected node menu at clicked barplots! Doesn't work well with zooming/panning/etc tho yet. We _should_ modify the selected node menu class so that updateMenuPosition() is only called after a node has already been selected, I guess...? Or, like, rather than following a node's position, it now follows the barplot's position. Or the node's position at some barplot displacement. IDK. --- empress/support_files/js/select-node-menu.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/empress/support_files/js/select-node-menu.js b/empress/support_files/js/select-node-menu.js index a0f86a00b..88072fd13 100644 --- a/empress/support_files/js/select-node-menu.js +++ b/empress/support_files/js/select-node-menu.js @@ -514,17 +514,16 @@ define(["underscore", "util"], function (_, util) { return; } - var tableLoc; + var x,y, tableLoc; if (_.isUndefined(customX) && _.isUndefined(customY)) { - console.log("case1"); var nodeToPositionAt = this.nodeKeys[0]; - var x = this.empress.getX(nodeToPositionAt); - var y = this.empress.getY(nodeToPositionAt); - tableLoc = this.drawer.toScreenSpace(x, y); + x = this.empress.getX(nodeToPositionAt); + y = this.empress.getY(nodeToPositionAt); } else { - console.log("case2"); - tableLoc = {x: customX, y: customY}; + 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. From d8d7cf05dac1e667612a5b074b4ce1d24a3a6092 Mon Sep 17 00:00:00 2001 From: Marcus Fedarko Date: Tue, 20 Oct 2020 16:51:14 -0700 Subject: [PATCH 4/6] tidy & document arctan stuff Remaining TODOs: - Fix schmoovement of node selection menu when zooming/panning after clicking on barplots -- rather than "anchoring" the menu to a node's position in tree space we should anchor it to a barplot (or really just the coordinate clicked)'s position. hm. Not sure how best to do that; will likely require some decent refactoring. - Fix the way barplot clicks are mapped to tips. Right now it's possible to click outside of a tip's half-angle range (i.e. clicking visibly on another bar) and another tip will get selected. I think it might be best to address this by just checking which tip's "half angle range" the click falls in? not sure how best to do that tho. --- empress/support_files/js/empress.js | 35 ++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 7fd16ed62..076d750dd 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -3284,9 +3284,9 @@ define([ Empress.prototype.getTipByBarplotClickPoint = function (x, y) { if (this._barplotsDrawn) { + var closestTip; if (this._currentLayout === "Rectangular") { // Find the tip with the closest y - var closestTip; var closestYDist = Infinity; // Omit this._tree.size since the root is not a tip for (var node = 1; node < this._tree.size; node++) { @@ -3298,17 +3298,30 @@ define([ } } } - return closestTip; } else if (this._currentLayout === "Circular") { - // Shoutouts to - // https://www.mathsisfun.com/polar-cartesian-coordinates.html - var ptAngle = Math.atan(y / x); - if (x < 0) { - ptAngle += Math.PI; - } else if (x > 0 && y < 0) { - ptAngle += Math.PI * 2; + // 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 closestTip; var closestAngleDist = Infinity; for (var node = 1; node < this._tree.size; node++) { if (this._tree.isleaf(this._tree.postorderselect(node))) { @@ -3321,10 +3334,10 @@ define([ } } } - return closestTip; } else { throw new Error("Unclear layout for barplots?"); } + return closestTip; } else { throw new Error("Barplots not drawn?"); } From 17b05af9d842ae0a2e7f3e48d97f0bcc1d864326 Mon Sep 17 00:00:00 2001 From: Marcus Fedarko Date: Tue, 20 Oct 2020 16:56:30 -0700 Subject: [PATCH 5/6] STY --- empress/support_files/js/canvas-events.js | 6 +++++- empress/support_files/js/empress.js | 5 +++-- empress/support_files/js/select-node-menu.js | 7 +++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/empress/support_files/js/canvas-events.js b/empress/support_files/js/canvas-events.js index 623715dc4..4a8144ede 100644 --- a/empress/support_files/js/canvas-events.js +++ b/empress/support_files/js/canvas-events.js @@ -437,7 +437,11 @@ define(["glMatrix", "SelectedNodeMenu"], function (gl, SelectedNodeMenu) { } }; - CanvasEvents.prototype.placeBarplotNodeSelectionMenu = function (tipKey, x, y) { + 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... diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 076d750dd..b1f08f0fb 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -1341,7 +1341,8 @@ define([ // Add a border on the outside of the outermost layer if (scope._barplotPanel.useBorders) { prevLayerMaxD = scope.addBorderBarplotLayerCoords( - coords, prevLayerMaxD + coords, + prevLayerMaxD ); } // Update data on the farthest barplot point @@ -3320,7 +3321,7 @@ define([ // https://www.mathsisfun.com/polar-cartesian-coordinates.html. var ptAngle = Math.atan2(y, x); if (ptAngle < 0) { - ptAngle += (Math.PI * 2); + ptAngle += Math.PI * 2; } var closestAngleDist = Infinity; for (var node = 1; node < this._tree.size; node++) { diff --git a/empress/support_files/js/select-node-menu.js b/empress/support_files/js/select-node-menu.js index 88072fd13..4a5f99932 100644 --- a/empress/support_files/js/select-node-menu.js +++ b/empress/support_files/js/select-node-menu.js @@ -509,12 +509,15 @@ 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 (customX, customY) { + SelectedNodeMenu.prototype.updateMenuPosition = function ( + customX, + customY + ) { if (this.nodeKeys === null) { return; } - var x,y, tableLoc; + var x, y, tableLoc; if (_.isUndefined(customX) && _.isUndefined(customY)) { var nodeToPositionAt = this.nodeKeys[0]; x = this.empress.getX(nodeToPositionAt); From 7f76df917be81e01c7241236ebde4004fcc4d89d Mon Sep 17 00:00:00 2001 From: Marcus Fedarko Date: Mon, 22 Mar 2021 15:43:17 -0700 Subject: [PATCH 6/6] STY: appease jshint / prettier --- empress/support_files/js/empress.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 2ff302caf..01ef582e7 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -236,7 +236,7 @@ define([ * @private */ this._maxDisplacement = null; - + /** * @type {Number} * A multiple of this._maxDisplacement. This is used as the unit for @@ -1597,7 +1597,8 @@ define([ // Add a border on the outside of the outermost layer if (this._barplotPanel.useBorders) { prevLayerMaxD = scope.addBorderBarplotLayerCoords( - barplotBuffer, prevLayerMaxD + barplotBuffer, + prevLayerMaxD ); } // Update data on the farthest barplot point @@ -3605,13 +3606,14 @@ define([ }; 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 (var node = 1; node < this._tree.size; node++) { + 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) { @@ -3645,7 +3647,7 @@ define([ ptAngle += Math.PI * 2; } var closestAngleDist = Infinity; - for (var node = 1; node < this._tree.size; node++) { + 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