Published
Edited
Nov 30, 2020
2 stars
Insert cell
Insert cell
Insert cell
chart3 = {
const tree_df = radialLayout(readTree(treestring))
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("font-family", "sans-serif")
.attr("font-size", 10);
// create a grouping variable
var group = svg.append("g");
// zoom instead
function zoomed() {
group.attr("transform", d3.event.transform);
}
svg.call(d3.zoom()
.extent([[0, 0], [width, height]])
.scaleExtent([1, 8])
.on("zoom", zoomed));

var stroke_width = 3
// draw radii
group.append("g")
.attr("class", "phylo_lines")
.selectAll("lines")
.data(tree_df.arcs)
.join("path")
.attr("d", d => describeArc(xScale(0), yScale(0), d.radius*1225, d.start, d.end))
.attr('fill-opacity', '0')
.attr("class", "path")
.attr("stroke", "#777")
.attr("stroke-width", stroke_width)

// draw radii
group.append("g")
.attr("class", "phylo_lines")
.selectAll("lines")
.data(tree_df.radii)
.join("line")
.attr("class", "lines")
.attr("x1", d => xScale(d.x0))
.attr("y1", d => yScale(d.y0))
.attr("x2", d => xScale(d.x1))
.attr("y2", d => yScale(d.y1))
.attr("stroke-width", stroke_width)
.attr("stroke", "#777");
const tooltip = d3.select("body").append("div")
.attr("class", "svg-tooltip")
.attr("class", "tiplabs")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background-color", "black")
//draw nodes
group.append("g")
.attr("class", "phylo_points")
.selectAll(".dot")
// remove rogue dot.
.data(tree_df.radii)
.join("circle")
.attr("class", "dot")
.attr("r", function(d) {
if (d.isTip) {
return(4);
} else {
return(3);
}
})
.attr("cx", d => xScale(d.x0))
.attr("cy", d => yScale(d.y0))
.attr("stroke", "black")
.attr("stroke-width", 2)
.attr("fill", function(d) {
if (d.isTip) {
return("black");
} else {
return("white");
};
})
.on("mouseover", function(){
return tooltip.style("visibility", "visible");
})
.on("mousemove", function(d){
return tooltip
.style("top", (d3.event.pageY-10)+"px")
.style("left",(d3.event.pageX+10)+"px")
.style("color", "white")
.text(tree_df => d.thisId)
})
.on("mouseout", function(){
return tooltip.style("visibility", "hidden");
});
return svg.node()
}
Insert cell
radialLayout(readTree(treestring))
Insert cell
Insert cell
chart = {
const tree_df = rectangleLayout(readTree(treestring))
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("font-family", "sans-serif")
.attr("font-size", 10);
// create a grouping variable
var group = svg.append("g");
// zoom instead
function zoomed() {
group.attr("transform", d3.event.transform);
}
svg.call(d3.zoom()
.extent([[0, 0], [width, height]])
.scaleExtent([1, 8])
.on("zoom", zoomed));

var stroke_width = 3
// draw horizontal lines
group.append("g")
.attr("class", "phylo_lines")
.selectAll("lines")
.data(tree_df.horizontal_lines)
.join("line")
.attr("class", "lines")
.attr("x1", d => xScale(d.x0) - stroke_width/2)
.attr("y1", d => yScale(d.y0))
.attr("x2", d => xScale(d.x1) - stroke_width/2)
.attr("y2", d => yScale(d.y1))
.attr("stroke-width", stroke_width)
.attr("stroke", "#777");
// draw vertical lines
group.append("g")
.attr("class", "phylo_lines")
.selectAll("lines")
.data(tree_df.vertical_lines)
.join("line")
.attr("class", "lines")
.attr("x1", d => xScale(d.x0))
.attr("y1", d => yScale(d.y0))
.attr("x2", d => xScale(d.x1))
.attr("y2", d => yScale(d.y1))
.attr("stroke-width", stroke_width)
.attr("stroke", "#777");
// draw nodes
group.append("g")
.attr("class", "phylo_points")
.selectAll(".dot")
// remove rogue dot.
.data(tree_df.horizontal_lines.filter(d => d.x1 > 0))
.join("circle")
.attr("class", "dot")
.attr("r", function(d) {
if (d.isTip) {
return(4);
} else {
return(3);
}
})
.attr("cx", d => xScale(d.x1))
.attr("cy", d => yScale(d.y1))
.attr("stroke", "black")
.attr("stroke-width", 2)
.attr("fill", function(d) {
if (d.isTip) {
return("black");
} else {
return("white");
};
})
return svg.node()
}
Insert cell
Insert cell
xScale = d3
.scaleLinear()
.domain([0, 1])
.range([0, width])
Insert cell
yScale = d3
.scaleLinear()
.domain([0, 100])
.range([height, 0])
Insert cell
Insert cell
radialLayout(readTree(treestring))
Insert cell
tree = readTree(treestring)
Insert cell
Insert cell
Insert cell
margin = ({top: 0, right: 10, bottom: 10, left: 10})
Insert cell
width = 1000 - margin.left - margin.right
Insert cell
height = 1000 - margin.top - margin.bottom
Insert cell
origin = ({y: height/2,
x: width/2})
Insert cell
Insert cell
Insert cell
Insert cell
get_vertical(readTree(treestring))
Insert cell
get_horizontal(readTree(treestring))
Insert cell
Insert cell
function get_vertical(node){
var data = get_horizontal(node)
// for the current iteration of the loop find the matching parentId
// then take the difference
function find_pairs(current_node){
for(var i=0; i < data.length; i++){
if(data[i].parentId === current_node.parentId){
var height = Math.abs(data[i].y0 - current_node.y0)
}
}
return height
}
var verticals = []
// find root
var root = data.map(d => d.parentId === null ? d.thisId:null).filter(d => d != null)[0]
for(var i=0; i < data.length; i++){
if(data[i].thisId !== root){
verticals.push({
'thisId': data[i].thisId,
'x0': data[i].x0,
'x1': data[i].x0, // x values remain constant
'y0': data[i].y0,
'y1': data[i].y0 + find_pairs(data[i]),
'heights': find_pairs(data[i])
})

}
}

return verticals
}
Insert cell
/**
* Rectangle layout algorithm.
* Attempted copy of .layout.rect() function here https://github.com/ArtPoon/ggfree/blob/master/R/tree.R
* @param {object} node
*/

