123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- /*
- (agr@ncgr.org : this is a significantly modified version of
- d3.phylogram.js.... retaining attribution/copyright per below)
- d3.phylogram.js http://bl.ocks.org/kueda/1036776
- Wrapper around a d3-based phylogram (tree where branch lengths are scaled)
- Also includes a radial dendrogram visualization (branch lengths not scaled)
- along with some helper methods for building angled-branch trees.
- Copyright (c) 2013, Ken-ichi Ueda
- All rights reserved.
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer. Redistributions in binary
- form must reproduce the above copyright notice, this list of conditions and
- the following disclaimer in the documentation and/or other materials
- provided with the distribution.
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- POSSIBILITY OF SUCH DAMAGE.
- DOCUEMENTATION
- d3.phylogram.build(selector, nodes, options)
- Creates a phylogram.
- Arguments:
- selector: selector of an element that will contain the SVG
- nodes: JS object of nodes
- 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.
- fill
- Function for generating fill color for leaf nodes.
- 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"
- diagonal
- Function that creates the d attribute for an svg:path. Defaults to a
- right-angle diagonal.
- skipTicks
- Skip the tick rule.
- skipBranchLengthScaling
- Make a dendrogram instead of a phylogram.
-
- d3.phylogram.buildRadial(selector, nodes, options)
- Creates a radial dendrogram.
- Options: same as build, but without diagonal, skipTicks, and
- skipBranchLengthScaling
-
- d3.phylogram.rightAngleDiagonal()
- Similar to d3.diagonal except it create an orthogonal crook instead of a
- smooth Bezier curve.
-
- d3.phylogram.radialRightAngleDiagonal()
- d3.phylogram.rightAngleDiagonal for radial layouts.
- */
- if (!d3) { throw "d3 wasn't included!"};
- (function() {
- d3.phylogram = {}
- d3.phylogram.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.phylogram.radialRightAngleDiagonal = function() {
- return d3.phylogram.rightAngleDiagonal()
- .path(function(pathData) {
- var src = pathData[0],
- mid = pathData[1],
- dst = pathData[2],
- radius = Math.sqrt(src[0]*src[0] + src[1]*src[1]),
- srcAngle = d3.phylogram.coordinateToAngle(src, radius),
- midAngle = d3.phylogram.coordinateToAngle(mid, radius),
- clockwise = Math.abs(midAngle - srcAngle) > Math.PI ? midAngle <= srcAngle : midAngle > srcAngle,
- rotation = 0,
- largeArc = 0,
- sweep = clockwise ? 0 : 1;
- return 'M' + src + ' ' +
- "A" + [radius,radius] + ' ' + rotation + ' ' + largeArc+','+sweep + ' ' + mid +
- 'L' + dst;
- })
- .projection(function(d) {
- var r = d.y, a = (d.x - 90) / 180 * Math.PI;
- return [r * Math.cos(a), r * Math.sin(a)];
- })
- }
-
- // Convert XY and radius to angle of a circle centered at 0,0
- d3.phylogram.coordinateToAngle = function(coord, radius) {
- var wholeAngle = 2 * Math.PI,
- quarterAngle = wholeAngle / 4
-
- var coordQuad = coord[0] >= 0 ? (coord[1] >= 0 ? 1 : 2) : (coord[1] >= 0 ? 4 : 3),
- coordBaseAngle = Math.abs(Math.asin(coord[1] / radius))
-
- // Since this is just based on the angle of the right triangle formed
- // by the coordinate and the origin, each quad will have different
- // offsets
- switch (coordQuad) {
- case 1:
- coordAngle = quarterAngle - coordBaseAngle
- break
- case 2:
- coordAngle = quarterAngle + coordBaseAngle
- break
- case 3:
- coordAngle = 2*quarterAngle + quarterAngle - coordBaseAngle
- break
- case 4:
- coordAngle = 3*quarterAngle + coordBaseAngle
- }
- return coordAngle
- }
- function scaleBranchLengths(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 = 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) + (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
- }
-
- d3.phylogram.build = function(selector, nodes, 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'),
- w = parseInt(w),
- h = parseInt(h);
- var fill = options.fill || function(d) {
- return 'cyan';
- };
- var size = options.size || function(d) {
- return 6;
- }
- var nodeMouseOver = options.nodeMouseOver || function(d) {};
- var nodeMouseOut = options.nodeMouseOut || function(d) {};
- var nodeMouseDown = options.nodeMouseDown || function(d) {};
-
- var tree = options.tree || d3.layout.cluster()
- .size([h, w])
- .sort(function(node) { return node.children ? node.children.length : -1; })
- .children(options.children || function(node) {
- return node.children;
- });
- var diagonal = options.diagonal || d3.phylogram.rightAngleDiagonal();
- var vis = options.vis || d3.select(selector).append("svg:svg")
- .attr("width", w + 300)
- .attr("height", h + 30)
- .append("svg:g")
- .attr("transform", "translate(20, 20)");
- var nodes = tree(nodes);
-
- if (options.skipBranchLengthScaling) {
- var yscale = d3.scale.linear()
- .domain([0, w])
- .range([0, w]);
- }
- else {
- var 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', '9px')
- .attr('fill', 'grey')
- .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 node = vis.selectAll("g.node")
- .data(nodes)
- .enter().append("svg:g")
- .attr("class", function(n) {
- if (n.children) {
- if (n.depth == 0) {
- return "root node"
- }
- else {
- return "inner node"
- }
- }
- else {
- return "leaf node"
- }
- })
- .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
- // style the root node
- vis.selectAll('g.root.node')
- .append('svg:circle')
- .on('click', nodeMouseDown)
- .on('mouseover', nodeMouseOver)
- .on('mouseout', nodeMouseOut)
- .attr("r", size)
- .attr('fill', 'dimgrey')
- .attr('stroke', 'black')
- .attr('stroke-width', '2px');
- // style the leaf nodes and add js event handlers
- vis.selectAll('g.leaf.node')
- .on('click', nodeMouseDown)
- .on('mouseover', nodeMouseOver)
- .on('mouseout', nodeMouseOut)
- .append("svg:circle")
- .attr("r", size)
- .attr('stroke', 'dimgrey')
- .attr('fill', fill)
- .attr('stroke-width', '2px');
- vis.selectAll('g.inner.node')
- .on('click', nodeMouseDown)
- .on('mouseover', nodeMouseOver)
- .on('mouseout', nodeMouseOut)
- .append("svg:circle")
- .attr("r", size)
- .attr('stroke', 'dimgrey')
- .attr('stroke-width', '2px')
- .attr('fill', 'white');
-
- if (!options.skipLabels) {
- vis.selectAll('g.inner.node')
- .append("svg:text")
- .attr("dx", -6)
- .attr("dy", -6)
- .attr("text-anchor", 'end')
- .attr('font-size', '9px')
- .attr('fill', 'black')
- //.text(function(d) { return d.length.toFixed(4); }); // hide length
- vis.selectAll('g.leaf.node').append("svg:text")
- .attr("dx", 8)
- .attr("dy", 3)
- .attr("text-anchor", "start")
- .attr('font-family', 'Helvetica Neue, Helvetica, sans-serif')
- .attr('font-size', '10px')
- .attr('fill', 'black')
- .text(function(d) {
- // return d.name + ' (' + d.length.toFixed(4) + ')'; // hide length
- return d.name;
- });
- }
- return {tree: tree, vis: vis}
- }
-
- d3.phylogram.buildRadial = function(selector, nodes, options) {
- options = options || {};
-
- var fill = options.fill || function(d) {
- return 'cyan';
- };
- var size = options.size || function(d) {
- return 6;
- }
- var nodeMouseOver = options.nodeMouseOver || function(d) {};
- var nodeMouseOut = options.nodeMouseOut || function(d) {};
- var nodeMouseDown = options.nodeMouseDown || function(d) {};
-
- var w = options.width || d3.select(selector).style('width') || d3.select(selector).attr('width'),
- r = w / 2,
- labelWidth = options.skipLabels ? 10 : options.labelWidth || 120;
-
- var vis = d3.select(selector).append("svg:svg")
- .attr("width", r * 2)
- .attr("height", r * 2)
- .append("svg:g")
- .attr("transform", "translate(" + r + "," + r + ")");
-
- var tree = d3.layout.tree()
- .size([360, r - labelWidth])
- .sort(function(node) { return node.children ? node.children.length : -1; })
- .children(options.children || function(node) {
- return node.children;
- })
- .separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
- var phylogram = d3.phylogram.build(selector, nodes, {
- vis: vis,
- tree: tree,
- fill : fill,
- size: size,
- nodeMouseOver : nodeMouseOver,
- nodeMouseOut : nodeMouseOut,
- nodeMouseDown : nodeMouseDown,
- skipBranchLengthScaling: true,
- skipTicks: true,
- skipLabels: options.skipLabels,
- diagonal: d3.phylogram.radialRightAngleDiagonal()
- })
- vis.selectAll('g.node')
- .attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
-
- if (!options.skipLabels) {
- vis.selectAll('g.leaf.node text')
- .attr("dx", function(d) { return d.x < 180 ? 8 : -8; })
- .attr("dy", ".31em")
- .attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
- .attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; })
- .attr('font-family', 'Helvetica Neue, Helvetica, sans-serif')
- .attr('font-size', '10px')
- .attr('fill', 'black')
- .text(function(d) {
- return d.name;
- });
- vis.selectAll('g.inner.node text')
- .attr("dx", function(d) { return d.x < 180 ? -6 : 6; })
- .attr("text-anchor", function(d) { return d.x < 180 ? "end" : "start"; })
- .attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; });
- }
-
- return {tree: tree, vis: vis}
- }
- }());
|