Skip to content

Instantly share code, notes, and snippets.

@nielshanson
Last active August 29, 2015 14:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nielshanson/fdd4648e15d34a71f95f to your computer and use it in GitHub Desktop.
Save nielshanson/fdd4648e15d34a71f95f to your computer and use it in GitHub Desktop.
Bubble Tree

About the plot

This plot is a modification of the Mike Bostock's dendrogram to show the taxonomy of multiple samples. Taxonomy in metagenomics are sometimes only partially observed, thus it is often desitrable to comparte the taxonomic distribution of multiple samples at multiple levels of the tree-of-life. This visualization is dynamically expandable and contractable, and recursively calculates counts at the displayed leaves of the tree.

Data being displayed is the 16S rRNA gene distribution of William's Lake Long-term Soil Productivity (LTSP) samples taken from four different soil horizons.

This data was graciously provided by Aria Hahn, UBC Microbiology Ph.D student from the Steven J. Hallam Laboratory.

Usage

Unlike some other visualizations, the default input the Bubble Tree is a hierarchical json file, where each node is assume to have a children array. More information can be found in the Hierarchy Layout d3.js documentation.

Suffice to say this is not the typical format that for most taxnomic analysis. Here I've written a quick script matrix_to_json.py to convert a matrix with ;-separated taxonomic hierarchy as row names and samples as columns to the proper JSON format.

  • For example,
-,N-surface,C-Surface,Mid,Bt
root;cellular organisms;Bacteria;Proteobacteria;Alphaproteobacteria;Rhizobiales,24,37,12,20
root;cellular organisms;Bacteria;Proteobacteria;Alphaproteobacteria;Rhodospirillales,18,21,8,6
root;cellular organisms;Bacteria;Proteobacteria;Alphaproteobacteria;Other,4,43,0,1
root;cellular organisms;Bacteria;Proteobacteria;Betaproteobacteria;Burkholderiales,6,13,0,1

If this file was named WL_Bubble_Species.csv this would be the useage

python matrix_to_json.py -i WL_Bubble_Species.csv -o WL_Bubble_Species.json

SVG Crowbar is a great tool for extracting SVG files from d3 plots for downstream purposes.