function get_horizontal(node) {
// phylodata layout...
var pd = fortify(node);
// get the names of the tips
var tips = pd.map(d => (d.isTip ? d.thisLabel : null)).filter(d => d != null);
// set y to null... not needed here.
pd.map(d => (d.y = null));
// where y corresponds to a tip, return tip number
// make y0 and y1 equal.
var tipID = 1;
for (var i = 0; i < pd.length; i++) {
if (pd[i].isTip == true) {
pd[i].y0 = tipID;
pd[i].y1 = tipID;
tipID += 1;
}
}

// probably incredibly inefficient for large trees.
// gets the y values of two child branches by looping through the whole tree...
function y_vals(child_1, child_2) {
for (var i = 0; i < pd.length; i++) {
if (pd[i].thisId === child_1) {
var y1 = pd[i].y0;
}
if (pd[i].thisId === child_2) {
var y2 = pd[i].y0;
}
}
return d3.mean([y1, y2]);
}

// if the node is not a tip...
for (var i = 0; i < pd.length; i++) {
if (pd[i].isTip === false) {
// then y0 === y1 and is the mean of the parental nodes
pd[i].y0 = y_vals(pd[i].children[0], pd[i].children[1]);
pd[i].y1 = y_vals(pd[i].children[0], pd[i].children[1]);
}
}

// find root
var root = pd
.map(d => (d.parentId === null ? d.thisId : null))
.filter(d => d != null)[0];

// sort the data temporarily in decreasing parentId
pd.sort((a, b) => b.thisId - a.thisId);

// get the branchlength of the parentID
function get_parent_branchLength(current_parentId) {
for (var i = 0; i < pd.length; i++) {
if (pd[i].thisId === current_parentId) {
var branchLength = pd[i].x1;
}
}
return branchLength;
}

// last loop...
// now get the x0 and x1 coordinates.
for (var i = 0; i < pd.length; i++) {
// special cases where parent is the root.
if (pd[i].parentId === root) {
// x0 = 0 and x1 is branch length
pd[i].x0 = 0;
pd[i].x1 = pd[i].branchLength;
} else {
// the x0 is that of the parent
var parent_branchLength = get_parent_branchLength(pd[i].parentId);
pd[i].x0 = parent_branchLength;
// the x1 is the sum of parent and current branchlength
pd[i].x1 = parent_branchLength + pd[i].branchLength;
}
}

// return the original sorted data
pd.sort((a, b) => a.thisId - b.thisId);

// remove root?

// finally get rid of unwanted y, x and angle properties
return pd;
}
Insert cell
Insert cell
radialLayout(readTree(treestring))
Insert cell
{var test = d3.create("svg")
.attr("width", 300)
.attr("height", 300)

// which one is right? match the negative or match the positive?
//-3.07329716112045, 3.141592653589793
// -0.3809338619903472, 3.013538605209775
test.append("g")
.selectAll("lines")
.data([1])
.join("path")
.attr("d", d => describeArc(150, 150, 100, -0.3809338619903472, 3.013538605209775))
.attr('fill-opacity', '0')
.attr("class", "path")
.attr("stroke", "#777")
return test.node()
}
Insert cell
function polarToCartesian(centerX, centerY, radius, angleInRadians) {
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}

Insert cell
function describeArc(x, y, radius, startAngle, endAngle){

var start = polarToCartesian(x, y, radius, startAngle);
var end = polarToCartesian(x, y, radius, endAngle);

// refers to middle zero below... but no large arcs needed?
// var largeArcFlag = endAngle - startAngle <= Math.PI ? "0" : "1";

var d = [
"M", start.x, start.y,
"A", radius, radius, 0, 0, 0, end.x, end.y
].join(" ");

return d;
}
Insert cell
function radialLayout(node){
var data = {}
data.radii = get_radii(node)
data.data = radial_data(node)
data.arcs = get_arcs(data.data)
return data
}
Insert cell
function get_arcs(pd){
// must return start of arc and end
// these are pairs of edges that have the same parent
// start is the min(angle) of the children and end is the max(angle)
// radius is the radius of the parent
// origin is 0, 0
var data = []
var start = {}
var end = {}
var radius = {}
var root = pd.map(d => d.parentId === null ? d.thisId:null).filter(d => d != null)[0]
// get the branchlength of the parentID
function sister_angle(current_parentId){
for(var i=0; i<pd.length; i++){
if(pd[i].parentId === current_parentId){
var sister_angle = pd[i].angle
}
}
return sister_angle
}
function parent_radius(current_parentId){
for(var i=0; i<pd.length; i++){
if(pd[i].thisId === current_parentId){
var parent_r = pd[i].r
}
}
return parent_r
}
for(var i=0; i<pd.length; i++){
if(pd[i].thisId !== root){
data.push({
'start': reflectAngle(d3.min([pd[i].angle, sister_angle(pd[i].parentId)]), "Y"),
'end': reflectAngle(d3.max([pd[i].angle, sister_angle(pd[i].parentId)]), "Y"),
'radius': parent_radius(pd[i].parentId),
'thisId': pd[i].thisId,
'parentId': pd[i].parentId
})
}
}
for(var i=0; i<data.length; i++){
if(Math.sign(data[i].start) !== Math.sign(data[i].end)){
data[i].end = Math.abs(data[i].end)
data[i].start = -Math.abs(data[i].start)
}
}

return data.filter(d => d.start !== d.end & d.radius !== 0)
}
Insert cell
Math.sign(100) === -1
Insert cell
// thanks https://codereview.stackexchange.com/questions/187510/angle-reflection-function
function reflectAngle(rad, dir) {
const c = Math.cos(rad), s = Math.sin(rad);
const PI_sub = "3.1415"
function check_sign(x){
if(x.toString().includes(PI_sub)){
x = Math.abs(x);
}
return x
}
return check_sign(Math.atan2(...(dir === "X" ? [s, -c] : [-s, c])));
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
/**
* Convert parsed Newick tree from fortify() into data frame of edges
* this is akin to a "phylo" object in R, where thisID and parentId
* are the $edge slot. I think.
* @param {object} tree Return value of fortify
* @return Array of Objects
*/

function edges(df, rectangular = false) {
var result = [],
parent;

// make sure data frame is sorted
df.sort(function(a, b) {
return a.thisId - b.thisId;
});

for (const row of df) {
if (row.parentId === null) {
continue; // skip the root
}
parent = df[row.parentId];
if (parent === null || parent === undefined) continue;

if (rectangular) {
var pair1 = {
x1: row.x,
y1: row.y,
id1: row.thisId,
x2: parent.x,
y2: row.y,
id2: undefined
};
result.push(pair1);
var pair2 = {
x1: parent.x,
y1: row.y,
id1: undefined,
x2: parent.x,
y2: parent.y,
id2: row.parentId
};
result.push(pair2);
} else {
var pair3 = {
x1: row.x,
y1: row.y,
id1: row.thisId,
x2: parent.x,
y2: parent.y,
id2: row.parentId
};
result.push(pair3);
}
}
return result;
}
Insert cell
Insert cell
Insert cell
Insert cell
/**
* Recursive function for post-order traversal of tree
* the root node is visited last.
* @param {object} node
* @param {Array} list An Array of nodes
* @return An Array of nodes in pre-order
*/

function postorder(node, list=[]) {
for (var i=0; i < node.children.length; i++) {
list = postorder(node.children[i], list);
}
list.push(node);
return(list);
}

Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more