mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-25 00:12:08 +08:00
955 lines
33 KiB
JavaScript
955 lines
33 KiB
JavaScript
var UserMenuBase = Backbone.View.extend({
|
|
/**
|
|
* Base class of any menus that takes in user interaction. Contains checking methods.
|
|
*/
|
|
|
|
className: 'UserMenuBase',
|
|
|
|
isAcceptableValue : function ($inputKey, min, max) {
|
|
/**
|
|
* Check if an input value is a number and falls within max min.
|
|
*/
|
|
var self = this,
|
|
value = $inputKey.val(),
|
|
fieldName = $inputKey.attr("displayLabel") || $inputKey.attr("id").replace("phyloViz", "");
|
|
|
|
function isNumeric(n) {
|
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
|
}
|
|
|
|
if (!isNumeric(value)){
|
|
alert(fieldName + " is not a number!");
|
|
return false;
|
|
}
|
|
|
|
if ( value > max){
|
|
alert(fieldName + " is too large.");
|
|
return false;
|
|
} else if ( value < min) {
|
|
alert(fieldName + " is too small.");
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
hasIllegalJsonCharacters : function($inputKey) {
|
|
/**
|
|
* Check if any user string inputs has illegal characters that json cannot accept
|
|
*/
|
|
if ($inputKey.val().search(/"|'|\\/) !== -1){
|
|
alert("Named fields cannot contain these illegal characters: double quote(\"), single guote(\'), or back slash(\\). ");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
|
|
function PhyloTreeLayout() {
|
|
/**
|
|
* -- Custom Layout call for phyloViz to suit the needs of a phylogenetic tree.
|
|
* -- Specifically: 1) Nodes have a display display of (= evo dist X depth separation) from their parent
|
|
* 2) Nodes must appear in other after they have expand and contracted
|
|
*/
|
|
|
|
var self = this,
|
|
hierarchy = d3.layout.hierarchy().sort(null).value(null),
|
|
height = 360, // ! represents both the layout angle and the height of the layout, in px
|
|
layoutMode = "Linear",
|
|
leafHeight = 18, // height of each individual leaf node
|
|
depthSeparation = 200, // separation between nodes of different depth, in px
|
|
leafIndex = 0, // change to recurssive call
|
|
defaultDist = 0.5, // tree defaults to 0.5 dist if no dist is specified
|
|
maxTextWidth = 50; // maximum length of the text labels
|
|
|
|
|
|
self.leafHeight = function(inputLeafHeight){
|
|
if (typeof inputLeafHeight === "undefined"){ return leafHeight; }
|
|
else { leafHeight = inputLeafHeight; return self;}
|
|
};
|
|
|
|
self.layoutMode = function(mode){
|
|
if (typeof mode === "undefined"){ return layoutMode; }
|
|
else { layoutMode = mode; return self;}
|
|
};
|
|
|
|
self.layoutAngle = function(angle) { // changes the layout angle of the display, which is really changing the height
|
|
if (typeof angle === "undefined"){ return height; }
|
|
if (isNaN(angle) || angle < 0 || angle > 360) { return self; } // to use default if the user puts in strange values
|
|
else { height = angle; return self;}
|
|
};
|
|
|
|
self.separation = function(dist){ // changes the dist between the nodes of different depth
|
|
if (typeof dist === "undefined"){ return depthSeparation; }
|
|
else { depthSeparation = dist; return self;}
|
|
};
|
|
|
|
self.links = function (nodes) { // uses d3 native method to generate links. Done.
|
|
return d3.layout.tree().links(nodes);
|
|
};
|
|
|
|
// -- Custom method for laying out phylogeny tree in a linear fashion
|
|
self.nodes = function (d, i) {
|
|
var _nodes = hierarchy.call(self, d, i), // self is to find the depth of all the nodes, assumes root is passed in
|
|
nodes = [],
|
|
maxDepth = 0,
|
|
numLeaves = 0;
|
|
|
|
// changing from hierarchy's custom format for data to usable format
|
|
_nodes.forEach(function (_node){
|
|
var node = _node.data;
|
|
node.depth = _node.depth;
|
|
maxDepth = node.depth > maxDepth ? node.depth : maxDepth; //finding max depth of tree
|
|
nodes.push(node);
|
|
});
|
|
// counting the number of leaf nodes and assigning max depth to nodes that do not have children to flush all the leave nodes
|
|
nodes.forEach(function(node){
|
|
if ( !node.children ) { //&& !node._children
|
|
numLeaves += 1;
|
|
node.depth = maxDepth; // if a leaf has no child it would be assigned max depth
|
|
}
|
|
});
|
|
|
|
leafHeight = layoutMode === "Circular" ? height / numLeaves : leafHeight;
|
|
leafIndex = 0;
|
|
layout(nodes[0], maxDepth, leafHeight, null);
|
|
|
|
return nodes;
|
|
};
|
|
|
|
|
|
function layout (node, maxDepth, vertSeparation, parent) {
|
|
/**
|
|
* -- Function with side effect of adding x0, y0 to all child; take in the root as starting point
|
|
* assuming that the leave nodes would be sorted in presented order
|
|
* horizontal(y0) is calculated according to (= evo dist X depth separation) from their parent
|
|
* vertical (x0) - if leave node: find its order in all of the leave node === node.id, then multiply by verticalSeparation
|
|
* - if parent node: is place in the mid point all of its children nodes
|
|
* -- The layout will first calculate the y0 field going towards the leaves, and x0 when returning
|
|
*/
|
|
var children = node.children,
|
|
sumChildVertSeparation = 0;
|
|
|
|
// calculation of node's dist from parents, going down.
|
|
var dist = node.dist || defaultDist;
|
|
dist = dist > 1 ? 1 : dist; // We constrain all dist to be less than one
|
|
node.dist = dist;
|
|
if (parent !== null){
|
|
node.y0 = parent.y0 + dist * depthSeparation;
|
|
} else { //root node
|
|
node.y0 = maxTextWidth;
|
|
}
|
|
|
|
|
|
// if a node have no children, we will treat it as a leaf and start laying it out first
|
|
if (!children) {
|
|
node.x0 = leafIndex++ * vertSeparation;
|
|
} else {
|
|
// if it has children, we will visit all its children and calculate its position from its children
|
|
children.forEach( function (child) {
|
|
child.parent = node;
|
|
sumChildVertSeparation += layout(child, maxDepth, vertSeparation, node);
|
|
});
|
|
node.x0 = sumChildVertSeparation / children.length;
|
|
}
|
|
|
|
// adding properties to the newly created node
|
|
node.x = node.x0;
|
|
node.y = node.y0;
|
|
return node.x0;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
|
|
/**
|
|
* -- PhyloTree Model --
|
|
*/
|
|
var PhyloTree = Visualization.extend({
|
|
defaults : {
|
|
layout: "Linear",
|
|
separation : 250, // px dist between nodes of different depth to represent 1 evolutionary until
|
|
leafHeight: 18,
|
|
type : "phyloviz", // visualization type
|
|
title : "Title",
|
|
scaleFactor: 1,
|
|
translate: [0,0],
|
|
fontSize: 12, //fontSize of node label
|
|
selectedNode : null,
|
|
nodeAttrChangedTime : 0
|
|
},
|
|
|
|
root : {}, // Root has to be its own independent object because it is not part of the viz_config
|
|
|
|
toggle : function (d) {
|
|
/**
|
|
* Mechanism to expand or contract a single node. Expanded nodes have a children list, while for
|
|
* contracted nodes the list is stored in _children. Nodes with their children data stored in _children will not have their
|
|
* children rendered.
|
|
*/
|
|
if(typeof d === "undefined") {return ;}
|
|
if (d.children ) {
|
|
d._children = d.children;
|
|
d.children = null;
|
|
} else {
|
|
d.children = d._children;
|
|
d._children = null;
|
|
}
|
|
},
|
|
|
|
toggleAll : function(d) {
|
|
/**
|
|
* Contracts the phylotree to a single node by repeatedly calling itself to place all the list
|
|
* of children under _children.
|
|
*/
|
|
if (d.children && d.children.length !== 0) {
|
|
d.children.forEach(this.toggleAll);
|
|
toggle(d);
|
|
}
|
|
},
|
|
|
|
getData : function (){
|
|
/**
|
|
* Return the data of the tree. Used for preserving state.
|
|
*/
|
|
return this.root;
|
|
},
|
|
|
|
save: function() {
|
|
/**
|
|
* Overriding the default save mechanism to do some clean of circular reference of the
|
|
* phyloTree and to include phyloTree in the saved json
|
|
*/
|
|
var root = this.root;
|
|
cleanTree(root);
|
|
this.set("root", root);
|
|
|
|
function cleanTree(node){
|
|
// we need to remove parent to delete circular reference
|
|
delete node.parent;
|
|
|
|
// removing unnecessary attributes
|
|
if (node._selected){ delete node._selected;}
|
|
|
|
node.children ? node.children.forEach(cleanTree) : 0;
|
|
node._children ? node._children.forEach(cleanTree) : 0;
|
|
}
|
|
|
|
var config = jQuery.extend(true, {}, this.attributes);
|
|
config["selectedNode"] = null;
|
|
|
|
show_message("Saving to Galaxy", "progress");
|
|
|
|
return $.ajax({
|
|
url: this.url(),
|
|
type: "POST",
|
|
dataType: "json",
|
|
data: {
|
|
vis_json: JSON.stringify(config)
|
|
},
|
|
success: function(res){
|
|
var viz_id = res.url.split("id=")[1].split("&")[0],
|
|
viz_url = "/phyloviz/visualization?id=" + viz_id;
|
|
window.history.pushState({}, "", viz_url + window.location.hash);
|
|
hide_modal();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
* -- Views --
|
|
*/
|
|
var PhylovizLayoutBase = Backbone.View.extend({
|
|
/**
|
|
* Stores the default variable for setting up the visualization
|
|
*/
|
|
defaults : {
|
|
nodeRadius : 4.5 // radius of each node in the diagram
|
|
},
|
|
|
|
|
|
stdInit : function (options) {
|
|
/**
|
|
* Common initialization in layouts
|
|
*/
|
|
|
|
var self = this;
|
|
self.model.on("change:separation change:leafHeight change:fontSize change:nodeAttrChangedTime", self.updateAndRender, self);
|
|
|
|
self.vis = options.vis;
|
|
self.i = 0;
|
|
self.maxDepth = -1; // stores the max depth of the tree
|
|
|
|
self.width = options.width;
|
|
self.height = options.height;
|
|
},
|
|
|
|
|
|
updateAndRender : function(source) {
|
|
/**
|
|
* Updates the visualization whenever there are changes in the expansion and contraction of nodes
|
|
* AND possibly when the tree is edited.
|
|
*/
|
|
var vis = d3.select(".vis"),
|
|
self = this;
|
|
source = source || self.model.root;
|
|
|
|
self.renderNodes(source);
|
|
self.renderLinks(source);
|
|
self.addTooltips();
|
|
},
|
|
|
|
|
|
renderLinks : function(source) {
|
|
/**
|
|
* Renders the links for the visualization.
|
|
*/
|
|
var self = this;
|
|
var diagonal = self.diagonal;
|
|
var duration = self.duration;
|
|
var layoutMode = self.layoutMode;
|
|
var link = self.vis.selectAll("g.completeLink")
|
|
.data(self.tree.links(self.nodes), function(d) { return d.target.id; });
|
|
|
|
var calcalateLinePos = function(d) {
|
|
d.pos0 = d.source.y0 + " " + d.source.x0; // position of the source node <=> starting location of the line drawn
|
|
d.pos1 = d.source.y0 + " " + d.target.x0; // position where the line makes a right angle bend
|
|
d.pos2 = d.target.y0 + " " + d.target.x0; // point where the horizontal line becomes a dotted line
|
|
};
|
|
|
|
var linkEnter = link.enter().insert("svg:g","g.node")
|
|
.attr("class", "completeLink");
|
|
|
|
|
|
linkEnter.append("svg:path")
|
|
.attr("class", "link")
|
|
.attr("d", function(d) {
|
|
calcalateLinePos(d);
|
|
return "M " + d.pos0 + " L " + d.pos1;
|
|
});
|
|
|
|
var linkUpdate = link.transition().duration(500);
|
|
|
|
linkUpdate.select("path.link")
|
|
.attr("d", function(d) {
|
|
calcalateLinePos(d);
|
|
return "M " + d.pos0 + " L " + d.pos1 + " L " + d.pos2;
|
|
});
|
|
|
|
var linkExit = link.exit().remove();
|
|
|
|
},
|
|
|
|
// User Interaction methods below
|
|
|
|
selectNode : function(node){
|
|
/**
|
|
* Displays the information for editting
|
|
*/
|
|
var self = this;
|
|
d3.selectAll("g.node")
|
|
.classed("selectedHighlight", function(d){
|
|
if (node.id === d.id){
|
|
if(node._selected) { // for de=selecting node.
|
|
delete node._selected;
|
|
return false;
|
|
} else {
|
|
node._selected = true;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
|
|
self.model.set("selectedNode", node);
|
|
$("#phyloVizSelectedNodeName").val(node.name);
|
|
$("#phyloVizSelectedNodeDist").val(node.dist);
|
|
$("#phyloVizSelectedNodeAnnotation").val(node.annotation || "");
|
|
},
|
|
|
|
addTooltips : function (){
|
|
/**
|
|
* Creates bootstrap tooltip for the visualization. Has to be called repeatedly due to newly generated
|
|
* enterNodes
|
|
*/
|
|
$(".bs-tooltip").remove(); //clean up tooltip, just in case its listeners are removed by d3
|
|
$(".node")
|
|
.attr("data-original-title", function(){
|
|
var d = this.__data__,
|
|
annotation = d.annotation || "None" ;
|
|
return d ? (d.name ? d.name + "<br/>" : "") + "Dist: " + d.dist + " <br/>Annotation: " + annotation: "";
|
|
})
|
|
.tooltip({'placement':'top', 'trigger' : 'hover'});
|
|
|
|
}
|
|
});
|
|
|
|
|
|
|
|
|
|
var PhylovizLinearView = PhylovizLayoutBase.extend({
|
|
/**
|
|
* Linea layout class of Phyloviz, is responsible for rendering the nodes
|
|
* calls PhyloTreeLayout to determine the positions of the nodes
|
|
*/
|
|
initialize : function(options){
|
|
// Default values of linear layout
|
|
var self = this;
|
|
self.margins = options.margins;
|
|
self.layoutMode = "Linear";
|
|
|
|
self.stdInit(options);
|
|
|
|
self.layout();
|
|
self.updateAndRender(self.model.root);
|
|
},
|
|
|
|
layout : function() {
|
|
/**
|
|
* Creates the basic layout of a linear tree by precalculating fixed values.
|
|
* One of calculations are also made here
|
|
*/
|
|
|
|
var self = this;
|
|
|
|
self.tree = new PhyloTreeLayout().layoutMode("Linear");
|
|
self.diagonal = d3.svg.diagonal()
|
|
.projection(function(d) { return [d.y, d.x ]; });
|
|
},
|
|
|
|
renderNodes : function (source) {
|
|
/**
|
|
* Renders the nodes base on Linear layout.
|
|
*/
|
|
var self = this,
|
|
fontSize = self.model.get("fontSize") + "px";
|
|
|
|
// assigning properties from models
|
|
self.tree.separation(self.model.get("separation")).leafHeight(self.model.get("leafHeight"));
|
|
|
|
var duration = 500,
|
|
nodes = self.tree.separation(self.model.get("separation")).nodes(self.model.root);
|
|
|
|
var node = self.vis.selectAll("g.node")
|
|
.data(nodes, function(d) { return d.name + d.id || (d.id = ++self.i); });
|
|
|
|
// These variables has to be passed into update links which are in the base methods
|
|
self.nodes = nodes;
|
|
self.duration = duration;
|
|
|
|
// ------- D3 ENTRY --------
|
|
// Enter any new nodes at the parent's previous position.
|
|
var nodeEnter = node.enter().append("svg:g")
|
|
.attr("class", "node")
|
|
.on("dblclick", function(){ d3.event.stopPropagation(); })
|
|
.on("click", function(d) {
|
|
if (d3.event.altKey) {
|
|
self.selectNode(d); // display info if alt is pressed
|
|
} else {
|
|
if(d.children && d.children.length === 0){ return;} // there is no need to toggle leaves
|
|
self.model.toggle(d); // contract/expand nodes at data level
|
|
self.updateAndRender(d); // re-render the tree
|
|
}
|
|
});
|
|
|
|
nodeEnter.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; });
|
|
|
|
nodeEnter.append("svg:circle")
|
|
.attr("r", 1e-6)
|
|
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
|
|
|
|
nodeEnter.append("svg:text")
|
|
.attr("class", "nodeLabel")
|
|
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
|
|
.attr("dy", ".35em")
|
|
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
|
|
.style("fill-opacity", 1e-6);
|
|
|
|
// ------- D3 TRANSITION --------
|
|
// Transition nodes to their new position.
|
|
var nodeUpdate = node.transition()
|
|
.duration(duration);
|
|
|
|
nodeUpdate.attr("transform", function(d) {
|
|
return "translate(" + d.y + "," + d.x + ")"; });
|
|
|
|
nodeUpdate.select("circle")
|
|
.attr("r", self.defaults.nodeRadius)
|
|
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
|
|
|
|
nodeUpdate.select("text")
|
|
.style("fill-opacity", 1)
|
|
.style("font-size", fontSize)
|
|
.text(function(d) { return d.name; });
|
|
|
|
// ------- D3 EXIT --------
|
|
// Transition exiting nodes to the parent's new position.
|
|
var nodeExit =node.exit().transition()
|
|
.duration(duration)
|
|
.remove();
|
|
|
|
nodeExit.select("circle")
|
|
.attr("r", 1e-6);
|
|
|
|
nodeExit.select("text")
|
|
.style("fill-opacity", 1e-6);
|
|
|
|
// Stash the old positions for transition.
|
|
nodes.forEach(function(d) {
|
|
d.x0 = d.x; // we need the x0, y0 for parents with children
|
|
d.y0 = d.y;
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
var PhylovizView = Backbone.View.extend({
|
|
|
|
className: 'phyloviz',
|
|
|
|
initialize: function(options) {
|
|
var self = this;
|
|
// -- Default values of the vis
|
|
self.MIN_SCALE = 0.05; //for zooming
|
|
self.MAX_SCALE = 5;
|
|
self.MAX_DISPLACEMENT = 500;
|
|
self.margins = [10, 60, 10, 80];
|
|
|
|
self.width = $("#PhyloViz").width();
|
|
self.height = $("#PhyloViz").height();
|
|
self.radius = self.width;
|
|
self.data = options.data;
|
|
|
|
// -- Events Phyloviz view responses to
|
|
$(window).resize(function(){
|
|
self.width = $("#PhyloViz").width();
|
|
self.height = $("#PhyloViz").height();
|
|
self.render();
|
|
});
|
|
|
|
// -- Create phyloTree model
|
|
self.phyloTree = new PhyloTree(options.config);
|
|
self.phyloTree.root = self.data;
|
|
|
|
// -- Set up UI functions of main view
|
|
self.zoomFunc = d3.behavior.zoom().scaleExtent([self.MIN_SCALE, self.MAX_SCALE]);
|
|
self.zoomFunc.translate(self.phyloTree.get("translate"));
|
|
self.zoomFunc.scale(self.phyloTree.get("scaleFactor"));
|
|
|
|
// -- set up header buttons, search and settings menu
|
|
self.navMenu = new HeaderButtons(self);
|
|
self.settingsMenu = new SettingsMenu({phyloTree : self.phyloTree});
|
|
self.nodeSelectionView = new NodeSelectionView({phyloTree : self.phyloTree});
|
|
self.search = new PhyloVizSearch();
|
|
|
|
|
|
setTimeout(function(){ // using settimeout to call the zoomAndPan function according to the stored attributes in viz_config
|
|
self.zoomAndPan();
|
|
}, 1000);
|
|
},
|
|
|
|
render: function(){
|
|
// -- Creating helper function for vis. --
|
|
var self = this;
|
|
$("#PhyloViz").empty();
|
|
|
|
// -- Layout viz. --
|
|
self.mainSVG = d3.select("#PhyloViz").append("svg:svg")
|
|
.attr("width", self.width)
|
|
.attr("height", self.height)
|
|
.attr("pointer-events", "all")
|
|
.call(self.zoomFunc.on("zoom", function(){
|
|
self.zoomAndPan();
|
|
}));
|
|
|
|
self.boundingRect = self.mainSVG.append("svg:rect")
|
|
.attr("class", "boundingRect")
|
|
.attr("width", self.width)
|
|
.attr("height", self.height)
|
|
.attr("stroke", "black")
|
|
.attr("fill", "white");
|
|
|
|
self.vis = self.mainSVG
|
|
.append("svg:g")
|
|
.attr("class", "vis");
|
|
|
|
self.layoutOptions = {
|
|
model : self.phyloTree,
|
|
width : self.width,
|
|
height : self.height,
|
|
vis: self.vis,
|
|
margins: self.margins
|
|
};
|
|
|
|
// -- Creating Title
|
|
$("#title").text("Phylogenetic Tree from " + self.phyloTree.get("title") + ":");
|
|
|
|
// -- Create Linear view instance --
|
|
var linearView = new PhylovizLinearView(self.layoutOptions)
|
|
},
|
|
|
|
zoomAndPan : function(event){
|
|
/**
|
|
* Function to zoom and pan the svg element which the entire tree is contained within
|
|
* Uses d3.zoom events, and extend them to allow manual updates and keeping states in model
|
|
*/
|
|
if (typeof event !== "undefined") {
|
|
var zoomParams = event.zoom,
|
|
translateParams = event.translate;
|
|
}
|
|
|
|
var self = this,
|
|
scaleFactor = self.zoomFunc.scale(),
|
|
translationCoor = self.zoomFunc.translate(),
|
|
zoomStatement = "",
|
|
translateStatement = "";
|
|
|
|
// Do manual scaling.
|
|
switch (zoomParams) {
|
|
case "reset":
|
|
scaleFactor = 1.0;
|
|
translationCoor = [0,0]; break;
|
|
case "+":
|
|
scaleFactor *= 1.1; break;
|
|
case "-":
|
|
scaleFactor *= 0.9; break;
|
|
default:
|
|
if (typeof zoomParams === "number") {
|
|
scaleFactor = zoomParams;
|
|
} else if (d3.event !== null) {
|
|
scaleFactor = d3.event.scale;
|
|
}
|
|
}
|
|
if (scaleFactor < self.MIN_SCALE || scaleFactor > self.MAX_SCALE) { return;}
|
|
self.zoomFunc.scale(scaleFactor); //update scale Factor
|
|
zoomStatement = "translate(" + self.margins[3] + "," + self.margins[0] + ")" +
|
|
" scale(" + scaleFactor + ")";
|
|
|
|
// Do manual translation.
|
|
if( d3.event !== null) {
|
|
translateStatement = "translate(" + d3.event.translate + ")";
|
|
} else {
|
|
if(typeof translateParams !== "undefined") {
|
|
var x = translateParams.split(",")[0];
|
|
var y = translateParams.split(",")[1];
|
|
if (!isNaN(x) && !isNaN(y)){
|
|
translationCoor = [translationCoor[0] + parseFloat(x), translationCoor[1] + parseFloat(y)];
|
|
}
|
|
}
|
|
self.zoomFunc.translate(translationCoor); // update zoomFunc
|
|
translateStatement = "translate(" + translationCoor + ")";
|
|
}
|
|
|
|
self.phyloTree.set("scaleFactor", scaleFactor);
|
|
self.phyloTree.set("translate", translationCoor);
|
|
self.vis.attr("transform", translateStatement + zoomStatement); //refers to the view that we are actually zooming
|
|
},
|
|
|
|
|
|
reloadViz : function() {
|
|
/**
|
|
* Primes the Ajax URL to load another Nexus tree
|
|
*/
|
|
var self = this,
|
|
treeIndex = $("#phylovizNexSelector :selected").val(),
|
|
dataset_id = self.phyloTree.get("dataset_id"),
|
|
url = "phyloviz/getJsonData?dataset_id=" + dataset_id + "&treeIndex=" + String(treeIndex);
|
|
$.getJSON(url, function(packedJson){
|
|
window.initPhyloViz(packedJson.data, packedJson.config);
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
var HeaderButtons = Backbone.View.extend({
|
|
|
|
initialize : function(phylovizView){
|
|
var self = this;
|
|
self.phylovizView = phylovizView;
|
|
|
|
// Clean up code - if the class initialized more than once
|
|
$("#panelHeaderRightBtns").empty();
|
|
$("#phyloVizNavBtns").empty();
|
|
$("#phylovizNexSelector").off();
|
|
|
|
self.initNavBtns();
|
|
self.initRightHeaderBtns();
|
|
|
|
// Initial a tree selector in the case of nexus
|
|
$("#phylovizNexSelector").off().on("change", function() {self.phylovizView.reloadViz();} );
|
|
|
|
},
|
|
|
|
initRightHeaderBtns : function(){
|
|
var self = this;
|
|
|
|
rightMenu = create_icon_buttons_menu([
|
|
{ icon_class: 'gear', title: 'PhyloViz Settings', on_click: function(){
|
|
$("#SettingsMenu").show();
|
|
self.settingsMenu.updateUI();
|
|
} },
|
|
{ icon_class: 'disk', title: 'Save visualization', on_click: function() {
|
|
var nexSelected = $("#phylovizNexSelector option:selected").text();
|
|
if(nexSelected) {
|
|
self.phylovizView.phyloTree.set("title", nexSelected);
|
|
}
|
|
self.phylovizView.phyloTree.save();
|
|
} },
|
|
{ icon_class: 'chevron-expand', title: 'Search / Edit Nodes', on_click: function() {
|
|
$("#nodeSelectionView").show();
|
|
} },
|
|
{ icon_class: 'information', title: 'Phyloviz Help', on_click: function() {
|
|
window.open('http://wiki.g2.bx.psu.edu/Learn/Visualization/PhylogeneticTree');
|
|
// https://docs.google.com/document/d/1AXFoJgEpxr21H3LICRs3EyMe1B1X_KFPouzIgrCz3zk/edit
|
|
} }
|
|
],
|
|
{
|
|
tooltip_config: { placement: 'bottom' }
|
|
});
|
|
$("#panelHeaderRightBtns").append(rightMenu.$el);
|
|
},
|
|
|
|
initNavBtns: function() {
|
|
var self = this,
|
|
navMenu = create_icon_buttons_menu([
|
|
{ icon_class: 'zoom-in', title: 'Zoom in', on_click: function() {
|
|
self.phylovizView.zoomAndPan({ zoom : "+"});
|
|
} },
|
|
{ icon_class: 'zoom-out', title: 'Zoom out', on_click: function() {
|
|
self.phylovizView.zoomAndPan({ zoom : "-"});
|
|
} },
|
|
{ icon_class: 'arrow-circle', title: 'Reset Zoom/Pan', on_click: function() {
|
|
self.phylovizView.zoomAndPan({ zoom : "reset"});
|
|
} }
|
|
],
|
|
{
|
|
tooltip_config: { placement: 'bottom' }
|
|
});
|
|
$("#phyloVizNavBtns").append(navMenu.$el);
|
|
}
|
|
});
|
|
|
|
|
|
var SettingsMenu = UserMenuBase.extend({
|
|
|
|
className: 'Settings',
|
|
|
|
initialize: function(options){
|
|
// settings needs to directly interact with the phyloviz model so it will get access to it.
|
|
var self = this;
|
|
self.phyloTree = options.phyloTree;
|
|
self.el = $("#SettingsMenu");
|
|
self.inputs = {
|
|
separation : $("#phyloVizTreeSeparation"),
|
|
leafHeight : $("#phyloVizTreeLeafHeight"),
|
|
fontSize : $("#phyloVizTreeFontSize")
|
|
};
|
|
|
|
//init all buttons of settings
|
|
$("#settingsCloseBtn").off().on("click", function() { self.el.hide(); });
|
|
$("#phylovizResetSettingsBtn").off().on("click", function() { self.resetToDefaults(); });
|
|
$("#phylovizApplySettingsBtn").off().on("click", function() { self.apply(); });
|
|
},
|
|
|
|
apply : function(){
|
|
/**
|
|
* Applying user values to phylotree model.
|
|
*/
|
|
var self = this;
|
|
if (!self.isAcceptableValue(self.inputs["separation"], 50, 2500) ||
|
|
!self.isAcceptableValue(self.inputs["leafHeight"], 5, 30) ||
|
|
!self.isAcceptableValue(self.inputs["fontSize"], 5, 20)){
|
|
return;
|
|
}
|
|
$.each(self.inputs, function(key, $input){
|
|
self.phyloTree.set(key, $input.val());
|
|
});
|
|
},
|
|
updateUI : function(){
|
|
/**
|
|
* Called to update the values input to that stored in the model
|
|
*/
|
|
var self = this;
|
|
$.each(self.inputs, function(key, $input){
|
|
$input.val(self.phyloTree.get(key));
|
|
});
|
|
},
|
|
resetToDefaults : function(){
|
|
/**
|
|
* Resets the value of the phyloTree model to its default
|
|
*/
|
|
$(".bs-tooltip").remove(); // just in case the tool tip was not removed
|
|
var self = this;
|
|
$.each(self.phyloTree.defaults, function(key, value) {
|
|
self.phyloTree.set(key, value);
|
|
});
|
|
self.updateUI();
|
|
},
|
|
|
|
render: function(){
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
var NodeSelectionView = UserMenuBase.extend({
|
|
/**
|
|
* View for inspecting node properties and editing them
|
|
*/
|
|
className: 'Settings',
|
|
|
|
initialize : function (options){
|
|
var self = this;
|
|
self.el = $("#nodeSelectionView");
|
|
self.phyloTree = options.phyloTree;
|
|
|
|
self.UI = {
|
|
enableEdit : $('#phylovizEditNodesCheck'),
|
|
saveChanges : $('#phylovizNodeSaveChanges'),
|
|
cancelChanges : $("#phylovizNodeCancelChanges"),
|
|
name : $("#phyloVizSelectedNodeName"),
|
|
dist : $("#phyloVizSelectedNodeDist"),
|
|
annotation : $("#phyloVizSelectedNodeAnnotation")
|
|
};
|
|
|
|
self.valuesOfConcern = {
|
|
name : null,
|
|
dist : null,
|
|
annotation : null
|
|
}; // temporarily stores the values in case user change their mind
|
|
|
|
//init UI buttons
|
|
$("#nodeSelCloseBtn").off().on("click", function() { self.el.hide(); });
|
|
self.UI.saveChanges.off().on("click", function(){ self.updateNodes(); });
|
|
self.UI.cancelChanges.off().on("click", function(){ self.cancelChanges(); });
|
|
|
|
(function ($) {
|
|
// extending jquery fxn for enabling and disabling nodes.
|
|
$.fn.enable = function (isEnabled) {
|
|
return $(this).each(function () {
|
|
if(isEnabled){
|
|
$(this).removeAttr('disabled');
|
|
} else {
|
|
$(this).attr('disabled', 'disabled');
|
|
}
|
|
});
|
|
};
|
|
})(jQuery);
|
|
|
|
self.UI.enableEdit.off().on("click", function () {
|
|
self.toggleUI();
|
|
});
|
|
},
|
|
|
|
toggleUI : function(){
|
|
/**
|
|
* For turning on and off the child elements
|
|
*/
|
|
var self = this,
|
|
checked = self.UI.enableEdit.is(':checked');
|
|
|
|
!checked ? self.cancelChanges() : "";
|
|
|
|
$.each(self.valuesOfConcern, function(key, value) {
|
|
self.UI[key].enable(checked);
|
|
});
|
|
if(checked){
|
|
self.UI.saveChanges.show();
|
|
self.UI.cancelChanges.show();
|
|
} else {
|
|
self.UI.saveChanges.hide();
|
|
self.UI.cancelChanges.hide();
|
|
}
|
|
|
|
},
|
|
|
|
cancelChanges : function() {
|
|
/**
|
|
* Reverting to previous values in case user change their minds
|
|
*/
|
|
var self = this,
|
|
node = self.phyloTree.get("selectedNode");
|
|
if (node){
|
|
$.each(self.valuesOfConcern, function(key, value) {
|
|
self.UI[key].val(node[key]);
|
|
});
|
|
}
|
|
},
|
|
|
|
updateNodes : function (){
|
|
/**
|
|
* Changing the data in the underlying tree with user-specified values
|
|
*/
|
|
var self = this,
|
|
node = self.phyloTree.get("selectedNode");
|
|
if (node){
|
|
if (!self.isAcceptableValue(self.UI.dist, 0, 1) ||
|
|
self.hasIllegalJsonCharacters(self.UI.name) ||
|
|
self.hasIllegalJsonCharacters(self.UI.annotation) ) {
|
|
return;
|
|
}
|
|
$.each(self.valuesOfConcern, function(key, value) {
|
|
(node[key]) = self.UI[key].val();
|
|
});
|
|
self.phyloTree.set("nodeAttrChangedTime", new Date());
|
|
} else {
|
|
alert("No node selected");
|
|
}
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
var PhyloVizSearch = UserMenuBase.extend({
|
|
/**
|
|
* Initializes the search panel on phyloviz and handles its user interaction
|
|
* It allows user to search the entire free based on some qualifer, like dist <= val.
|
|
*/
|
|
initialize : function () {
|
|
var self = this;
|
|
|
|
$("#phyloVizSearchBtn").on("click", function(){
|
|
var searchTerm = $("#phyloVizSearchTerm"),
|
|
searchConditionVal = $("#phyloVizSearchCondition").val().split("-"),
|
|
attr = searchConditionVal[0],
|
|
condition = searchConditionVal[1];
|
|
self.hasIllegalJsonCharacters(searchTerm);
|
|
|
|
if (attr === "dist"){
|
|
self.isAcceptableValue(searchTerm, 0, 1);
|
|
}
|
|
self.searchTree(attr, condition, searchTerm.val());
|
|
});
|
|
},
|
|
|
|
searchTree : function (attr, condition, val){
|
|
/**
|
|
* Searches the entire tree and will highlight the nodes that match the condition in green
|
|
*/
|
|
d3.selectAll("g.node")
|
|
.classed("searchHighlight", function(d){
|
|
var attrVal = d[attr];
|
|
if (typeof attrVal !== "undefined" && attrVal !== null){
|
|
if (attr === "dist"){
|
|
switch (condition) {
|
|
case "greaterEqual":
|
|
return attrVal >= +val;
|
|
case "lesserEqual":
|
|
return attrVal <= +val;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
} else if (attr === "name" || attr === "annotation") {
|
|
return attrVal.toLowerCase().indexOf(val.toLowerCase()) !== -1;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}); |