.node circle {
cursor: pointer;
fill: #fff;
stroke: #888C93;
stroke-width: 1.5px;
}
.node text {
font-size: 10px;
font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.legend circle {
fill: none;
stroke: #ccc;
}
.legend text {
fill: #777;
font: 10px sans-serif;
text-anchor: middle;
}
.color_legend text {
fill: #777;
font: 10px sans-serif;
text-anchor: start;
}
var m = [20, 120, 20, 120],
w = 1280 - m[1] - m[3],
h = 800 - m[0] - m[2],
i = 0,
root;
var tree = d3.layout.cluster()
.size([h, w])
.separation(function separation(a, b) {
return a.parent == b.parent ? 1 : 1;
});
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var vis = d3.select("#figure").append("svg:svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
// bubble plot variables
var plot_offset = 300;
var radius = null;
var samples = 4;
var sample_names = [];
var trans = 0.5;
var colors = ["red", "blue", "green", "orange", "purple" ];
// pie stuff
var arc = d3.svg.arc()
.outerRadius(radius * 0.8)
.innerRadius(radius * 0.4);
var outerArc = d3.svg.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.value;
});
// quick crawl of tree to load sample_names
// variable
function initializeSampleNames(d){
if(d.values) {
if (sample_names.length == 0) {
for (var sample_name in d.values) {
sample_names.push(sample_name);
}
}
}
if (d.children) {
for (var i=0; i < d.children.length; i++) {
initializeSampleNames(d.children[i]);
}
} else if (d._children) {
for (var i=0; i < d._children.length; i++) {
initializeSampleNames(d._children[i]);
}
}
}
d3.json("https://cdn.rawgit.com/nielshanson/d3/master/data/WL_Bubble_Species.json", function(json) {
root = json;
root.x0 = h / 2;
root.y0 = 0;
// set full node status of all nodes
function toggleAll(d) {
if (d.children) {
d.children.forEach(toggleAll);
toggle(d);
}
}
// sets leaf status of all nodes to false
initializeLeaves(root);
root._leaf = true;
// Initialize the display to show a few nodes.
root.children.forEach(toggleAll);
initializeSampleNames(root);
// Set max bubble size to max samples
max_size = 0
for(var i=0; i < sample_names.length; i++){
var sample_sum = sumCountsBySample2(root, sample_names[i]);
if (sample_sum > max_size) {
max_size = sample_sum;
}
}
radius = d3.scale.sqrt()
.domain([0, max_size])
.range([0, 30]);
var legend = vis.append("g")
.attr("class", "legend")
.attr("transform", "translate(" + (0) + "," + (50) + ")")
.selectAll("g")
.data([Math.round(max_size), Math.round(max_size/2), Math.round(max_size/4)])
.enter().append("g");
legend.append("circle")
.attr("cy", function(d) { return -radius(d); })
.attr("r", radius);
legend.append("text")
.attr("y", function(d) { return -2 * radius(d); })
.attr("dy", "1.2em")
.text(d3.format(".1s"));
var color_legend = vis.append("g")
.attr("class", "legend")
.attr("transform", "translate(" + (0) + "," + (70) + ")")
.selectAll("g")
.data(sample_names)
.enter().append("g");
color_legend.append("circle")
.attr("cy", function(d,i) { return i*20; })
.attr("r", 8)
.style("fill", function(d,i) {
return colors[i];
})
.attr("opacity", trans);
color_legend.append("text")
.attr("y", function(d,i) { return i*20; })
.attr("dy", 4)
.attr("x", 40)
.text(function(d,i) {
return d
});
toggle(root);
update(root);
});
function getChildrenNames(parent) {
children = parent.children;
for (i = 0; i < children.length; i++) {
// alert(children[i]["size"]);
}
}
function update(source) {
var duration = d3.event && d3.event.altKey ? 5000 : 500;
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse();
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 90; });
// Update the nodes…
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
//.on("click", function(d) { toggle(d); update(d); });
.on("click", function(d) { update_leaf(d); toggle(d); update(d); });
nodeEnter.append("svg:circle")
.attr("r", 1e-6)
.attr("class", "node_circle")
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
//create_slices(nodeEnter);
nodeEnter.append("svg:text")
.attr("x", function(d) { return d.children || d._children ? -10 : -10; })
.attr("y", 12)
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "end"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);
// circle that appears when leaf
create_leaf_nodes(nodeEnter);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
// update leaf nodes with colors
update_leaf_nodes(nodeUpdate);
nodeUpdate.select("circle.node_circle")
.attr("r", function(d) {return 6;} )
.style("fill", function(d) { return d._children ? "#CECECE" : "#fff"; });
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
nodeExit.select(".leaf")
.style("r", 1e-6)
.attr("opacity", 0);
// Update the links…
var link = vis.selectAll("path.link")
.data(tree.links(nodes), function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert("svg:path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
})
.transition()
.duration(duration)
.attr("d", diagonal);
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Toggle children.
function toggle(d) {
if (d.children) {
// hide children
d._children = d.children;
d.children = null;
} else {
// show children
d.children = d._children;
d._children = null;
}
}
// Update leaf status of children on click
function set_children_leaves(d) {
// update leaf status
if (d.children) {
children = d.children;
} else if (d._children) {
children = d._children;
}
for (i = 0; i < children.length; i++) {
// alert("leaf " + children[i]["name"]);
children[i]._leaf = true;
}
}
function update_leaf(d) {
if (d._leaf) {
// if currently a leaf, expand and make children leaves
d._leaf = false;
if(d.children) {
d.children.forEach(function(d){ d._leaf = true;});
} else if (d._children){
d._children.forEach(function(d){ d._leaf = true;});
}
} else {
// if not a leaf, collapsing up traverse all setting leaves to false
d._leaf = true;
if(d.children) {
d.children.forEach(initializeLeaves);
} else if (d._children){
d._children.forEach(initializeLeaves);
}
}
}
// sets current leaf and all children leaves to false
function initializeLeaves(d) {
d._leaf = false;
if (d.children) {
d.children.forEach(initializeLeaves);
} else if (d._children) {
d._children.forEach(initializeLeaves);
}
}
function sumCountsBySample2(d, sample){
if (d.children) {
var sum = 0;
for (var i=0; i < d.children.length; i++) {
var value = sumCountsBySample2(d.children[i], sample);
sum += value;
}
if(d.values) {
var value = parseFloat(d.values[sample]);
sum += value;
}
return(sum);
} else if (d._children) {
var sum = 0;
for (var i=0; i < d._children.length; i++) {
var value = sumCountsBySample2(d._children[i], sample);
sum += value;
}
if(d.values) {
var value = parseFloat(d.values[sample]);
sum += value;
}
return(sum);
} else {
if(d.values) {
var value = parseFloat(d.values[sample]);
return(value);
}
}
}
function hideChildren(d) {
if(d.children) {
d._children = d.children;
d.children.forEach(hideChildren)
d.children = null;
}
}
// Toggle leaf status of node
function toggle_leaf(d) {
if (d._leaf){
d._leaf = false;
} else {
d._leaf = true;
}
}
function create_leaf_nodes(nodeEnter) {
for (itr2 = 0; itr2 < samples; itr2++) {
nodeEnter.append("svg:circle")
.attr("class", "leaf")
.attr("cx", (itr2 + 1) * 30)
.attr("r", 1e-6)
.attr("opacity", 0)
.style("fill", function(d) { return d._leaf ? colors[itr2] : "#fff"; });
}
console.log()
}
function update_leaf_nodes(nodeUpdate) {
console.log("In update_leaf_nodes");
leaf_circles = nodeUpdate.selectAll("circle.leaf")
.transition()
.attr("r", function(d,i) {
//alert(sample_names[i]);
value = sumCountsBySample2(d,sample_names[i]);
return(radius(value));
})
.attr("opacity", function(d,i) {
if (d._leaf) {
return(trans);
} else {
return(0);
}
});
}
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div class="container">
<div id="figure"></div>
</div>
<link href="bubble_tree.css" type="text/css" rel="stylesheet"/>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<script src="bubble_tree.js"></script>
<script type="text/javascript">
d3.select(self.frameElement).style("height", "850px");
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment