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/621017cdd84eb8afd2a3 to your computer and use it in GitHub Desktop.
Save nielshanson/621017cdd84eb8afd2a3 to your computer and use it in GitHub Desktop.
Bubble Plot

About the plot

Re-orderable bubble plot with one-sided marginal distribution. In the field of Metagenomics Bubble Plots are popular for displaying the taxonomic distribution of a microbial community. This work is heavily inspired heavily by the Les Misérable Co-occurence example designed by Mike Bostock. In this case the csv-matrix can be displayed in its original row-order, or by the row averages. Row averages are being displayed in the margin.

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

Data graciously provided by Mrs. Aria Hahn, UBC Microbiology Ph.D student from the Steven J. Hallam Laboratory.

Usage

A simple comma-separated value file with row-names and column-names is sufficient.

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

// function to pick colors of bubbles
function colorPicker(d) {
return "rgb(0, 0, 0)";
}
// comparator function for sorting numeric arrays
function sortNumber(a,b) {
return a - b;
}
// input parameters
var filename = "https://cdn.rawgit.com/nielshanson/d3/master/data/WL_Species.csv";
var orders_bars = {
hallam: new Array(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
abundance: new Array(36,108,108,264,348,288,300,300,264,132,156,432,24,300,504,252,492,-192,480,-84,24,-144,48,-96,-72,-228,-156,-324,-276,-96,-168,-348,-180,-312,60,60,-336,0,-228,-144,-432,120,204,12,12,180,-144,-276,0,-84,24,-60,-36,0,0,-144,-72,-264,-36,-216,-384,0)
};
var orders_rows = {
hallam: new Array(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68),
abundance: new Array(3,10,11,25,33,29,31,32,30,20,23,47,14,38,56,36,57,1,58,12,22,9,26,15,18,6,13,0,5,21,16,2,17,7,39,40,8,37,19,27,4,51,59,44,45,60,34,24,48,42,52,46,49,53,54,43,50,35,55,41,28,61,59,64,60,42,66,37,62)
};
// size parameters
var h_spacing = 20; // horizontal spacing
var v_spacing = 12; // vertical spacing
var margin = {top: 100, right: 160, bottom: 20, left: 160};
var circle_factor = 0.75; //radius of circles relative to data point magnitude
var circle_opacity = 0.70;
var x = d3.scale.ordinal().rangeBands([0, 300]);
d3.text(filename, function(datasetText) {
var parsedCSV = d3.csv.parseRows(datasetText);
// parse out x and y axis labels
var xlab = parsedCSV[0].slice(1,parsedCSV[0].length);
var ylab = [];
for(var i = 1; i < parsedCSV.length; i++){
var temp = parsedCSV[i];
ylab = ylab.concat(temp[0]);
}
// parse out the datamatrix
var data_matrix = [];
for(var i = 1; i < parsedCSV.length; i++){
var temp = parsedCSV[i].slice(1,parsedCSV[i].length );
data_matrix = data_matrix.concat([temp]);
}
// calculate marginal sums
var sum_rows = []; // sum of the rows
for(var i = 0; i < data_matrix.length; i++){
var temp_sum = 0;
for(var j = 0; j < data_matrix[i].length; j++){
temp_sum = parseFloat(temp_sum) + parseFloat(data_matrix[i][j]);
}
sum_rows = sum_rows.concat([temp_sum]);
}
original_ranks = [];
for(var i = 0; i < data_matrix.length; i++) {
original_ranks[i] = i+1;
}
sum_rows_sorted = sum_rows.slice();
sum_rows_sorted.sort(sortNumber);
sum_rows_sorted.reverse();
ranks = sum_rows.slice().map(function(v){ return sum_rows_sorted.indexOf(v)+1 });
// shift ranks
already_seen = [];
for (var i = 0; i < ranks.length; i++) {
if (already_seen[ranks[i]] == undefined) {
already_seen[ranks[i]] = ranks[i];
} else {
var seen_val = ranks[i]
while (already_seen[seen_val] != undefined) {
seen_val += 1;
}
ranks[i] = seen_val;
already_seen[seen_val] = seen_val;
}
}
delta_ranks = [];
delta_points = [];
for (var i = 0; i < data_matrix.length; i++) {
delta_ranks[i] = (ranks[i] - original_ranks[i]);
delta_points[i] = (ranks[i] - original_ranks[i]) * 12;
}
// shift ranks down by one
for (var i = 0; i < ranks.length; i++) {
ranks[i] = ranks[i] - 1;
}
orders_rows['abundance'] = ranks;
orders_bars['abundance'] = delta_points;
var sum_col = []; // sum of the columns
for(var i = 0; i < data_matrix[0].length; i++){
var temp_sum = 0;
for(var j = 0; j < data_matrix.length; j++){
temp_sum = parseFloat(temp_sum) + parseFloat(data_matrix[j][i]);
}
sum_col = sum_col.concat([temp_sum]);
}
// find marginal averages
var avg_rows = []; // avg of columns
for(var i = 0; i < data_matrix.length; i++){
var temp_sum = 0;
for(var j = 0; j < data_matrix[i].length; j++){
temp_sum = parseFloat(temp_sum) + parseFloat(data_matrix[i][j]);
}
avg_rows = avg_rows.concat([temp_sum]/xlab.length);
}
var avg_col = []; // avg of columns
for(var i = 0; i < data_matrix[0].length; i++){
var temp_sum = 0;
for(var j = 0; j < data_matrix.length; j++){
temp_sum = parseFloat(temp_sum) + parseFloat(data_matrix[j][i]);
}
avg_col = avg_col.concat([temp_sum]/ylab.length);
}
var width = data_matrix[1].length*h_spacing;
var height = data_matrix.length*v_spacing;
var svg = d3.select("#table").append("svg");
svg.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.selectAll("g")
.data(data_matrix)
.enter()
.append("g")
.attr("class", "row")
.attr("transform", function(d, i){ return "translate(0," + (i*v_spacing) + ")"; })
.selectAll("circle")
.data(function(d){return d;})
.enter().append("circle")
.attr("fill", function(d) {
return colorPicker(d);
})
.attr("r", function(d) {
return Math.sqrt(d*circle_factor);
})
.attr("cx", function(d,i) {
return (i * h_spacing) + 6;
})
.attr("cy", 6)
.attr("opacity", circle_opacity)
.on("mouseover", function(){
d3.select(this).attr("fill", "#F77E1C");
var temp = d3.select(this).attr("alt");
temp = Math.round(temp*100)/100
tooltip.show("<strong>" + temp + "</strong>");
})
.on("mouseout", function(){
d3.select(this).attr("fill", function(d) {
tooltip.hide();
return colorPicker(d);
})
})
.attr("alt", function(d) {
return d;
})
// axis
var xScale = d3.scale.ordinal()
.domain(xlab)
.rangePoints([0+5, width-14]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("right")
var yScale = d3.scale.ordinal()
.domain(ylab)
.rangePoints([0+4, height-4]);
var yAxis = d3.svg.axis()
.scale(yScale)
.tickSize(0)
.orient("left")
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate("+ margin.left + "," + (margin.top) + ") rotate(270)")
.call(xAxis);
svg.append("g")
.attr("class", "axis")
.attr("id","leftwords")
.attr("transform", "translate("+ (margin.left -2) + "," + margin.top + ") rotate(0)")
.call(yAxis);
var col_hist_size = 0.5* margin.top;
var col_hist_scale = d3.scale.linear()
.domain([0, d3.max(avg_col)])
.range([0, col_hist_size]);
// upper marginal axis line
// row histogram
var row_hist_size = 0.8* margin.right;
var row_hist_scale = d3.scale.linear()
.domain([0, d3.max(avg_rows)])
.range([0, row_hist_size]);
// right hand side marginal bars
svg.append("g")
.attr("transform", "translate(" + (width + margin.right) + "," + margin.top + ")")
.selectAll("rect")
.data(sum_rows)
.enter()
.append("rect")
.attr("x", function(d, i) {
return 0; // spacing of 5
})
.attr("y", function(d, i) {
return i * 12; //Bar width of 12 plus 1 for padding
})
.attr("width", function(d,i) {
return row_hist_scale(avg_rows[i]);
})
.attr("fill","rgb(54, 144, 192)")
.attr("height", 10)
.attr("class", "rowright");
// right marginal text labels
svg.append("g")
.attr("transform", "translate(" + (width + margin.right) + "," + margin.top + ")")
.selectAll("text")
.data(avg_rows)
.enter()
.append("text")
.text(function(d) {
return Math.round(d*100)/100;
})
.attr("x", function(d, i) {
return row_hist_scale(d) + 5;
})
.attr("y", function(d, i) {
return i * 12 + 8;
})
.attr("font-family", "sans-serif")
.attr("font-size", "8px")
.attr("class", "righttext")
.attr("fill", "black");
// legend text
var legend = [4, 8, 16, 32];
// legend color boxes
svg.append("g")
.attr("transform", "translate(20,20)")
.selectAll("circle")
.data(legend)
.enter()
.append("circle")
.attr("cx", function(d, i) {
return i * (h_spacing + 12); //h_spacing plus padding
})
.attr("cy", function(d) {
return 30;
})
.attr("r", function(d){
return Math.sqrt(d*circle_factor);
})
.attr("fill",function(d){
return colorPicker(d);
})
.attr("height", 10);
// legend text
svg.append("g")
.attr("transform", "translate(20,45)")
.selectAll("text")
.data(legend)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("x", function(d, i) {
return i * (h_spacing+12);
})
.attr("y", function(d, i) {
return 25;
})
.attr("font-family", "sans-serif")
.attr("font-size", "8px")
.attr("fill", "black")
.attr("text-anchor", "middle");
svg.append("text")
.attr("transform", "translate(20,25)")
.text("Legend:")
.attr("font-family", "sans-serif")
.attr("font-size", "12px");
});
function animate(value) {
var rows = orders_rows[value];
var bars = orders_bars[value];
d3.selectAll(".row").transition()
.duration(2500)
.attr("transform", function(d,i){
return " translate(0," + rows[i]*12 + " )";
});
d3.selectAll(".rowright").transition()
.duration(2500)
.attr("transform", function(d,i){
return " translate(0," + bars[i] + " )";
});
d3.selectAll(".righttext").transition()
.duration(2500)
.attr("transform", function(d,i){
return " translate(0," + bars[i] + " )";
});
d3.selectAll("#leftwords > g").transition()
.duration(2500)
.attr("transform", function(d,i){
return " translate(0," + ((rows[i]*12) + 5) + " )";
});
};
d3.select("#order").on("change", function() {
animate(this.value);
});
/* Bubble-plot styles */
body {
}
.combo_box {
font-family: Arial,Helvetica Neue,Helvetica,sans-serif;
font-size: 13px;
margin: 20px
}
.main {
width:500px;
margin-left: auto ;
margin-right: auto ;
}
div.bar {
display: inline-block;
width: 20px;
height: 75px; /* We'll override this later */
background-color: teal;
}
.background {
fill: #eee;
}
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
}
<html>
<head>
<title>Sortable Bubble-plot</title>
<!--<script type="text/javascript" src="test.js"></script> -->
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<link rel="stylesheet" type="text/css" href="bubble_style.css" />
<script type="text/javascript" language="javascript" src="tooltip.js"></script>
</head>
<body>
<div class="main">
<div class="combo_box">Sorting:
<select id="order">
<option value="hallam">Original</option>
<option value="abundance">Row Average</option>
</select>
</div>
<div id="table">
</div>
</div>
<script type="text/javascript">
d3.select(self.frameElement).style("height", "920px");
</script>
<script src="bubble_plot.js"></script>
</body>
</html>
function pw() {return window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth}; function mouseX(evt) {return evt.clientX ? evt.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft) : evt.pageX;} function mouseY(evt) {return evt.clientY ? evt.clientY + (document.documentElement.scrollTop || document.body.scrollTop) : evt.pageY} function popUp(evt,oi) {if (document.getElementById) {var wp = pw(); dm = document.getElementById(oi); ds = dm.style; st = ds.visibility; if (dm.offsetWidth) ew = dm.offsetWidth; else if (dm.clip.width) ew = dm.clip.width; if (st == "visible" || st == "show") { ds.visibility = "hidden"; } else {tv = mouseY(evt) + 20; lv = mouseX(evt) - (ew/4); if (lv < 2) lv = 2; else if (lv + ew > wp) lv -= ew/2; lv += 'px';tv += 'px'; ds.left = lv; ds.top = tv; ds.visibility = "visible";}}}
var tooltip=function(){
var id = 'tt';
var top = 3;
var left = 3;
var maxw = 300;
var speed = 10;
var timer = 20;
var endalpha = 95;
var alpha = 0;
var tt,t,c,b,h;
var ie = document.all ? true : false;
return{
show:function(v,w){
if(tt == null){
tt = document.createElement('div');
tt.setAttribute('id',id);
t = document.createElement('div');
t.setAttribute('id',id + 'top');
c = document.createElement('div');
c.setAttribute('id',id + 'cont');
b = document.createElement('div');
b.setAttribute('id',id + 'bot');
tt.appendChild(t);
tt.appendChild(c);
tt.appendChild(b);
document.body.appendChild(tt);
tt.style.opacity = 0;
tt.style.filter = 'alpha(opacity=0)';
document.onmousemove = this.pos;
}
tt.style.display = 'block';
c.innerHTML = v;
tt.style.width = w ? w + 'px' : 'auto';
if(!w && ie){
t.style.display = 'none';
b.style.display = 'none';
tt.style.width = tt.offsetWidth;
t.style.display = 'block';
b.style.display = 'block';
}
if(tt.offsetWidth > maxw){tt.style.width = maxw + 'px'}
h = parseInt(tt.offsetHeight) + top;
clearInterval(tt.timer);
tt.timer = setInterval(function(){tooltip.fade(1)},timer);
},
pos:function(e){
var u = ie ? event.clientY + document.documentElement.scrollTop : e.pageY;
var l = ie ? event.clientX + document.documentElement.scrollLeft : e.pageX;
tt.style.top = (u - h) + 'px';
tt.style.left = (l + left) + 'px';
},
fade:function(d){
var a = alpha;
if((a != endalpha && d == 1) || (a != 0 && d == -1)){
var i = speed;
if(endalpha - a < speed && d == 1){
i = endalpha - a;
}else if(alpha < speed && d == -1){
i = a;
}
alpha = a + (i * d);
tt.style.opacity = alpha * .01;
tt.style.filter = 'alpha(opacity=' + alpha + ')';
}else{
clearInterval(tt.timer);
if(d == -1){tt.style.display = 'none'}
}
},
hide:function(){
clearInterval(tt.timer);
tt.timer = setInterval(function(){tooltip.fade(-1)},timer);
}
};
}();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment