//////////////////////////////////////////////////////////// //////////////////////// Set-up //////////////////////////// //////////////////////////////////////////////////////////// var screenWidth = $(window).innerWidth(), mobileScreen = (screenWidth > 500 ? false : true); var margin = {left: 50, top: 10, right: 50, bottom: 10}, width = Math.min(screenWidth, 800) - margin.left - margin.right, height = (mobileScreen ? 300 : Math.min(screenWidth, 800)*5/6) - margin.top - margin.bottom; var svg = d3.select("#chart").append("svg") .attr("width", (width + margin.left + margin.right)) .attr("height", (height + margin.top + margin.bottom)); var wrapper = svg.append("g").attr("class", "chordWrapper") .attr("transform", "translate(" + (width / 2 + margin.left) + "," + (height / 1.75 + margin.top) + ")");; var outerRadius = Math.min(width, height) / 2 - (mobileScreen ? 80 : 100), innerRadius = outerRadius * 0.95, opacityDefault = 0.7, //default opacity of chords opacityLow = 0.02; //hover opacity of those chords not hovered over //How many pixels should the two halves be pulled apart var pullOutSize = (mobileScreen? 20 : 50) ////////////////////////////////////////////////////// //////////////////// Titles on top /////////////////// ////////////////////////////////////////////////////// var titleWrapper = svg.append("g").attr("class", "chordTitleWrapper"), titleOffset = mobileScreen ? 15 : 40, titleSeparate = mobileScreen ? 30 : 0; //Title top left titleWrapper.append("text") .attr("class","title left") .style("font-size", mobileScreen ? "12px" : "30px" ) .attr("x", (width/2 + margin.left - outerRadius - titleSeparate)) .attr("y", titleOffset) .text("Copepod"); titleWrapper.append("line") .attr("class","titleLine left") .attr("x1", (width/2 + margin.left - outerRadius - titleSeparate)*0.6) .attr("x2", (width/2 + margin.left - outerRadius - titleSeparate)*1.4) .attr("y1", titleOffset+8) .attr("y2", titleOffset+8); //Title top right titleWrapper.append("text") .attr("class","title right") .style("font-size", mobileScreen ? "12px" : "30px" ) .attr("x", (width/2 + margin.left + outerRadius + titleSeparate)) .attr("y", titleOffset) .text("Similarity"); titleWrapper.append("line") .attr("class","titleLine right") .attr("x1", (width/2 + margin.left - outerRadius - titleSeparate)*0.6 + 2*(outerRadius + titleSeparate)) .attr("x2", (width/2 + margin.left - outerRadius - titleSeparate)*1.4 + 2*(outerRadius + titleSeparate)) .attr("y1", titleOffset+8) .attr("y2", titleOffset+8); //////////////////////////////////////////////////////////// /////////////////// Animated gradient ////////////////////// //////////////////////////////////////////////////////////// var defs = wrapper.append("defs"); var linearGradient = defs.append("linearGradient") .attr("id","animatedGradient") .attr("x1","0%") .attr("y1","0%") .attr("x2","100%") .attr("y2","0") .attr("spreadMethod", "reflect"); linearGradient.append("animate") .attr("attributeName","x1") .attr("values","0%;100%") // .attr("from","0%") // .attr("to","100%") .attr("dur","7s") .attr("repeatCount","indefinite"); linearGradient.append("animate") .attr("attributeName","x2") .attr("values","100%;200%") // .attr("from","100%") // .attr("to","200%") .attr("dur","7s") .attr("repeatCount","indefinite"); linearGradient.append("stop") .attr("offset","5%") .attr("stop-color","#a551ff"); linearGradient.append("stop") .attr("offset","45%") .attr("stop-color","#6d00c6"); linearGradient.append("stop") .attr("offset","55%") .attr("stop-color","#6d00c6"); linearGradient.append("stop") .attr("offset","95%") .attr("stop-color","#a551ff"); //////////////////////////////////////////////////////////// ////////////////////////// Data //////////////////////////// //////////////////////////////////////////////////////////// var Names = ["High","Moderate","Low","", "a62032;8:g129.t1","a181091;6:g176.t1", "a123900;7:g103.t1","a101529;8:g218.t1","a52440;9:g147.t1","a69769;9:g66.t1","a44833;11:g29.t1","a18604;33:g5.t1","a62666;11:g270.t1","a171373;7:g183.t1","a110923;7:g272.t1","a139729;7:g99.t1","a100325;8:g116.t1","a156879;7:g122.t1","a185333;7:g206.t1","a68619;8:g91.t1","a190650;6:g235.t1","a109509;7:g234.t1","a75038;8:g287.t1","a178293;6:g100.t1","a59106;10:g106.t1","a68702;9:g25.t1","a60722;8:g150.t1",""]; var Nematodes =[ "AgB14_g001_t01","AgE05_g008_t17","AgE04_g004_t05","AgE77_g001_t01","AgE54_g003_t03","AgE09_g003_t02","AgE47_g005_t02","AgB03_g007_t04","AgB04_g462_t03","AgE11_g008_t06","AgE09_g002_t06","AgE66_g002_t01","AgE07_g013_t01","AgE02_g006_t11","AgE13_g008_t01","AgE05_g005_t04","AgE76_g001_t11","AgB19_g001_t04","AgB29_g009_t10","AgE49_g002_t01","AgB21_g006_t03","AgE22_g001_t03","AgE14_g010_t04","AgE46_g003_t05","AgB28X_g018_t01","AgE10_g003_t13","AgE01_g016_t01","AgE06_g024_t02","AgE01_g026_t01","AgE01_g027_t07","AgB28X_g013_t01","AgE02_g014_t04","AgB03_g007_t04","AgE52_g004_t02","AgB23_g079_t04","AgB28X_g023_t08","AgE01_g004_t02","AgE14_g014_t06","AgB17_g092_t02","AgE19_g001_t02","AgE85_g001_t01","AgB31X_g003_t01","AgE02_g034_t08","AgE01_g027_t07","AgE03_g025_t01","AgE06_g022_t01","AgE03_g022_t01","AgB24_g004_t05","AgB23_g072_t01","AgE11_g006_t01","AgE01_g047_t02","AgE44_g001_t02","AgE22_g001_t03","AgB31X_g004_t01","AgE07_g004_t04","AgE01_g054_t02","AgB24_g005_t05","AgE02_g034_t08","AgE25_g003_t04","AgE03_g018_t01","AgE42_g003_t02","AgE01_g016_t01","AgB34_g004_t01","AgE54_g001_t01","AgE01_g037_t02","AgE06_g023_t02","AgE70_g002_t04","AgE06_g007_t07","AgE04_g007_t14" ]; var respondents = 3290.3, //Total number of respondents (i.e. the number that make up the total group emptyPerc = 0.45, //What % of the circle should become empty emptyStroke = Math.round(respondents*emptyPerc); var matrix = [ [0,0,0,0,88.9,87.6,85.7,85.7,80,80,80,80,80,70,70,70,71.4,71.4,72.7,72.7,75,75,75,75,75,77.8,77.8,0], [0,0,0,0,63.6,87.5,77.8,50,40,66.7,45,47.8,47,66.7,46.2,50,60,45,52.9,60,37,58.4,40,46.7,53.8,35.3,37.5,0], [0,0,0,0,50,26.3,42,43.8,30.8,66.6,34.9,32.3,46.1,61.5,38.1,49.4,50,43.7,38.5,50,36,58.3,33.3,34.8,36.3,35,21.9,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,emptyStroke], [88.9,63.6,50,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], [87.6,87.5,26.3,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], [85.7,77.8,42,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], [85.7,50,43.8,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], [80,40,30.8,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], [80,66.7,66.6,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], [80,45,34.9,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], [80,47.8,32.3,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], [80,47,46.1,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], [70,66.7,61.5,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], [70,46.2,38.1,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], [70,50,49.4,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], [71.4,60,50,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], [71.4,45,43.7,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], [72.7,52.9,38.5,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], [72.7,60,50,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], [75,37,36,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], [75,58.4,58.3,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], [75,40,33.3,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], [75,46.7,34.8,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], [75,53.8,36.3,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], [77.8,35.3,35,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], [77.8,37.5,21.9,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,emptyStroke,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] ]; //Calculate how far the Chord Diagram needs to be rotated clockwise to make the dummy //invisible chord center vertically var offset = (2 * Math.PI) * (emptyStroke/(respondents + emptyStroke))/7.5; //Custom sort function of the chords to keep them in the original order var chord = customChordLayout() //d3.layout.chord() .padding(.02) .sortChords(d3.descending) //which chord should be shown on top when chords cross. Now the biggest chord is at the bottom .matrix(matrix); var arc = d3.svg.arc() .innerRadius(innerRadius) .outerRadius(outerRadius) .startAngle(startAngle) //startAngle and endAngle now include the offset in degrees .endAngle(endAngle); var path = stretchedChord() //Call the stretched chord function .radius(innerRadius) .startAngle(startAngle) .endAngle(endAngle) .pullOutSize(pullOutSize); //////////////////////////////////////////////////////////// //////////////////// Draw outer Arcs /////////////////////// //////////////////////////////////////////////////////////// var g = wrapper.selectAll("g.group") .data(chord.groups) .enter().append("g") .attr("class", "group") .on("mouseover", fade(opacityLow)) .on("mouseout", fade(opacityDefault)); g.append("path") .style("stroke", function(d,i) { return (Names[i] === "" ? "none" : "#fff605"); }) .style("fill", function(d,i) { return (Names[i] === "" ? "none" : "#fff605"); }) .style("pointer-events", function(d,i) { return (Names[i] === "" ? "none" : "auto"); }) .attr("d", arc) .attr("transform", function(d, i) { //Pull the two slices apart d.pullOutSize = pullOutSize * ( d.startAngle + 0.001 > Math.PI ? -1 : 1); return "translate(" + d.pullOutSize + ',' + 0 + ")"; }); //////////////////////////////////////////////////////////// ////////////////////// Append Names //////////////////////// //////////////////////////////////////////////////////////// //The text also needs to be displaced in the horizontal directions //And also rotated with the offset in the clockwise direction g.append("text") .each(function(d) { d.angle = ((d.startAngle + d.endAngle) / 2) + offset;}) .attr("dy", ".35em") .attr("class", "titles") .style("font-size", mobileScreen ? "8px" : "8px" ) .attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; }) .attr("transform", function(d,i) { var c = arc.centroid(d); return "translate(" + (c[0] + d.pullOutSize) + "," + c[1] + ")" + "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" + "translate(" + 20 + ",0)" + (d.angle > Math.PI ? "rotate(180)" : "") }) .text(function(d,i) { return Names[i]; }) .call(wrapChord, 100); //////////////////////////////////////////////////////////// //////////////////// Draw inner chords ///////////////////// //////////////////////////////////////////////////////////// wrapper.selectAll("path.chord") .data(chord.chords) .enter().append("path") .attr("class", "chord") .style("stroke", "none") .style("fill", "url(#animatedGradient)") //An SVG Gradient to give the impression of a flow from left to right .style("opacity", function(d) { return (Names[d.source.index] === "" ? 0 : opacityDefault); }) //Make the dummy strokes have a zero opacity (invisible) .style("pointer-events", function(d,i) { return (Names[d.source.index] === "" ? "none" : "auto"); }) //Remove pointer events from dummy strokes .attr("d", path) .on("mouseover", fadeOnChord) .on("mouseout", fade(opacityDefault)) .append("title") .text(function(d, i) { console.log("i:", i); console.log(Names.length, Nematodes.length); if (d.source.index === "High") { return [Math.round(d.source.value), "% Identity between ", Names[d.target.index], " and ", Nematodes[((i-4)*3)]].join(""); } else if (d.source.index === "Moderate") { return [Math.round(d.source.value), "% Identity between ", Names[d.target.index], " and ", Nematodes[((i-4)*3)+1]].join(""); } else { return [Math.round(d.source.value), "% Identity between ", Names[d.target.index], " and ", Nematodes[((i-4)*3)+2]].join(""); } //return [Math.round(d.source.value), "% Identity between ", Names[d.target.index], " and ", Nematodes[d.source.index]].join(""); }); //////////////////////////////////////////////////////////// ////////////////// Extra Functions ///////////////////////// //////////////////////////////////////////////////////////// //Include the offset in de start and end angle to rotate the Chord diagram clockwise function startAngle(d) { return d.startAngle + offset; } function endAngle(d) { return d.endAngle + offset; } // Returns an event handler for fading a given chord group function fade(opacity) { return function(d, i) { wrapper.selectAll("path.chord") .filter(function(d) { return d.source.index !== i && d.target.index !== i && Names[d.source.index] !== ""; }) .transition() .style("opacity", opacity); }; }//fade // Fade function when hovering over chord function fadeOnChord(d) { var chosen = d; wrapper.selectAll("path.chord") .transition() .style("opacity", function(d) { return d.source.index === chosen.source.index && d.target.index === chosen.target.index ? opacityDefault : opacityLow; }); }//fadeOnChord /*Taken from http://bl.ocks.org/mbostock/7555321 //Wraps SVG text*/ function wrapChord(text, width) { text.each(function() { var text = d3.select(this), words = text.text().split(/\s+/).reverse(), word, line = [], lineNumber = 0, lineHeight = 1.1, // ems y = 0, x = 0, dy = parseFloat(text.attr("dy")), tspan = text.text(null).append("tspan").attr("x", x).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", x).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word); } } }); }//wrapChord