Created
December 6, 2012 14:02
-
-
Save rdmpage/4224658 to your computer and use it in GitHub Desktop.
Javascript phylogeny viewer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<title>Javascript Phylogeny Viewer</title> | |
<style type="text/css" title="text/css"> | |
@import url("/style.css?20120730"); | |
</style> | |
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script> | |
<script type="text/javascript" src="http://blog.accursedware.com/jquery-svgpan/jquery-svgpan.js"></script> | |
<script src="treelib.js"></script> | |
</head> | |
<body> | |
<p><b>Paste in a <a href="http://en.wikipedia.org/wiki/Newick_format">Newick-format</a> tree description</b></p> | |
<div> | |
<textarea id="newick" rows="10" cols=100"></textarea> | |
<select id="style"> | |
<option value="cladogram">Cladogram</option> | |
<option value="rectanglecladogram">Rectangular cladogram</option> | |
<option value="phylogram">Phylogram</option> | |
<option value="circle">Circle tree</option> | |
<option value="circlephylogram">Circle phylogram</option> | |
</select> | |
<button onclick="showtree('newick')">Show</button> | |
</div> | |
<p><span id="message"></span></p> | |
<div style="width:500px;height:500px;background-color:white;border:1px solid rgb(228,228,228);"> | |
<svg id="svg" xmlns="http://www.w3.org/2000/svg" version="1.1" height="500" width="500"> | |
<g id="viewport"> | |
</g> | |
</svg> | |
</div> | |
<script> | |
// http://stackoverflow.com/questions/498970/how-do-i-trim-a-string-in-javascript | |
if (!String.prototype.trim) | |
{ | |
String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g, '');}; | |
} | |
function showtree(element_id) | |
{ | |
var t = new Tree(); | |
var element = document.getElementById(element_id); | |
var newick = element.value; | |
newick = newick.trim(newick); | |
t.Parse(newick); | |
if (t.error != 0) | |
{ | |
document.getElementById('message').innerHTML='Error parsing tree'; | |
} | |
else | |
{ | |
document.getElementById('message').innerHTML='Parsed OK'; | |
t.WriteNewick(); | |
t.ComputeWeights(t.root); | |
var td = null; | |
var selectmenu = document.getElementById('style'); | |
var drawing_type = (selectmenu.options[selectmenu.selectedIndex].value); | |
switch (drawing_type) | |
{ | |
case 'rectanglecladogram': | |
td = new RectangleTreeDrawer(); | |
break; | |
case 'phylogram': | |
if (t.has_edge_lengths) | |
{ | |
td = new PhylogramTreeDrawer(); | |
} | |
else | |
{ | |
td = new RectangleTreeDrawer(); | |
} | |
break; | |
case 'circle': | |
td = new CircleTreeDrawer(); | |
break; | |
case 'circlephylogram': | |
if (t.has_edge_lengths) | |
{ | |
td = new CirclePhylogramDrawer(); | |
} | |
else | |
{ | |
td = new CircleTreeDrawer(); | |
} | |
break; | |
case 'cladogram': | |
default: | |
td = new TreeDrawer(); | |
break; | |
} | |
// clear existing diagram, if any | |
var svg = document.getElementById('svg'); | |
while (svg.hasChildNodes()) | |
{ | |
svg.removeChild(svg.lastChild); | |
} | |
var cssStyle = document.createElementNS('http://www.w3.org/2000/svg','style'); | |
cssStyle.setAttribute('type','text/css'); | |
var style=document.createTextNode("text{font-size:6px;}"); | |
cssStyle.appendChild(style); | |
svg.appendChild(cssStyle); | |
var g = document.createElementNS('http://www.w3.org/2000/svg','g'); | |
g.setAttribute('id','viewport'); | |
svg.appendChild(g); | |
td.Init(t, {svg_id: 'viewport', width:500, height:500, fontHeight:10, root_length:0.1} ); | |
td.CalcCoordinates(); | |
td.Draw(); | |
// label leaves... | |
var n = new NodeIterator(t.root); | |
var q = n.Begin(); | |
while (q != null) | |
{ | |
if (q.IsLeaf()) | |
{ | |
switch (drawing_type) | |
{ | |
case 'circle': | |
case 'circlephylogram': | |
var align = 'left'; | |
var angle = q.angle * 180.0/Math.PI; | |
if ((q.angle > Math.PI/2.0) && (q.angle < 1.5 * Math.PI)) | |
{ | |
align = 'right'; | |
angle += 180.0; | |
} | |
drawRotatedText('viewport', q.xy, q.label, angle, align) | |
break; | |
case 'cladogram': | |
case 'rectanglecladogram': | |
case 'phylogram': | |
default: | |
drawText('viewport', q.xy, q.label); | |
break; | |
} | |
} | |
q = n.Next(); | |
} | |
// pan | |
$('svg').svgPan('viewport'); | |
} | |
} | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* | |
* Javascript library to display phylogenetic trees | |
* | |
*/ | |
//-------------------------------------------------------------------------------------------------- | |
// http://stackoverflow.com/questions/3019278/any-way-to-specify-the-base-of-math-log-in-javascript | |
function log10(val) { | |
return Math.log(val) / Math.LN10; | |
} | |
// http://stackoverflow.com/questions/387707/whats-the-best-way-to-define-a-class-in-javascript | |
//-------------------------------------------------------------------------------------------------- | |
// http://stackoverflow.com/questions/1303646/check-whether-variable-is-number-or-string-in-javascript | |
function isNumber (o) { | |
return ! isNaN (o-0); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
function ctype_alnum (str) | |
{ | |
return (str.match(/^[a-z0-9]+$/i) != null); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
function linePath(p0, p1) | |
{ | |
var path = 'M ' + p0['x'] + ' ' + p0['y'] + ' ' + p1['x'] + ' ' + p1['y']; | |
return path; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
function drawLine(svg_id, p0, p1) | |
{ | |
var line = document.createElementNS('http://www.w3.org/2000/svg','path'); | |
//newLine.setAttribute('id','node' + p.id); | |
line.setAttribute('vector-effect','non-scaling-stroke'); | |
line.setAttribute('style','stroke:black;stroke-width:1;'); | |
line.setAttribute('d', linePath(p0, p1)); | |
var svg = document.getElementById(svg_id); | |
svg.appendChild(line); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
function drawText(svg_id, p, string) | |
{ | |
var text = document.createElementNS('http://www.w3.org/2000/svg','text'); | |
//newLine.setAttribute('id','node' + p.id); | |
text.setAttribute('style','alignment-baseline:middle'); | |
text.setAttribute('x', p['x']); | |
text.setAttribute('y', p['y']); | |
var textNode=document.createTextNode(string) | |
text.appendChild(textNode); | |
var svg = document.getElementById(svg_id); | |
svg.appendChild(text); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
function drawRotatedText(svg_id, p, string, angle, align) | |
{ | |
var text = document.createElementNS('http://www.w3.org/2000/svg','text'); | |
//newLine.setAttribute('id','node' + p.id); | |
text.setAttribute('style','alignment-baseline:middle'); | |
text.setAttribute('x', p['x']); | |
text.setAttribute('y', p['y']); | |
switch (align) | |
{ | |
case 'left': | |
text.setAttribute('text-anchor', 'start'); | |
break; | |
case 'centre': | |
case 'center': | |
text.setAttribute('text-anchor', 'middle'); | |
break; | |
case 'right': | |
text.setAttribute('text-anchor', 'end'); | |
break; | |
default: | |
text.setAttribute('text-anchor', 'start'); | |
break; | |
} | |
if (angle != 0) | |
{ | |
text.setAttribute('transform', 'rotate(' + angle + ' ' + p['x'] + ' ' + p['y'] + ')'); | |
} | |
var textNode=document.createTextNode(string) | |
text.appendChild(textNode); | |
var svg = document.getElementById(svg_id); | |
svg.appendChild(text); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
function circeArcPath(p0, p1, radius, large_arc_flag) | |
{ | |
var path = 'M ' | |
+ p0['x'] + ' ' + p0['y'] | |
+ ' A ' + radius + ' ' + radius | |
+ ' 0 '; | |
if (large_arc_flag) | |
{ | |
path += ' 1 '; | |
} | |
else | |
{ | |
path += ' 0 '; | |
} | |
path += ' 1 ' | |
+ p1['x'] + ' ' + p1['y'] ; | |
return path; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
function drawCircleArc(svg_id, p0, p1, radius, large_arc_flag) | |
{ | |
var arc = document.createElementNS('http://www.w3.org/2000/svg','path'); | |
arc.setAttribute('vector-effect','non-scaling-stroke'); | |
arc.setAttribute('style','stroke:black;stroke-width:1;'); | |
arc.setAttribute('fill','none'); | |
var path = circeArcPath(p0, p1, radius, large_arc_flag); | |
arc.setAttribute('d', path) | |
var svg = document.getElementById(svg_id); | |
svg.appendChild(arc); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
function drawPath(svg_id, pathString) | |
{ | |
var path = document.createElementNS('http://www.w3.org/2000/svg','path'); | |
//newLine.setAttribute('id','node' + p.id); | |
path.setAttribute('vector-effect','non-scaling-stroke'); | |
path.setAttribute('style','stroke:blue;stroke-width:1;'); | |
path.setAttribute('d', pathString); | |
var svg = document.getElementById(svg_id); | |
svg.appendChild(path); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
// http://stackoverflow.com/questions/894860/set-a-default-parameter-value-for-a-javascript-function | |
function Node(label) | |
{ | |
this.ancestor = null; | |
this.child = null; | |
this.sibling = null; | |
this.label = typeof label !== 'undefined' ? label : ''; | |
this.id = 0; | |
this.weight = 0; | |
this.xy = []; | |
this.edge_length = 0.0; | |
this.path_length = 0.0; | |
this.depth = 0; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
Node.prototype.IsLeaf = function() | |
{ | |
return (!this.child); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
Node.prototype.GetRightMostSibling = function() | |
{ | |
var p = this; | |
while (p.sibling) | |
{ | |
p = p.sibling; | |
} | |
return p; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
function Tree() | |
{ | |
this.root = null; | |
this.num_leaves = 0; | |
this.num_nodes = 0; | |
this.label_to_node_map = []; | |
this.nodes = []; | |
this.rooted = true; | |
this.has_edge_lengths = false; | |
this.error = 0; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
Tree.prototype.NewNode = function(label) | |
{ | |
var node = new Node(label); | |
node.id = this.num_nodes++; | |
this.nodes[node.id] = node; | |
if (typeof label !== undefined) | |
{ | |
this.label_to_node_map[label] = node.id; | |
} | |
return node; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
Tree.prototype.Parse = function(str) | |
{ | |
str = str.replace('"', ""); | |
str = str.replace(/\(/g, "|(|"); | |
str = str.replace(/\)/g, "|)|"); | |
str = str.replace(/,/g, "|,|"); | |
str = str.replace(/:/g, "|:|"); | |
str = str.replace(/;/g, "|;|"); | |
str = str.replace(/\|\|/g, "|"); | |
str = str.replace(/^\|/, ""); | |
str = str.replace(/\|$/, ""); | |
//console.log(str); | |
var token = str.split("|"); | |
var curnode = this.NewNode(); | |
this.root = curnode; | |
var state = 0; | |
var stack = []; | |
var i = 0; | |
var q = null; | |
this.error = 0; | |
while ((state != 99) && (this.error == 0)) | |
{ | |
switch (state) | |
{ | |
case 0: | |
if (ctype_alnum(token[i].charAt(0))) | |
{ | |
this.num_leaves++; | |
label = token[i]; | |
// to do: KML | |
curnode.label = label; | |
this.label_to_node_map[label] = curnode; | |
i++; | |
state = 1; | |
} | |
else | |
{ | |
if (token[i].charAt(0) == "'") | |
{ | |
label = token[i]; | |
label = label.replace(/^'/, ""); | |
label = label.replace(/'$/, ""); | |
this.num_leaves++; | |
// to do: KML | |
curnode.label = label; | |
this.label_to_node_map[label] = curnode; | |
i++; | |
state = 1; | |
} | |
else | |
{ | |
switch (token[i]) | |
{ | |
case '(': | |
state = 2; | |
break; | |
default: | |
state = 99; | |
this.error = 1; // syntax | |
break; | |
} | |
} | |
} | |
break; | |
case 1: // getinternode | |
switch (token[i]) | |
{ | |
case ':': | |
case ',': | |
case ')': | |
state = 2; | |
break; | |
default: | |
state = 99; | |
this.error = 1; // syntax | |
break; | |
} | |
break; | |
case 2: // nextmove | |
switch (token[i]) | |
{ | |
case ':': | |
i++; | |
if (isNumber(token[i])) | |
{ | |
curnode.edge_length = parseFloat(token[i]); | |
this.has_edge_lengths = true; | |
i++; | |
} | |
break; | |
case ',': | |
q = this.NewNode(); | |
curnode.sibling = q; | |
var c = stack.length; | |
if (c == 0) | |
{ | |
state = 99; | |
this.error = 2; // missing ( | |
} | |
else | |
{ | |
q.ancestor = stack[c - 1]; | |
curnode = q; | |
state = 0; | |
i++; | |
} | |
break; | |
case '(': | |
stack.push(curnode); | |
q = this.NewNode(); | |
curnode.child = q; | |
q.ancestor = curnode; | |
curnode = q; | |
state = 0; | |
i++; | |
break; | |
case ')': | |
if (stack.length == 0) | |
{ | |
state = 99; | |
this.error = 3; // unbalanced | |
} | |
else | |
{ | |
curnode = stack.pop(); | |
state = 3; | |
i++; | |
} | |
break; | |
case ';': | |
if (stack.length == 0) | |
{ | |
state = 99; | |
} | |
else | |
{ | |
state = 99; | |
this.error = 4; // stack not empty | |
} | |
break; | |
default: | |
state = 99; | |
this.error = 1; // syntax | |
break; | |
} | |
break; | |
case 3: // finishchildren | |
if (ctype_alnum(token[i].charAt(0))) | |
{ | |
curnode.label = token[i]; | |
this.label_to_node_map[token[i]] = curnode; | |
i++; | |
} | |
else | |
{ | |
switch (token[i]) | |
{ | |
case ':': | |
i++; | |
if (isNumber(token[i])) | |
{ | |
curnode.edge_length = parseFloat(token[i]); | |
this.has_edge_lengths = true; | |
i++; | |
} | |
break; | |
case ')': | |
if (stack.length == 0) | |
{ | |
state = 99; | |
this.error = 3; // unbalanced | |
} | |
else | |
{ | |
curnode = stack.pop(); | |
i++; | |
} | |
break; | |
case ',': | |
q = this.NewNode(); | |
curnode.sibling = q; | |
if (stack.length == 0) | |
{ | |
state = 99; | |
this.error = 2; // missing ( | |
} | |
else | |
{ | |
q.ancestor = stack[stack.length - 1]; | |
curnode = q; | |
state = 0; | |
i++; | |
} | |
break; | |
case ';': | |
state = 2; | |
break; | |
default: | |
state = 99; | |
this.error = 1; // syntax | |
break; | |
} | |
} | |
break; | |
} | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
Tree.prototype.ComputeWeights = function(p) | |
{ | |
if (p) | |
{ | |
p.weight = 0; | |
this.ComputeWeights(p.child); | |
this.ComputeWeights(p.sibling); | |
if (p.IsLeaf()) | |
{ | |
p.weight = 1; | |
} | |
if (p.ancestor) | |
{ | |
p.ancestor.weight += p.weight; | |
} | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
Tree.prototype.ComputeDepths = function() | |
{ | |
for (var i in this.nodes) | |
{ | |
if (this.nodes[i].IsLeaf()) | |
{ | |
p = this.nodes[i].ancestor; | |
var count = 1; | |
while (p) | |
{ | |
p.depth = Math.max(p.depth, count); | |
count++; | |
p = p.ancestor; | |
} | |
} | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
Tree.prototype.WriteNewick = function() | |
{ | |
var newick = ''; | |
var stack = []; | |
var curnode = this.root; | |
while (curnode) | |
{ | |
//console.log(curnode.label); | |
if (curnode.child) | |
{ | |
newick += '('; | |
stack.push(curnode); | |
curnode = curnode.child; | |
} | |
else | |
{ | |
newick += curnode.label; | |
var length = curnode.edge_length; | |
if (length) | |
{ | |
newick += ':' + length; | |
} | |
while (stack.length > 0 && curnode.sibling == null) | |
{ | |
newick += ')'; | |
curnode = stack.pop(); | |
// internal node label and length | |
if (typeof curnode.label !== undefined) | |
{ | |
newick += curnode.label; | |
} | |
var length = curnode.edge_length; | |
if (length) | |
{ | |
newick += ':' + length; | |
} | |
} | |
if (stack.length == 0) | |
{ | |
curnode = null; | |
} | |
else | |
{ | |
newick += ','; | |
curnode = curnode.sibling; | |
} | |
} | |
} | |
newick += ';'; | |
return newick; | |
//console.log(newick); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
function NodeIterator(root) | |
{ | |
this.root = root; | |
this.cur = null; | |
this.stack = []; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
NodeIterator.prototype.Begin = function() | |
{ | |
this.cur = this.root; | |
while (this.cur.child) | |
{ | |
this.stack.push(this.cur); | |
this.cur = this.cur.child; | |
} | |
return this.cur; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
NodeIterator.prototype.Next = function() | |
{ | |
if (this.stack.length == 0) | |
{ | |
this.cur = null; | |
} | |
else | |
{ | |
if (this.cur.sibling) | |
{ | |
var p = this.cur.sibling; | |
while (p.child) | |
{ | |
this.stack.push(p); | |
p = p.child; | |
} | |
this.cur = p; | |
} | |
else | |
{ | |
this.cur = this.stack.pop(); | |
} | |
} | |
return this.cur; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
PreorderIterator.prototype = new NodeIterator; | |
function PreorderIterator() | |
{ | |
NodeIterator.apply(this, arguments) | |
}; | |
//-------------------------------------------------------------------------------------------------- | |
PreorderIterator.prototype.Begin = function() | |
{ | |
this.cur = this.root; | |
return this.cur; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
PreorderIterator.prototype.Next = function() | |
{ | |
if (this.cur.child) | |
{ | |
this.stack.push(this.cur); | |
this.cur = this.cur.child; | |
} | |
else | |
{ | |
while (this.stack.length > 0 && this.cur.sibling == null) | |
{ | |
this.cur = this.stack.pop(); | |
} | |
if (this.stack.length == 0) | |
{ | |
this.cur = null; | |
} | |
else | |
{ | |
this.cur = this.cur.sibling; | |
} | |
} | |
return this.cur; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
function TreeDrawer() | |
{ | |
//this.t = tree; | |
this.leaf_count = 0; | |
this.leaf_gap = 0; | |
this.node_gap = 0; | |
this.last_y = 0; | |
this.svg_id; | |
this.draw_scale_bar = false; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
TreeDrawer.prototype.Init = function(tree, settings) | |
{ | |
this.t = tree; | |
// defaults | |
this.settings = settings; | |
this.left = 0; | |
this.top = 0; | |
/* | |
if (this.settings.fontHeight) | |
{ | |
this.top += this.settings.fontHeight/2.0; | |
this.settings.height -= this.settings.fontHeight; | |
} | |
*/ | |
} | |
//-------------------------------------------------------------------------------------------------- | |
TreeDrawer.prototype.CalcInternal = function(p) | |
{ | |
var pt = []; | |
pt['x'] = this.left + this.node_gap * (this.t.num_leaves - p.weight); | |
pt['y'] = this.last_y - ((p.weight - 1) * this.leaf_gap)/2; | |
p.xy = pt; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
TreeDrawer.prototype.CalcLeaf = function(p) | |
{ | |
var pt = []; | |
pt['y'] = this.top + (this.leaf_count * this.leaf_gap); | |
this.last_y = pt['y']; | |
this.leaf_count++; | |
// slanted cladogram | |
pt['x'] = this.left + this.settings.width; | |
p.xy = pt; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
TreeDrawer.prototype.CalcNodeGap = function() | |
{ | |
if (this.t.rooted) | |
{ | |
this.node_gap = this.settings.width / this.t.num_leaves; | |
this.left += this.node_gap; | |
this.settings.width -= this.node_gap; | |
} | |
else | |
{ | |
this.node_gap = this.settings.width / (this.t.num_leaves - 1); | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
TreeDrawer.prototype.CalcCoordinates = function() | |
{ | |
this.t.ComputeWeights(this.t.root); | |
this.leaf_count = 0; | |
this.leaf_gap = this.settings.height/(this.t.num_leaves - 1); | |
this.CalcNodeGap(); | |
var n = new NodeIterator(this.t.root); | |
var q = n.Begin(); | |
while (q != null) | |
{ | |
if (q.IsLeaf()) | |
{ | |
this.CalcLeaf(q); | |
} | |
else | |
{ | |
this.CalcInternal(q); | |
} | |
q = n.Next(); | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
TreeDrawer.prototype.DrawLeaf = function(p) | |
{ | |
var p0 = p.xy | |
var anc = p.ancestor; | |
if (anc) | |
{ | |
var p1 = anc.xy; | |
drawLine(this.settings.svg_id, p0, p1); | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
TreeDrawer.prototype.DrawInternal = function(p) | |
{ | |
var p0 = p.xy | |
var anc = p.ancestor; | |
if (anc) | |
{ | |
var p1 = anc.xy; | |
drawLine(this.settings.svg_id, p0, p1); | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
TreeDrawer.prototype.DrawRoot = function() | |
{ | |
var p0 = this.t.root.xy | |
var p1 = []; | |
p1['x'] = p0['x']; | |
p1['y'] = p0['y']; | |
p1['x'] -= this.node_gap; | |
drawLine(this.settings.svg_id, p0, p1); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
TreeDrawer.prototype.Draw = function() | |
{ | |
var n = new NodeIterator(this.t.root); | |
var q = n.Begin(); | |
while (q != null) | |
{ | |
if (q.IsLeaf()) | |
{ | |
this.DrawLeaf(q); | |
} | |
else | |
{ | |
this.DrawInternal(q); | |
} | |
q = n.Next(); | |
} | |
if (this.t.rooted) | |
{ | |
this.DrawRoot(); | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
RectangleTreeDrawer.prototype = new TreeDrawer(); | |
function RectangleTreeDrawer() | |
{ | |
TreeDrawer.apply(this, arguments); | |
this.max_depth = 0; | |
}; | |
//-------------------------------------------------------------------------------------------------- | |
RectangleTreeDrawer.prototype.CalcInternal = function(p) | |
{ | |
var pt = []; | |
pt['x'] = this.left + this.node_gap * (this.t.root.depth - p.depth); | |
var pl = p.child.xy; | |
var pr = p.child.GetRightMostSibling().xy; | |
pt['y'] = pl['y'] + (pr['y'] - pl['y'])/2; | |
p.xy['x'] = pt['x']; | |
p.xy['y'] = pt['y']; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
RectangleTreeDrawer.prototype.CalcNodeGap = function() | |
{ | |
this.t.ComputeDepths(); | |
//console.log(this.t.root.depth); | |
if (this.t.rooted) | |
{ | |
this.node_gap = this.settings.width / (this.t.root.depth + 1); | |
this.left += this.node_gap; | |
this.settings.width -= this.node_gap; | |
} | |
else | |
{ | |
this.node_gap = this.settings.width / this.t.root.depth; | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
RectangleTreeDrawer.prototype.DrawLeaf = function(p) | |
{ | |
var p0 = p.xy | |
var p1 = []; | |
var anc = p.ancestor; | |
if (anc) | |
{ | |
p1['x'] = anc.xy['x']; | |
p1['y'] = p0['y']; | |
drawLine(this.settings.svg_id, p0, p1); | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
RectangleTreeDrawer.prototype.DrawInternal = function(p) | |
{ | |
var p0 = []; | |
var p1 = []; | |
p0['x'] = p.xy['x']; | |
p0['y'] = p.xy['y']; | |
var anc = p.ancestor; | |
if (anc) | |
{ | |
p1['x'] = anc.xy['x']; | |
p1['y'] = p0['y']; | |
drawLine(this.settings.svg_id, p0, p1); | |
} | |
// vertical line | |
var pl = p.child.xy; | |
var pr = p.child.GetRightMostSibling().xy; | |
p0['x'] = p0['x']; | |
p0['y'] = pl['y']; | |
p1['x'] = p0['x']; | |
p1['y'] = pr['y']; | |
drawLine(this.settings.svg_id, p0, p1); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
PhylogramTreeDrawer.prototype = new RectangleTreeDrawer(); | |
function PhylogramTreeDrawer() | |
{ | |
RectangleTreeDrawer.apply(this, arguments); | |
this.max_path_length = 0; | |
this.draw_scale_bar = true; | |
}; | |
//-------------------------------------------------------------------------------------------------- | |
PhylogramTreeDrawer.prototype.CalcInternal = function(p) | |
{ | |
var pt = []; | |
pt['x'] = this.left + (p.path_length / this.max_path_length) * this.settings.width; | |
var pl = p.child.xy; | |
var pr = p.child.GetRightMostSibling().xy; | |
pt['y'] = pl['y'] + (pr['y'] - pl['y'])/2; | |
p.xy['x'] = pt['x']; | |
p.xy['y'] = pt['y']; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
PhylogramTreeDrawer.prototype.CalcLeaf = function(p) | |
{ | |
var pt = []; | |
pt['x'] = this.left + (p.path_length / this.max_path_length) * this.settings.width; | |
pt['y'] = this.top + (this.leaf_count * this.leaf_gap); | |
this.last_y = pt['y']; | |
this.leaf_count++; | |
p.xy['x'] = pt['x']; | |
p.xy['y'] = pt['y']; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
PhylogramTreeDrawer.prototype.CalcCoordinates = function() | |
{ | |
this.max_path_length = 0; | |
//console.log(this.max_path_length); | |
this.t.root.path_length = this.t.root.edge_length; | |
// build path lengths | |
var n = new PreorderIterator(this.t.root); | |
var q = n.Begin(); | |
while (q != null) | |
{ | |
var d = q.edge_length; | |
if (d < 0.00001) | |
{ | |
d = 0.0; | |
} | |
if (q != this.t.root) | |
{ | |
q.path_length = q.ancestor.path_length + d; | |
} | |
//console.log(q.label + ' ' + q.path_length + ' ' + q.edge_length); | |
this.max_path_length = Math.max(this.max_path_length, q.path_length); | |
q = n.Next(); | |
} | |
//console.log(this.max_path_length); | |
this.leaf_count = 0; | |
this.leaf_gap = this.settings.height/(this.t.num_leaves - 1); | |
n = new NodeIterator(this.t.root); | |
var q = n.Begin(); | |
while (q != null) | |
{ | |
if (q.IsLeaf()) | |
{ | |
this.CalcLeaf(q); | |
} | |
else | |
{ | |
this.CalcInternal(q); | |
} | |
q = n.Next(); | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
PhylogramTreeDrawer.prototype.Draw = function() | |
{ | |
// parent method | |
RectangleTreeDrawer.prototype.Draw.call(this); | |
// scale bar | |
if (this.draw_scale_bar) | |
{ | |
this.DrawScaleBar(); | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
PhylogramTreeDrawer.prototype.DrawScaleBar = function() | |
{ | |
var p0 = []; | |
var p1 = []; | |
var m = log10(this.max_path_length); | |
var i = Math.floor(m); | |
var bar = Math.pow(10,i); | |
var scalebar = (bar/this.max_path_length) * this.settings.width; | |
p0['x'] = this.left; | |
p0['y'] = this.top + this.settings.height + this.leaf_gap; | |
p1['x'] = p0['x'] + scalebar; | |
p1['y'] = p0['y']; | |
drawLine(this.settings.svg_id, p0, p1); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CircleTreeDrawer.prototype = new RectangleTreeDrawer(); | |
function CircleTreeDrawer() | |
{ | |
RectangleTreeDrawer.apply(this, arguments); | |
this.leaf_angle = 0; | |
this.leaf_radius = 0; | |
this.max_path_length = 0; | |
this.root_length = 0; | |
}; | |
//-------------------------------------------------------------------------------------------------- | |
CircleTreeDrawer.prototype.CalcInternalRadius = function(p) | |
{ | |
p.radius = this.node_gap * (this.t.root.depth - p.depth); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CircleTreeDrawer.prototype.CalcInternal = function(p) | |
{ | |
var left_angle = p.child.angle; | |
var right_angle = p.child.GetRightMostSibling().angle; | |
p.angle = left_angle + (right_angle - left_angle)/2; | |
this.CalcInternalRadius(p); | |
var pt = []; | |
pt['x'] = p.radius * Math.cos(p.angle); | |
pt['y'] = p.radius * Math.sin(p.angle); | |
p.xy['x'] = pt['x']; | |
p.xy['y'] = pt['y']; | |
var q = p.child; | |
while (q) | |
{ | |
pt = []; | |
pt['x'] = p.radius * Math.cos(q.angle); | |
pt['y'] = p.radius * Math.sin(q.angle); | |
q.backarc = []; | |
q.backarc['x'] = pt['x']; | |
q.backarc['y'] = pt['y']; | |
q = q.sibling; | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CircleTreeDrawer.prototype.CalcLeafRadius = function(p) | |
{ | |
p.radius = this.leaf_radius; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CircleTreeDrawer.prototype.CalcLeaf = function(p) | |
{ | |
p.angle = this.leaf_angle * this.leaf_count; | |
this.leaf_count++; | |
this.CalcLeafRadius(p); | |
var pt = []; | |
pt['x'] = p.radius * Math.cos(p.angle); | |
pt['y'] = p.radius * Math.sin(p.angle); | |
p.xy['x'] = pt['x']; | |
p.xy['y'] = pt['y']; | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CircleTreeDrawer.prototype.DrawLeaf = function(p) | |
{ | |
var p0 = p.xy | |
var p1 = p.backarc; | |
drawLine(this.settings.svg_id, p0, p1); | |
/* | |
var p0 = p.xy | |
var p1 = p.backarc; | |
var path = linePath(p0, p1); | |
var anc = p.ancestor; | |
if (anc) | |
{ | |
var p2 = anc.xy; | |
var large_arc_flag = false; | |
if (p.angle < anc.angle) | |
{ | |
large_arc_flag = (Math.abs(anc.angle - p.angle) > Math.PI) ? true : false; | |
path += circeArcPath(p1, p2, p.radius, large_arc_flag); | |
} | |
else | |
{ | |
large_arc_flag = (Math.abs(p.angle - anc.angle) > Math.PI) ? true : false; | |
path += circeArcPath(p2, p1, p.radius, large_arc_flag); | |
} | |
} | |
drawPath(path); | |
*/ | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CircleTreeDrawer.prototype.DrawInternal = function(p) | |
{ | |
var p0 = []; | |
var p1 = []; | |
p0['x'] = p.xy['x']; | |
p0['y'] = p.xy['y']; | |
var anc = p.ancestor; | |
if (anc) | |
{ | |
p0 = p.xy; | |
p1 = p.backarc; | |
drawLine(this.settings.svg_id, p0, p1); | |
} | |
// draw arc | |
p0 = p.child.backarc; | |
p1 = p.child.GetRightMostSibling().backarc; | |
var large_arc_flag = (Math.abs(p.child.GetRightMostSibling().angle - p.child.angle) > Math.PI) ? true : false; | |
drawCircleArc(this.settings.svg_id, p0, p1, p.radius, large_arc_flag); | |
/* | |
var anc = p.ancestor; | |
if (anc) | |
{ | |
var p0 = p.xy | |
var p1 = p.backarc; | |
var path = '';//linePath(p0, p1); | |
var p2 = anc.xy; | |
var large_arc_flag = false; //(Math.abs(p.angle - anc.angle) > Math.PI) ? true : false; | |
if (p.angle < anc.angle) | |
{ | |
path += circeArcPath(p1, p2, p.radius, large_arc_flag); | |
} | |
else | |
{ | |
path += circeArcPath(p2, p1, p.radius, large_arc_flag); | |
} | |
drawPath(path); | |
} | |
*/ | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CircleTreeDrawer.prototype.DrawRoot = function() | |
{ | |
var p0 = this.t.root.xy | |
var p1 = []; | |
p1['x'] = 0; | |
p1['y'] = 0; | |
drawLine(this.settings.svg_id, p0, p1); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CircleTreeDrawer.prototype.CalcCoordinates = function() | |
{ | |
this.t.ComputeDepths(); | |
this.max_path_length = 0; | |
//console.log(this.max_path_length); | |
this.t.root.path_length = this.t.root.edge_length; | |
// build path lengths | |
var n = new PreorderIterator(this.t.root); | |
var q = n.Begin(); | |
while (q != null) | |
{ | |
var d = q.edge_length; | |
if (d < 0.00001) | |
{ | |
d = 0.0; | |
} | |
if (q != this.t.root) | |
{ | |
q.path_length = q.ancestor.path_length + d; | |
} | |
//console.log(q.label + ' ' + q.path_length + ' ' + q.edge_length); | |
this.max_path_length = Math.max(this.max_path_length, q.path_length); | |
q = n.Next(); | |
} | |
this.leaf_count = 0; | |
this.leaf_angle = 2 * Math.PI / this.t.num_leaves; | |
this.leaf_radius = this.settings.width/2; | |
this.node_gap = this.leaf_radius / this.t.root.depth; | |
n = new NodeIterator(this.t.root); | |
var q = n.Begin(); | |
while (q != null) | |
{ | |
if (q.IsLeaf()) | |
{ | |
this.CalcLeaf(q); | |
} | |
else | |
{ | |
this.CalcInternal(q); | |
} | |
q = n.Next(); | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CircleTreeDrawer.prototype.Draw = function() | |
{ | |
// parent method | |
TreeDrawer.prototype.Draw.call(this); | |
// move drawing to centre of viewport | |
var viewport = document.getElementById(this.settings.svg_id); | |
viewport.setAttribute('transform', 'translate(' + (this.settings.width + this.root_length)/2 + ' ' + this.settings.height/2 + ')'); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CirclePhylogramDrawer.prototype = new CircleTreeDrawer(); | |
function CirclePhylogramDrawer() | |
{ | |
CircleTreeDrawer.apply(this, arguments) | |
this.max_path_length = 0; | |
this.draw_scale_bar = true; | |
}; | |
//-------------------------------------------------------------------------------------------------- | |
CirclePhylogramDrawer.prototype.CalcInternalRadius = function(p) | |
{ | |
p.radius = this.root_length + (p.path_length / this.max_path_length) * (this.settings.width/2) | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CirclePhylogramDrawer.prototype.CalcLeafRadius = function(p) | |
{ | |
p.radius = this.root_length + (p.path_length / this.max_path_length) * (this.settings.width/2) | |
} | |
//-------------------------------------------------------------------------------------------------- | |
CirclePhylogramDrawer.prototype.CalcCoordinates = function() | |
{ | |
this.max_path_length = 0; | |
//console.log(this.max_path_length); | |
if (this.settings.root_length) | |
{ | |
this.root_length = this.settings.root_length * (this.settings.width/2); | |
this.settings.width -= 2 * this.root_length; | |
} | |
this.t.root.path_length = this.t.root.edge_length; | |
// build path lengths | |
var n = new PreorderIterator(this.t.root); | |
var q = n.Begin(); | |
while (q != null) | |
{ | |
var d = q.edge_length; | |
if (d < 0.00001) | |
{ | |
d = 0.0; | |
} | |
if (q != this.t.root) | |
{ | |
q.path_length = q.ancestor.path_length + d; | |
} | |
this.max_path_length = Math.max(this.max_path_length, q.path_length); | |
q = n.Next(); | |
} | |
this.leaf_count = 0; | |
this.leaf_angle = 2 * Math.PI / this.t.num_leaves; | |
n = new NodeIterator(this.t.root); | |
var q = n.Begin(); | |
while (q != null) | |
{ | |
if (q.IsLeaf()) | |
{ | |
this.CalcLeaf(q); | |
} | |
else | |
{ | |
this.CalcInternal(q); | |
} | |
q = n.Next(); | |
} | |
} | |
//-------------------------------------------------------------------------------------------------- | |
// http://stackoverflow.com/questions/3231459/create-unique-id-with-javascript | |
function uniqueid(){ | |
// always start with a letter (for DOM friendlyness) | |
var idstr=String.fromCharCode(Math.floor((Math.random()*25)+65)); | |
do { | |
// between numbers and characters (48 is 0 and 90 is Z (42-48 = 90) | |
var ascicode=Math.floor((Math.random()*42)+48); | |
if (ascicode<58 || ascicode>64){ | |
// exclude all chars between : (58) and @ (64) | |
idstr+=String.fromCharCode(ascicode); | |
} | |
} while (idstr.length<32); | |
return (idstr); | |
} | |
//-------------------------------------------------------------------------------------------------- | |
// Draw a tree using Newick tree description for tag "element" | |
function draw_tree(element) | |
{ | |
var resize = false; | |
var height = 200; | |
var width = 200; | |
if (element.style.width) | |
{ | |
width = parseInt(element.style.width); | |
} | |
if (element.style.height) | |
{ | |
height = parseInt(element.style.height); | |
} | |
width = Math.min(width, height); | |
element.style.width = width + 'px'; | |
element.style.height = height + 'px'; | |
element.style.overflow = 'hidden'; | |
var t = new Tree(); | |
t.Parse(element.innerHTML); | |
element.innerHTML = ''; | |
var svg_id = uniqueid(); | |
var svg_g_id = uniqueid(); | |
var svg = document.createElementNS('http://www.w3.org/2000/svg','svg'); | |
svg.setAttribute('id',svg_id); | |
svg.setAttribute('version','1.1'); | |
svg.setAttribute('height',height); | |
svg.setAttribute('width',width); | |
element.appendChild(svg); | |
var g = document.createElementNS('http://www.w3.org/2000/svg','g'); | |
g.setAttribute('id',svg_g_id); | |
svg.appendChild(g); | |
var td = null; | |
var drawing_type = 'cladogram'; | |
if (element.hasAttribute('data-drawing-type')) | |
{ | |
drawing_type = element.getAttribute('data-drawing-type'); | |
} | |
switch (drawing_type) | |
{ | |
case 'rectanglecladogram': | |
td = new RectangleTreeDrawer(); | |
break; | |
case 'phylogram': | |
td = new PhylogramTreeDrawer(); | |
break; | |
case 'circle': | |
td = new CircleTreeDrawer(); | |
break; | |
case 'circlephylogram': | |
td = new CirclePhylogramDrawer(); | |
break; | |
case 'cladogram': | |
default: | |
td = new TreeDrawer(); | |
break; | |
} | |
td.Init(t, {svg_id: svg_g_id, width:width, height:height, root_length:0.1} ); | |
td.CalcCoordinates(); | |
td.Draw(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment