d3.phylogram.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. /*
  2. (agr@ncgr.org : this is a significantly modified version of
  3. d3.phylogram.js.... retaining attribution/copyright per below)
  4. d3.phylogram.js http://bl.ocks.org/kueda/1036776
  5. Wrapper around a d3-based phylogram (tree where branch lengths are scaled)
  6. Also includes a radial dendrogram visualization (branch lengths not scaled)
  7. along with some helper methods for building angled-branch trees.
  8. Copyright (c) 2013, Ken-ichi Ueda
  9. All rights reserved.
  10. Redistribution and use in source and binary forms, with or without
  11. modification, are permitted provided that the following conditions are met:
  12. Redistributions of source code must retain the above copyright notice, this
  13. list of conditions and the following disclaimer. Redistributions in binary
  14. form must reproduce the above copyright notice, this list of conditions and
  15. the following disclaimer in the documentation and/or other materials
  16. provided with the distribution.
  17. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  18. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  19. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  20. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  21. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  22. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  23. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  24. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  25. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  26. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  27. POSSIBILITY OF SUCH DAMAGE.
  28. DOCUEMENTATION
  29. d3.phylogram.build(selector, nodes, options)
  30. Creates a phylogram.
  31. Arguments:
  32. selector: selector of an element that will contain the SVG
  33. nodes: JS object of nodes
  34. Options:
  35. width
  36. Width of the vis, will attempt to set a default based on the width of
  37. the container.
  38. height
  39. Height of the vis, will attempt to set a default based on the height
  40. of the container.
  41. fill
  42. Function for generating fill color for leaf nodes.
  43. vis
  44. Pre-constructed d3 vis.
  45. tree
  46. Pre-constructed d3 tree layout.
  47. children
  48. Function for retrieving an array of children given a node. Default is
  49. to assume each node has an attribute called "children"
  50. diagonal
  51. Function that creates the d attribute for an svg:path. Defaults to a
  52. right-angle diagonal.
  53. skipTicks
  54. Skip the tick rule.
  55. skipBranchLengthScaling
  56. Make a dendrogram instead of a phylogram.
  57. d3.phylogram.buildRadial(selector, nodes, options)
  58. Creates a radial dendrogram.
  59. Options: same as build, but without diagonal, skipTicks, and
  60. skipBranchLengthScaling
  61. d3.phylogram.rightAngleDiagonal()
  62. Similar to d3.diagonal except it create an orthogonal crook instead of a
  63. smooth Bezier curve.
  64. d3.phylogram.radialRightAngleDiagonal()
  65. d3.phylogram.rightAngleDiagonal for radial layouts.
  66. */
  67. if (!d3) { throw "d3 wasn't included!"};
  68. (function() {
  69. d3.phylogram = {}
  70. d3.phylogram.rightAngleDiagonal = function() {
  71. var projection = function(d) { return [d.y, d.x]; }
  72. var path = function(pathData) {
  73. return "M" + pathData[0] + ' ' + pathData[1] + " " + pathData[2];
  74. }
  75. function diagonal(diagonalPath, i) {
  76. var source = diagonalPath.source,
  77. target = diagonalPath.target,
  78. midpointX = (source.x + target.x) / 2,
  79. midpointY = (source.y + target.y) / 2,
  80. pathData = [source, {x: target.x, y: source.y}, target];
  81. pathData = pathData.map(projection);
  82. return path(pathData)
  83. }
  84. diagonal.projection = function(x) {
  85. if (!arguments.length) return projection;
  86. projection = x;
  87. return diagonal;
  88. };
  89. diagonal.path = function(x) {
  90. if (!arguments.length) return path;
  91. path = x;
  92. return diagonal;
  93. };
  94. return diagonal;
  95. }
  96. d3.phylogram.radialRightAngleDiagonal = function() {
  97. return d3.phylogram.rightAngleDiagonal()
  98. .path(function(pathData) {
  99. var src = pathData[0],
  100. mid = pathData[1],
  101. dst = pathData[2],
  102. radius = Math.sqrt(src[0]*src[0] + src[1]*src[1]),
  103. srcAngle = d3.phylogram.coordinateToAngle(src, radius),
  104. midAngle = d3.phylogram.coordinateToAngle(mid, radius),
  105. clockwise = Math.abs(midAngle - srcAngle) > Math.PI ? midAngle <= srcAngle : midAngle > srcAngle,
  106. rotation = 0,
  107. largeArc = 0,
  108. sweep = clockwise ? 0 : 1;
  109. return 'M' + src + ' ' +
  110. "A" + [radius,radius] + ' ' + rotation + ' ' + largeArc+','+sweep + ' ' + mid +
  111. 'L' + dst;
  112. })
  113. .projection(function(d) {
  114. var r = d.y, a = (d.x - 90) / 180 * Math.PI;
  115. return [r * Math.cos(a), r * Math.sin(a)];
  116. })
  117. }
  118. // Convert XY and radius to angle of a circle centered at 0,0
  119. d3.phylogram.coordinateToAngle = function(coord, radius) {
  120. var wholeAngle = 2 * Math.PI,
  121. quarterAngle = wholeAngle / 4
  122. var coordQuad = coord[0] >= 0 ? (coord[1] >= 0 ? 1 : 2) : (coord[1] >= 0 ? 4 : 3),
  123. coordBaseAngle = Math.abs(Math.asin(coord[1] / radius))
  124. // Since this is just based on the angle of the right triangle formed
  125. // by the coordinate and the origin, each quad will have different
  126. // offsets
  127. switch (coordQuad) {
  128. case 1:
  129. coordAngle = quarterAngle - coordBaseAngle
  130. break
  131. case 2:
  132. coordAngle = quarterAngle + coordBaseAngle
  133. break
  134. case 3:
  135. coordAngle = 2*quarterAngle + quarterAngle - coordBaseAngle
  136. break
  137. case 4:
  138. coordAngle = 3*quarterAngle + coordBaseAngle
  139. }
  140. return coordAngle
  141. }
  142. function scaleBranchLengths(nodes, w) {
  143. // Visit all nodes and adjust y pos width distance metric
  144. var visitPreOrder = function(root, callback) {
  145. callback(root)
  146. if (root.children) {
  147. for (var i = root.children.length - 1; i >= 0; i--){
  148. visitPreOrder(root.children[i], callback);
  149. };
  150. }
  151. }
  152. visitPreOrder(nodes[0], function(node) {
  153. node.rootDist = (node.parent ? node.parent.rootDist : 0) + (node.length || 0)
  154. })
  155. var rootDists = nodes.map(function(n) { return n.rootDist; });
  156. var yscale = d3.scale.linear()
  157. .domain([0, d3.max(rootDists)])
  158. .range([0, w]);
  159. visitPreOrder(nodes[0], function(node) {
  160. node.y = yscale(node.rootDist)
  161. })
  162. return yscale
  163. }
  164. d3.phylogram.build = function(selector, nodes, options) {
  165. options = options || {}
  166. var w = options.width || d3.select(selector).style('width') || d3.select(selector).attr('width'),
  167. h = options.height || d3.select(selector).style('height') || d3.select(selector).attr('height'),
  168. w = parseInt(w),
  169. h = parseInt(h);
  170. var fill = options.fill || function(d) {
  171. return 'cyan';
  172. };
  173. var size = options.size || function(d) {
  174. return 6;
  175. }
  176. var nodeMouseOver = options.nodeMouseOver || function(d) {};
  177. var nodeMouseOut = options.nodeMouseOut || function(d) {};
  178. var nodeMouseDown = options.nodeMouseDown || function(d) {};
  179. var tree = options.tree || d3.layout.cluster()
  180. .size([h, w])
  181. .sort(function(node) { return node.children ? node.children.length : -1; })
  182. .children(options.children || function(node) {
  183. return node.children;
  184. });
  185. var diagonal = options.diagonal || d3.phylogram.rightAngleDiagonal();
  186. var vis = options.vis || d3.select(selector).append("svg:svg")
  187. .attr("width", w + 300)
  188. .attr("height", h + 30)
  189. .append("svg:g")
  190. .attr("transform", "translate(20, 20)");
  191. var nodes = tree(nodes);
  192. if (options.skipBranchLengthScaling) {
  193. var yscale = d3.scale.linear()
  194. .domain([0, w])
  195. .range([0, w]);
  196. }
  197. else {
  198. var yscale = scaleBranchLengths(nodes, w)
  199. }
  200. if (!options.skipTicks) {
  201. vis.selectAll('line')
  202. .data(yscale.ticks(10))
  203. .enter().append('svg:line')
  204. .attr('y1', 0)
  205. .attr('y2', h)
  206. .attr('x1', yscale)
  207. .attr('x2', yscale)
  208. .attr("stroke", "#ddd");
  209. vis.selectAll("text.rule")
  210. .data(yscale.ticks(10))
  211. .enter().append("svg:text")
  212. .attr("class", "rule")
  213. .attr("x", yscale)
  214. .attr("y", 0)
  215. .attr("dy", -3)
  216. .attr("text-anchor", "middle")
  217. .attr('font-size', '9px')
  218. .attr('fill', 'grey')
  219. .text(function(d) { return Math.round(d*100) / 100; });
  220. }
  221. var link = vis.selectAll("path.link")
  222. .data(tree.links(nodes))
  223. .enter().append("svg:path")
  224. .attr("class", "link")
  225. .attr("d", diagonal)
  226. .attr("fill", "none")
  227. .attr("stroke", "#aaa")
  228. .attr("stroke-width", "4px");
  229. var node = vis.selectAll("g.node")
  230. .data(nodes)
  231. .enter().append("svg:g")
  232. .attr("class", function(n) {
  233. if (n.children) {
  234. if (n.depth == 0) {
  235. return "root node"
  236. }
  237. else {
  238. return "inner node"
  239. }
  240. }
  241. else {
  242. return "leaf node"
  243. }
  244. })
  245. .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
  246. // style the root node
  247. vis.selectAll('g.root.node')
  248. .append('svg:circle')
  249. .on('click', nodeMouseDown)
  250. .on('mouseover', nodeMouseOver)
  251. .on('mouseout', nodeMouseOut)
  252. .attr("r", size)
  253. .attr('fill', 'dimgrey')
  254. .attr('stroke', 'black')
  255. .attr('stroke-width', '2px');
  256. // style the leaf nodes and add js event handlers
  257. vis.selectAll('g.leaf.node')
  258. .on('click', nodeMouseDown)
  259. .on('mouseover', nodeMouseOver)
  260. .on('mouseout', nodeMouseOut)
  261. .append("svg:circle")
  262. .attr("r", size)
  263. .attr('stroke', 'dimgrey')
  264. .attr('fill', fill)
  265. .attr('stroke-width', '2px');
  266. vis.selectAll('g.inner.node')
  267. .on('click', nodeMouseDown)
  268. .on('mouseover', nodeMouseOver)
  269. .on('mouseout', nodeMouseOut)
  270. .append("svg:circle")
  271. .attr("r", size)
  272. .attr('stroke', 'dimgrey')
  273. .attr('stroke-width', '2px')
  274. .attr('fill', 'white');
  275. if (!options.skipLabels) {
  276. vis.selectAll('g.inner.node')
  277. .append("svg:text")
  278. .attr("dx", -6)
  279. .attr("dy", -6)
  280. .attr("text-anchor", 'end')
  281. .attr('font-size', '9px')
  282. .attr('fill', 'black')
  283. //.text(function(d) { return d.length.toFixed(4); }); // hide length
  284. vis.selectAll('g.leaf.node').append("svg:text")
  285. .attr("dx", 8)
  286. .attr("dy", 3)
  287. .attr("text-anchor", "start")
  288. .attr('font-family', 'Helvetica Neue, Helvetica, sans-serif')
  289. .attr('font-size', '10px')
  290. .attr('fill', 'black')
  291. .text(function(d) {
  292. // return d.name + ' (' + d.length.toFixed(4) + ')'; // hide length
  293. return d.name;
  294. });
  295. }
  296. return {tree: tree, vis: vis}
  297. }
  298. d3.phylogram.buildRadial = function(selector, nodes, options) {
  299. options = options || {};
  300. var fill = options.fill || function(d) {
  301. return 'cyan';
  302. };
  303. var size = options.size || function(d) {
  304. return 6;
  305. }
  306. var nodeMouseOver = options.nodeMouseOver || function(d) {};
  307. var nodeMouseOut = options.nodeMouseOut || function(d) {};
  308. var nodeMouseDown = options.nodeMouseDown || function(d) {};
  309. var w = options.width || d3.select(selector).style('width') || d3.select(selector).attr('width'),
  310. r = w / 2,
  311. labelWidth = options.skipLabels ? 10 : options.labelWidth || 120;
  312. var vis = d3.select(selector).append("svg:svg")
  313. .attr("width", r * 2)
  314. .attr("height", r * 2)
  315. .append("svg:g")
  316. .attr("transform", "translate(" + r + "," + r + ")");
  317. var tree = d3.layout.tree()
  318. .size([360, r - labelWidth])
  319. .sort(function(node) { return node.children ? node.children.length : -1; })
  320. .children(options.children || function(node) {
  321. return node.children;
  322. })
  323. .separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
  324. var phylogram = d3.phylogram.build(selector, nodes, {
  325. vis: vis,
  326. tree: tree,
  327. fill : fill,
  328. size: size,
  329. nodeMouseOver : nodeMouseOver,
  330. nodeMouseOut : nodeMouseOut,
  331. nodeMouseDown : nodeMouseDown,
  332. skipBranchLengthScaling: true,
  333. skipTicks: true,
  334. skipLabels: options.skipLabels,
  335. diagonal: d3.phylogram.radialRightAngleDiagonal()
  336. })
  337. vis.selectAll('g.node')
  338. .attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
  339. if (!options.skipLabels) {
  340. vis.selectAll('g.leaf.node text')
  341. .attr("dx", function(d) { return d.x < 180 ? 8 : -8; })
  342. .attr("dy", ".31em")
  343. .attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
  344. .attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; })
  345. .attr('font-family', 'Helvetica Neue, Helvetica, sans-serif')
  346. .attr('font-size', '10px')
  347. .attr('fill', 'black')
  348. .text(function(d) {
  349. return d.name;
  350. });
  351. vis.selectAll('g.inner.node text')
  352. .attr("dx", function(d) { return d.x < 180 ? -6 : 6; })
  353. .attr("text-anchor", function(d) { return d.x < 180 ? "end" : "start"; })
  354. .attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; });
  355. }
  356. return {tree: tree, vis: vis}
  357. }
  358. }());