1595e97504deb202b96a3e4bd5efe94f4b70c898 ceisenhart Wed Jan 13 13:31:36 2016 -0800 The new home for the visuzlization javascript code, refs# 16468, 16216,16341 diff --git src/hg/js/d3.dendrograms.js src/hg/js/d3.dendrograms.js new file mode 100644 index 0000000..a99f7c4 --- /dev/null +++ src/hg/js/d3.dendrograms.js @@ -0,0 +1,578 @@ +/* +d3.dendrogram.js + +Copyright (c) 2013, Ken-ichi Ueda +2015 UCSC Chris Eisenhart + +DOCUEMENTATION +d3.dendrogram.makeCartesianDendrogram(selector, nodes, options) + Generates a cartesian (flat) dendrogram from a set of .json data. The branch lengths are + scaled acording to the 'length' field in the .json file, buttons are automatically generated + for every .json field, in the leaf nodes only, not in this list, [x, y, name, kids, length, + colorGroup, parent, depth, rootDist]. + Arguments: + selector: selector of an element that will contain the SVG (a div id). + nodes: A Javascript object of nodes, most commonly a .json file parsed into javascript. + options: + width + Width of the vis, will attempt to set a default based on the width of + the container. + height + Height of the vis, will attempt to set a default based on the height + of the container. + vis + Pre-constructed d3 vis. + tree + Pre-constructed d3 tree layout. + children + Function for retrieving an array of children given a node. Default is + to assume each node has an attribute called "children" that contains the + nodes children. + diagonal + Function that creates the d attribute for an svg:path. Defaults to a + right-angle diagonal. + skipTicks + Skip the background lines for context. + skipBranchLengthScaling + Make a dendrogram instead of a dendrogram. + +d3.dendrogram.makeRadialDendrogram(selector, nodes, options) + Generates a radial (circular) dendrogram from a set of .json data. The dendrogram displays + internal node size based on the number of children nodes and intenal node color based of the + .json field "whiteToBlack" which is expected to be a rgb color. Buttons are automatically + generated to control internal node color, these buttons correspond to the .json fields + [whiteToBlack, whiteToBlackSqrt, whiteToBlackQuad]. Additionally buttons are automatically + generated for every .json field, in the leaf nodes only, not in this list, [x, y, name, kids, + length, colorGroup, parent, depth, rootDist]. + Arguments: + selector: selector of an element that will contain the SVG (a div id). + nodes: A Javascript object of nodes, most commonly a .json file parsed into javascript. + options: + width + Width of the vis, will attempt to set a default based on the width of + the container. + height + Height of the vis, will attempt to set a default based on the height + of the container. + vis + Pre-constructed d3 vis. + tree + Pre-constructed d3 tree layout. + children + Function for retrieving an array of children given a node. Default is + to assume each node has an attribute called "children" that contains the + nodes children. + diagonal + Function that creates the d attribute for an svg:path. Defaults to a + right-angle diagonal. + skipTicks + Skip the background lines for context. + skipBranchLengthScaling + Make a dendrogram instead of a dendrogram. + +d3.dendrogram.rightAngleDiagonal() + Computes the angles for the graph, the user can override this if needed. +d3.dendrogram.leafColors(val, layer, shift, selector) + This function is used by the buttons to color the leaf nodes. It should not be + modified. +d3.dendrogram.nodeColors(val, selector) + This function is used by the buttons in the radial dendrogram to color the + internal nodes. It should not be modified. + +Internal functions: + updateLegend(val, layer, vis, shift) + Update the legend to correspond to the current set of meta data. + scaleBranchLengths(nodes, w) + Scale the branch lengths to correspond to the 'length' field in the json data. + addLeafButton(title, count, layer, shift, dropdownSelector, graph) + Adds a leaf button to the dropdown skeleton. + addNodeButton(dropdownSelector, val, graph, title) + Adds a node button to the dropdown skeleton. + makeDropdownSkeleton(treeType) + Builds a bootstrap dropdown menu skeleton. + +*/ + +if (!d3) { throw "d3 wasn't included!";} +(function(){ + d3.dendrogram = {}; + + var colors=d3.scale.category20(); + var legendRectSize=20; + var legendSpacing=4; + var currentLegend=-1; + var radius=0; + + d3.dendrogram.rightAngleDiagonal=function (){ + var projection = function(d) { return [d.y, d.x]; }; + + var path = function(pathData) { + return "M" + pathData[0] + ' ' + pathData[1] + " " + pathData[2]; + }; + + function diagonal(diagonalPath, i) { + var source = diagonalPath.source, + target = diagonalPath.target, + midpointX = (source.x + target.x) / 2, + midpointY = (source.y + target.y) / 2, + pathData = [source, {x: target.x, y: source.y}, target]; + pathData = pathData.map(projection); + return path(pathData); + } + + diagonal.projection = function(x) { + if (!arguments.length) return projection; + projection = x; + return diagonal; + }; + + diagonal.path = function(x) { + if (!arguments.length) return path; + path = x; + return diagonal; + }; + return diagonal; + }; + + d3.dendrogram.leafColors = function(val, layer, shift, selector){ + colors = d3.scale.category20(); + var vis = d3.select(selector).select("svg"); + var first = 1; + var node = vis.selectAll("g."+ layer + "Leaf"); + + node.selectAll("circle") + .style("fill", function(d){ + // This block links the extra leaf json fields to a specific value (count). This value + // is provided when the buttons are written in the main function. + if (d.name != " "){ + var count = 0; + for (var key in d) { + if (d.hasOwnProperty(key)){ + if (key != "x" && key!="y" && key!= "name" && key!="kids" && key!="length" && key!="colorGroup" && key!="parent" && key!="depth" && key!="rootDist"){ + if (count == val){ + count += 1; + return colors(d[key]); + } + } + count += 1; + } + } + } + else{return 'none';} + }) + .attr("transform","translate(" + shift + "," + 0 + ")"); + + var legend=vis.selectAll('g.' + layer + 'Legend').remove(); + updateLegend(colors.domain(), layer, vis, shift); + colors=d3.scale.category20(); + }; + + d3.dendrogram.nodeColors = function(val, selector){ + var vis = d3.select(selector).selectAll("svg"); + var node=vis.selectAll("g.internalNode").selectAll("circle") + .style("fill",function(d){ + if (d.name === " "){ + if (val==1) return d3.rgb(d.colorGroup); + if (val==2) return d3.rgb(d.whiteToBlack); + if (val==3) return d3.rgb(d.whiteToBlackSqrt); + if (val==4) return d3.rgb(d.whiteToBlackQuad); + } + }); + }; + + updateLegend = function(val, layer, vis, shift){ + var legend=vis.selectAll('g.'+layer+'Legend').data(val); + + legend.enter() + .append('g') + .attr('class',layer+ 'Legend') + .attr('transform', function (d, i){ + var height=legendRectSize + legendSpacing; + var offset= height * colors.domain().length / 2; + var horz, vert; + if (layer=="inner"){ + horz=(-2 * legendRectSize) + radius; + vert=-(i * height - offset) + (radius*(1/2)); + } + if (layer=="middle"){ + horz=(-2 * legendRectSize) + radius + 75; + vert=-(i * height - offset) + (radius*(1/2)); + } + if (layer=="outer"){ + horz=(-2 * legendRectSize) + radius + 150; + vert=-(i * height - offset)+ (radius*(1/2)); + } + return 'translate(' + horz + ',' + vert + ')'; + }); + + legend.append('rect') + .attr('width', legendRectSize) + .attr('height', legendRectSize) + .style('fill', colors) + .style('stroke', colors); + + legend.append('text') + .attr('x', legendRectSize + legendSpacing) + .attr('y', legendRectSize - legendSpacing) + .text(function (d){return d;}); + }; + + scaleBranchLengths = function (nodes, w) { + // Visit all nodes and adjust y pos width distance metric + var visitPreOrder = function(root, callback) { + callback(root); + if (root.children) { + for (var i = parseInt(root.children.length) - 1; i >= 0; i--){ + visitPreOrder(root.children[i], callback); + } + } + }; + + visitPreOrder(nodes[0], function(node) { + node.rootDist = (node.parent ? node.parent.rootDist : 0) + (parseInt(node.length) || 0); + }); + + var rootDists = nodes.map(function(n) { return n.rootDist; }); + var yscale = d3.scale.linear() + .domain([0, d3.max(rootDists)]) + .range([0, w]); + visitPreOrder(nodes[0], function(node) { + node.y = yscale(node.rootDist); + }); + return yscale; + }; + + addLeafButton = function (title, count, layer, shift, dropdownSelector, graph) { + d3.select(dropdownSelector).append("li").html(""); + }; + + addNodeButton = function (dropdownSelector, val, graph, title) { + d3.select(dropdownSelector).append("li").html(""); + }; + + makeDropdownSkeleton = function (treeType){ + var dropdown = d3.select("#dropdown").append("ul").attr("style","list-style-type:none;display:inline-flex"); + + if (treeType == "Dendro"){ + var interiorNodeDropdown = dropdown.append("li").attr("class","dropdown"); + interiorNodeDropdown.append("button").attr("class","btn btn-default dropdown-toggle") + .attr("type", "button").attr("data-toggle","dropdown").html("Inner nodes").append("span").attr("class","caret"); + interiorNodeDropdown.append("ul").attr("class","dropdown-menu").html("
"); + } + + var innerDropdown = dropdown.append("li").attr("class","dropdown"); + innerDropdown.append("button").attr("class","btn btn-default dropdown-toggle") + .attr("type", "button").attr("data-toggle","dropdown").html("Inner nodes").append("span").attr("class","caret"); + innerDropdown.append("ul").attr("class","dropdown-menu").html("
"); + + var middleDropdown = dropdown.append("li").attr("class","dropdown"); + middleDropdown.append("button").attr("class","btn btn-default dropdown-toggle") + .attr("type", "button").attr("data-toggle","dropdown").html("Middle nodes").append("span").attr("class","caret"); + middleDropdown.append("ul").attr("class","dropdown-menu").html("
"); + + var outerDropdown = dropdown.append("li").attr("class","dropdown"); + outerDropdown.append("button").attr("class","btn btn-default dropdown-toggle") + .attr("type", "button").attr("data-toggle","dropdown").html("Outer nodes").append("span").attr("class","caret"); + outerDropdown.append("ul").attr("class","dropdown-menu").html("
"); + }; + + + d3.dendrogram.makeCartesianDendrogram =function (selector, data, options) { + options = options || {}; + var w = options.width || d3.select(selector).style('width') || d3.select(selector).attr('width'), + h = options.height || d3.select(selector).style('height') || d3.select(selector).attr('height'); + + var tree = options.tree || d3.layout.cluster() + .size([h, w]) + .sort(function(node) { return node.children ? parseInt(node.children.length) : -1; }) + .children(options.children || function(node) { + return node.children; + }); + + var diagonal = options.diagonal || d3.dendrogram.rightAngleDiagonal(); + + var vis = options.vis || d3.select(selector).append("svg") + .attr("width", w + 300) + .attr("height", h + 30) + .append("g") + .attr("transform", "translate(20, 20)"); + + var nodes = tree(data); + + var legendRectSize = 10; + var legendSpacing = 10; + + var yscale; + if (options.skipBranchLengthScaling) { + yscale = d3.scale.linear() + .domain([0, w]) + .range([0, w]); + } + else{ + yscale = scaleBranchLengths(nodes, w); + } + + if (!options.skipTicks) { + vis.selectAll('line') + .data(yscale.ticks(10)) + .enter().append('svg:line') + .attr('y1', 0) + .attr('y2', h) + .attr('x1', yscale) + .attr('x2', yscale) + .attr("stroke", "#ddd"); + + vis.selectAll("text.rule") + .data(yscale.ticks(10)) + .enter().append("svg:text") + .attr("class", "rule") + .attr("x", yscale) + .attr("y", 0) + .attr("dy", -3) + .attr("text-anchor", "middle") + .attr('font-size', '8px') + .attr('fill', '#ccc') + .text(function(d) { return Math.round(d*100) / 100; }); + } + + var link = vis.selectAll("path.link") + .data(tree.links(nodes)) + .enter().append("svg:path") + .attr("class", "link") + .attr("d", diagonal) + .attr("fill", "none") + .attr("stroke", "#aaa") + .attr("stroke-width", "4px"); + + var first = 1; + + makeDropdownSkeleton("Phylo"); + + var innerNodes = vis.selectAll("g.innerNode") + .data(nodes) + .enter().append("g") + .attr("class", function(n) { + if (n.children) { + if (n.depth === 0) { + return "root node"; + } else { + return "inner node"; + } + } + else{ + if (first){ + var count = 0; + for (var key in n) { + if (n.hasOwnProperty(key)) + { + if (key != "x" && key!="y" && key!= "name" && key!="kids" && key!="length" && key!="colorGroup" && key!="parent" && key!="depth" && key!="rootDist") + { + addLeafButton(key, count, 'inner', 0, '#innerPhyloLeaves', '#phylogram') ; + addLeafButton(key, count, 'middle', 0, '#middlePhyloLeaves', '#phylogram'); + addLeafButton(key, count, 'outer', 0, '#outerPhyloLeaves', '#phylogram'); + } + } + count += 1; + } + first = 0; + } + return "innerLeaf"; + } + }) + .attr("transform", function(d) { return "translate(" + (d.y) + "," + (d.x) + ")"; }); + + var innerLeaves = vis.selectAll("g.innerLeaf") + .append("circle") + .attr("r", '3') + .style("fil", 'none'); + + var middleLeaves = vis.selectAll("g.middleLeaf") + .data(nodes) + .enter().append("svg:g") + .attr("class", function(n) { + if (n.children) { + return "nill"; + } + else{ + return "middleLeaf"; + } + }); + + vis.selectAll("g.middleLeaf") + .attr("transform", function(d) { return "translate(" + (d.y +10 ) + "," + d.x + ")"; }) + .append("circle") + .attr("r", '3') + .style("fil", 'none'); + + + var outerLeaves = vis.selectAll("g.outerLeaf") + .data(nodes) + .enter().append("svg:g") + .attr("class", function(n) { + if (n.children) { + return "nill"; + } + else{ + return "outerLeaf"; + } + }) + .attr("transform", function(d) { return "translate(" + (d.y+20 ) + "," + d.x + ")"; }); + + vis.selectAll("g.outerLeaf") + .append("svg:circle") + .attr("r", '3') + .attr('stroke', 'black') + .style("stroke-width", ".25px") + .attr('stroke-width', '.25px') + .attr('fill', 'greenYellow') + .attr('fill', 'black') + .attr('stroke-width', '2px'); + + vis.selectAll('g.root.node') + .append('svg:circle') + .attr("r", 10) + .style("fill", function(d){ + if (d.name !==" "){return "d.whiteToBlack";} + }) + .attr('stroke', '#369') + .attr('stroke-width', '2px'); + + if (!options.skipLabels){ + vis.selectAll('g.inner.node') + .append("svg:text") + .attr("dx", -6) + .attr("dy", -6) + .attr("text-anchor", 'end') + .attr('font-size', '8px') + .attr('fill', '#ccc') + .text(function(d) { return d.length; }); + + vis.selectAll('g.outerLeaf') + .append("svg:text") + .attr("dx", 8) + .attr("dy", 3) + .attr("text-anchor", "start") + .attr('font-family', 'Helvetica Neue, Helvetica, sans-serif') + .attr('font-size', '6px') + .attr('fill', 'black') + .text(function(d) { return d.name + ' ('+d.length+')'; }); + } + + return {tree: tree, vis: vis}; + }; + + + d3.dendrogram.makeRadialDendrogram = function(selector, data, options) { + options = options || {}; + radius = (options.radius || 800) / 2; // Global for the legend placement. See function updateLegend. + var cluster = d3.layout.cluster().size([360, radius - 40]); + var tree = options.tree || d3.layout.cluster() + .size([360, radius - 40]); + + var diagonal=d3.svg.diagonal.radial() + .projection(function(d){return [d.y, d.x / 180 * Math.PI];}); + + var vis=d3.select(selector) + .append("svg") + .attr("width", 200+(radius * 2)) + .attr("height", radius * 2) + .append("g") + .attr("transform","translate(" + (radius + 150) + "," + radius + ")"); + + var legendRectSize=20; + var legendSpacing=4; + var currentLegend=-1; + var nodes=cluster.nodes(data); + + var link=vis.selectAll("path.link") + .data(cluster.links(nodes)).enter() + .append("path") + .attr("class", "link") + .attr("fill", "none") + .attr("stroke", "#aaa") + .attr("stroke-width", "4px") + .attr("d", diagonal); + + makeDropdownSkeleton("Dendro");//Looks for the div 'dropdown' and generates the outer list and buttons + // For the radial display the interior node color options are constant (calculated on the backend in the C code) + addNodeButton("#interiorDendroNodes" ,2, selector, "Tpm distance"); + addNodeButton("#interiorDendroNodes" ,3, selector, "Square root tpm distance"); + addNodeButton("#interiorDendroNodes" ,4, selector, "Quad root tpm distance"); + + var first = 1; + + var innerNodes = vis.selectAll("g") + .data(nodes).enter() + .append("g") + .attr("class", function(n) { + if (n.children) { + return "internalNode"; + } + else{ + if (first){ + var count = 0; + for (var key in n) { + if (n.hasOwnProperty(key)) { + if (key != "x" && key!="y" && key!= "name" && key!="kids" && key!="length" && key!="colorGroup" && key!="parent" && key!="depth" && key!="rootDist"){ + addLeafButton(key, count, 'inner', 0, '#innerDendroLeaves', '#dendrogram'); + addLeafButton(key, count, 'middle', 15, '#middleDendroLeaves', '#dendrogram'); + addLeafButton(key, count, 'outer', 30, '#outerDendroLeaves', '#dendrogram') ; + } + } + count += 1; + } + first = 0; + } + return "g.innerLeaf"; + } + }) + .attr("transform", function(d){return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";}); + + var interiorNodes = vis.selectAll("g.internalNode") + .append("circle") + .attr("r", function(d){ + if (d.name==" "){return 1 + Math.sqrt(d.kids);} + else{return 0;} + }) + .style("fill", function(d){ + if (d.name==" "){return d.whiteToBlack;} + else{return "none";}}) + .style("stroke", "steelblue") + .style("stroke-width", "1.25px"); + + var innerLeaves = vis.selectAll("g.innerLeaf") + .data(nodes).enter() + .append("g") + .attr("class", "innerLeaf") + .attr("transform",function(d){return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";}); + + innerLeaves.append("circle") + .attr("r", function(d){ + if (d.name !==" "){return 5;}}) + .style("fill", function(d){ + if (d.name !==" "){return "d.whiteToBlack";} + }); + + var middleLeaves = vis.selectAll("g.middleLeaf") + .data(nodes).enter() + .append("g") + .attr("class", "middleLeaf") + .attr("transform",function(d){return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";}); + + middleLeaves.append("circle") + .attr("r", 5) + .style("fill","none") + .attr("transform","translate(" + 15 + "," + 0 + ")"); + + var outerLeaves = vis.selectAll("g.outerLeaf") + .data(nodes).enter() + .append("g") + .attr("class", "outerLeaf") + .attr("transform",function(d){return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";}) + .append("circle") + .attr("r", 5) + .style("fill","none"); + + return {tree: tree, vis: vis}; + }; +})();