Published
Edited
Jun 2, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// d3.hierarchy constructs a root node from heiarchical data. I chose localTree.root because it seemed to have the closest object structure to example from https://github.com/d3/d3-hierarchy#hierarchy

tree_root = d3.hierarchy(localTree.root)
.sum(d => d.children ? 0 : 1)
.sort((a, b) => (a.value - b.value) || d3.ascending(a.data.length, b.data.length));
Insert cell
rectCluster(tree_root)
Insert cell
setRadius(tree_root, tree_root.data._length = 0, maxLength(tree_root));
Insert cell
Insert cell
setColor(tree_root)
Insert cell
//find most recent tip

Insert cell
tree_root.links()
Insert cell
Insert cell
rect_time_tree_chart_recomb = {

var root = tree_root
var filteredTips = removeRecombinationLeaves(root);

const svg = d3.create("svg")
.attr("viewBox", [-widthMargin, -10 , width + 2*widthMargin, height + heightMargin])
.attr("font-family", "sans-serif")
.attr("font-size", 10);

svg.append("g")
.attr('transform', `translate(50, ${height-200})`)
.call(legend);

//set up colors
var baseTipColor = "#36454f"

// green color #97b366
// blue color #226b94

// controls mouseover style
svg.append("style").text(`

.recombLink--active {
stroke: #36454f !important;
stroke-width: 5px;
stroke-opacity: 1.0;

}
.link--active {
stroke: #36454f !important;
stroke-width: 5px;
stroke-opacity: 1.0;

}


.tips--active {
fill: #36454f !important;
r: 8;
}

.label--active {
font-weight: bold
opacity: 1.0;
}

`);

// #AAFF00
const link = svg.append("g")
.attr("fill", "none")
.attr("stroke", "#696969")
.attr("stroke-width", "2px")
.selectAll("path")
//.data(root.edges) //if want to include all links in single object structurer
.data(root.links())
.join("path")
.each(function(d) { d.target.linkNode = this; }) //this is used for the highlighted path
.attr("d", linkVariableHorizontal);

// color settings for recomblinks
const colorRecombLinks = d3.scaleOrdinal(d3.schemeTableau10)
.domain(recombLinks.map(d => d.gene));
//could style recombination links separately. Add color by option here.
const recombLink = svg.append("g")
.attr("fill", "none")
.attr("stroke-width", "2px")
.selectAll("path")
.data(recombLinks)
.join("path")
.each(function(d) { d.target.recombLinkNode = this; }) //to include recombination events in highlighting
.style("stroke-dasharray", ("20, 5"))
.attr("d", linkVariableHorizontal)
.attr("stroke", d => colorRecombLinks(d.gene));
var x = d3.scaleTime()
.domain([new Date(1983, 5, 1), new Date(2019, 6, 1)])
.range([ 0, width ]);
var xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom()
.scale(x)
// .tickSize(-height)
.ticks(d3.timeYear.every(10)));
xAxis.selectAll(".tick text")
.attr("font-size","30px");
// tip shapes
const tips = svg.append("g")
.selectAll("circle")
//.data(root.leaves())
.data(filteredTips)
.join("circle")
.attr("dy", ".31em")
.attr("fill", baseTipColor)
.attr("fill-opacity", 1)
.attr("transform", d => `translate(${d.radius},${d.x})`) //time scaled tree
.attr("r", 4.5);
// .on("mouseover", mouseovered(true))
// .on("mouseout", mouseovered(false));
tips
.on('mouseover', mouseovered(true))
.on('mouseout', mouseovered(false))
recombLink
.on("mouseover", function () {
d3.select(this)
.style("stroke-width", '5px')
.append('title')
.text(d => `estimated breakpoint: nuc ${d.breakpoint}`);
}).on("mouseout", function () {
d3.select(this).style("stroke-width", '2px');
});

// tip labels
const labels = svg.append("g")
.selectAll("text")
.data(filteredTips)
.join("text")
.attr("dy", ".31em")
.each(function(d) { d.label = this; })
.attr("transform", d => `translate(${d.radius + tipLabelPadding},${d.x})`) //time scaled tree
.attr("opacity", 0)
.attr("font-size","13px")
.text(d => d.data.name.replace(/_/g, " ").split(' ', 1))
.call(wrap, 2)
.on("mouseover", mouseovered(true))
.on("mouseout", mouseovered(false));

// make lineage bold on mouseover
function mouseovered(active) {
return function(event, d) {
if (active) {
link.style('stroke', "#B8B8B8").style("opacity", 1.0)
recombLink.style('stroke', "#B8B8B8").style("opacity", 1.0)
//tips.style('fill', "#AAC5D5")
tips.style('fill', "#B8B8B8")
}
else {
link.style('stroke', "#708090").style("opacity", 1.0)
recombLink.style('stroke', d => colorRecombLinks(d.gene)).style("opacity", 1.0).style('stroke-width', '2px')
tips.style('fill', baseTipColor)
}
d3.select(this).classed("tips--active", active);
d3.select(this).classed("tips--active", active);


if (active) {
d3.select(d.label).style('opacity', 1)

} else {
d3.select(d.label).style('opacity', 0)
}

do {
d3.select(d.linkNode).classed("link--active", active).raise();
//d3.select(d.recombLinkNode).classed("recombLink--active", active).raise();
if (active) {
d3.select(d.recombLinkNode).style('stroke', d => colorRecombLinks(d.gene)).style('stroke-width', '5px')
} else {
d3.select(d.recombLinkNode).style('stroke', d => colorRecombLinks(d.gene)).style('stroke-width', '2px')
}
if (d.recombParent) {
var p = d.recombParent;
do {
d3.select(p.linkNode).classed("recombLink--active", active).raise();
} while(p = p.parent);
}

}
while (d = d.parent); // follows child-parent path
};
}

return Object.assign(svg.node());
}

Insert cell
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\W+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
// x = text.attr("x"),
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em")
while (word = words.pop()) {
line.push(word)
tspan.text(line.join(" "))
if (tspan.node().getComputedTextLength() > width) {
line.pop()
tspan.text(line.join(" "))
line = [word]
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", `${++lineNumber * lineHeight + dy}em`).text(word)
}
}
})
}
Insert cell
Insert cell
// legend
legend = svg => {
const titlePadding = 20; // padding between title and entries
const entrySpacing = 30; // spacing between legend entries
const lineLength = 40; // length of legend lines
const labelOffset = 10; // additional horizontal offset of text labels
const baselineOffset = 2; // text baseline offset, depends on radius and font size
const g = svg
.selectAll("g")
.data(colorRecombLinks.domain())
.join("g")
.attr("transform", (d, i) => `translate(0, ${titlePadding + i * entrySpacing})`);

svg.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('fill', 'black')
.attr('font-family', 'Helvetica Neue, Arial')
.attr('font-weight', 'bold')
.attr('font-size', '25px')
.text('Gene');

g.append("line")
.attr("x1", 5)
.attr("x2", 5+lineLength)
.attr("stroke-width", "3px")
.style("stroke-dasharray", ("15, 5"))
.attr("stroke", colorRecombLinks);

g.append("text")
.attr("x", lineLength + labelOffset)
.attr("y", baselineOffset)
.attr("dy", "0.35em")
.attr('fill', 'black')
.attr('font-family', 'Helvetica Neue, Arial')
.attr('font-size', '25px')
.text(d => d);
}
Insert cell
/// add function to map breakpoint to Gene add this to the object and add color by in. Also add color by legend.
function mapGene(genbank, location){
var arr = []
for (var feature of genbank.parsedSequence.features) {
if (feature.type == "CDS") {
if (location + 100 >= feature.start & location + 100 <= feature.end) {
arr.push(feature.name)
}
}
}
return arr.join(', ')
}
Insert cell
function getBreakPoint(node) {
var breaks = node.data._annotations.pr[0].split("-").map(x=>+x);
if (breaks[0] == 0){
return breaks[1]
} else {
return breaks[0]
}
}
Insert cell
function removeRecombinationLeaves(root){
var results = [];
root.leaves().forEach(function(node) {
//var nodeName = node.data._id;
addLeaf(node, root, results);
});
return results
}
Insert cell
function addLeaf(node, root, results){
if (!node.data._id.includes('#')){
results.push(node)
}
}
Insert cell
// based on https://stackoverflow.com/questions/9133500/how-to-find-a-node-in-a-tree-with-javascript

function searchTree(node, matchingID){
if(node.data._id == matchingID){
return node;
} else if (node.children != null){
var i;
var result = null;
for(i=0; result == null && i < node.children.length; i++){
result = searchTree(node.children[i], matchingID);
}
return result;
}
return null;
}
Insert cell
// based on https://stackoverflow.com/questions/9133500/how-to-find-a-node-in-a-tree-with-javascript

function searchTreeRadius(node, matchRadius){
if(node.radius == matchRadius){
return node;
} else if (node.children != null){
var i;
var result = null;
for(i=0; result == null && i < node.children.length; i++){
result = searchTreeRadius(node.children[i], matchRadius);
}
return result;
}
return null;
}
Insert cell
function makeAllRecombinationEvents(root){
var results = [];
root.each(d => addRecombinationEvents (d, root, results));
return results
}
Insert cell
addRecombinationEvents = function(node, root, results){
if (node.data._id.includes('#')){
results.push(createRecombinationLink(node, root))
}
}
Insert cell
Insert cell
function createLinkObject(sourceZh, targetZh, gene, loc){
return {source: sourceZh, target: targetZh, gene: gene, breakpoint: loc};
}
Insert cell
//TODO: Decide what to color by and fix these categories, may need to extract attributes from names
color = d3.scaleOrdinal()
.domain(["United States", "China"])
.range(d3.schemeTableau10)
.unknown(null)
Insert cell
rectCluster = d3.cluster()
.size([width*0.57, height*0.57])
.separation((a, b) => 0.01)
Insert cell
// Compute the maximum cumulative length of any node in the tree.
function maxLength(d) {
return d.data._length + (d.children ? d3.max(d.children, maxLength) : 0);
}
Insert cell
// Set the radius of each node by recursively summing and scaling the distance from the root.
// this could probably also just be used to calculate x value when y is calculated some other way?

function setRadius(d, y0, k) {
d.radius = (y0 += d.data._length) * width /k;
if (d.children) d.children.forEach(d => setRadius(d, y0, k));
}
Insert cell
// Set the color of each node by recursively inheriting.
function setColor(d) {
var name = d.data._id;
d.color = color.domain().indexOf(name) >= 0 ? color(name) : d.parent ? d.parent.color : null;
if (d.children) d.children.forEach(setColor);
}
Insert cell
function linkVariableHorizontal(d) {
return linkStepHorizontal(d.source.x, d.source.radius, d.target.x, d.target.radius);
}
Insert cell
function linkStepHorizontal(startY, startRadius, endY, endRadius) {

const endYbigger = startY + ((endY-startY)/2)
const startYbigger = startY - ((endY-startY)/2)

const startRadiusNew = startRadius
return "M " + startRadius + " " + startY + " "
+ (endY === startY ? "" : "L " + startRadiusNew + " " + startY + " L " + startRadiusNew + " " + endY)
+ " L " + endRadius + " " + endY;
}
Insert cell
widthRadial = 1000
Insert cell
width = 1000
Insert cell
height = .6*width
Insert cell
outerRadius = widthRadial / 2
Insert cell
innerRadius = outerRadius - 200
Insert cell
tipLabelPadding = 10
Insert cell
widthMargin = 50
Insert cell
heightMargin = 50
Insert cell
Insert cell
Insert cell
Insert cell
ft = import("https://cdn.jsdelivr.net/gh/rambaut/figtree.js@d12cb72/dist/figtree.esm.js")
Insert cell
import {swatches} from "@d3/color-legend"
Insert cell
bioparser = require('bio-parsers@8.3.9/umd/bio-parsers.js')
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