Skip to content

Instantly share code, notes, and snippets.

@baderone
Last active July 6, 2016 09:29
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 baderone/254205ac408d0b5ec581a8f83610dcd5 to your computer and use it in GitHub Desktop.
Save baderone/254205ac408d0b5ec581a8f83610dcd5 to your computer and use it in GitHub Desktop.
Responsive Chord
<!DOCTYPE html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<meta charset="utf-8">
<!-- affinity group data, circle rotation, auto-resize svg image, chord function, configuration -->
<title>Chord Diagram - Nepal migration visualization</title>
<style>
#visual {
font: 14px sans-serif;
}
.chord path {
fill-opacity: .67;
stroke: #000;
stroke-width: .5px;
}
@media only screen and (min-device-width: 320px) and (max-device-width: 568px) {
#visual {
-webkit-user-select: none;
font-size: 1.2em;
}
}
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) {
#visual {
-webkit-user-select: none;
}
}
html {
position: relative;
min-height: 100%;
}
body {
/* Margin bottom by footer height */
margin-bottom: 60px;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
/* Set the fixed height of the footer here */
height: 60px;
background-color: #f5f5f5;
}
#title {
position: absolute;
top: 0;
z-index: 1;
display: block;
font-size: 22px;
font-weight: bold;
text-shadow: 0 1px 0 #fff;
top: 30px;
left: 30px;
text-align: left;
}
#owner{
position: absolute;
z-index: 1;
display: block;
font-weight: 300;
text-shadow: 0 1px 0 #fff;
top: 900px;
left: 30px;
text-align: left;
color: #474747;
padding-bottom: 20px;
font-size: 10px;
}
#SideText{
position: absolute;
display: block;
font-size: 12px;
color: #474747;
top: 40px;
left: 600px;
width: 230px;
}
#BelowText{
position: absolute;
font-size: 14px;
color: #474747;
top: 900px;
left: 30px;
width: 1000px;
}
line {
stroke: #000;
stroke-width: 1.px;
}
.hint {
position: absolute;
left: 0;
top: 30px;
width: 500px;
font-size: 12px;
color: #999;
}
text {
font-size: 10px;
}
.titles{
font-size: 14px;
}
.annotate{
font-size: 11px;
fill: #474747;
}
path.chord {
fill-opacity: .80;
}
.pTitle {
font-size: 16px;
text-shadow: 0 1px 0 #fff;
fill: #474747;
font-weight: bold;
}
.survey {
font-size: 10px;
}
</style>
</head>
<div class="intror">
<h2>How to read</h2>
<p >The circlized graph shows the villages of origin in color and the destinations in gray. <br>Each strip represents a number of people emigrating to the same destination country. <br>Move your mouse over each village to see the different.</p>
</div>
<footer class="footer"><p id="p0" class="pTitle Side" style="opacity: 1;">Chord Diagram</p>
<p id="p1" class="Side" style="opacity: 1;">This chord diagram shows relationships in terms of migration from Nepali villages to the world.</p>
<p id="p2" class="Side" style="opacity: 1;">The colored parts are the origin and the grey parts are the destinations.</p>
<p id="p3" class="Side" style="opacity: 1;">The numbers show the number of people that migrated from each village.</p>
</footer>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var visual = document.getElementById("visual");
// persons moving between ...
var matrix = [
[0,0,0,0,0,1,8,0,1,8,0,2,14,1,6,0,0,0],
[0,0,0,0,0,1,5,1,3,7,2,0,6,0,7,2,0,0],
[0,0,0,0,0,1,12,0,2,10,0,0,1,0,4,0,0,0],
[0,0,0,0,0,0,34,0,0,16,0,0,1,0,8,0,0,0],
[0,0,0,0,0,0,46,1,4,1,2,0,0,0,12,8,0,0],
[0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5]
];
var array = ["Chisopani","Ghatichina","Lower Shidane","Makwanpur","Thulakhet",
"Bahrain","Nepal","Dubai","Hongkong","India","Japan","Korea",
"Kuwait","Malaysia","Oman","Qatar","S.A.","U.K."];
var rotation = 0;
var chord_options = {
"gnames": array,
"rotation": rotation,
"colors": ["#76AE2B","#1383FA","#FC0178","#3C3F53","#E49813","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC",
"#CCCCCC","#CCCCCC"]
};
function Chord(container, options, matrix) {
// initialize the chord configuration variables
var config = {
width: 640,
height: 560,
rotation: 0,
textgap: 26,
colors: ["#76AE2B","#1383FA","#FC0178","#3C3F53","#E49813","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC","#CCCCCC",
"#CCCCCC","#CCCCCC"]
};
// add options to the chord configuration object
if (options) {
extend(config, options);
}
// set chord visualization variables from the configuration object
var offset = Math.PI * config.rotation,
width = config.width,
height = config.height,
textgap = config.textgap
colors = config.colors;
// set viewBox and aspect ratio to enable a resize of the visual dimensions
var viewBoxDimensions = "0 0 " + width + " " + height,
aspect = width / height;
if (config.gnames) {
gnames = config.gnames;
} else {
// make a list of names
gnames = [];
for (var i=97; i<matrix.length; i++) {
gnames.push(String.fromCharCode(i));
}
}
// start the d3 magic
var chord = d3.layout.chord()
.padding(.04)
.sortSubgroups(d3.descending)
.sortChords(d3.ascending) //which chord should be shown on top when chords cross. Now the biggest chord is at the bottom
.matrix(matrix);
var innerRadius = Math.min(width, height) * .35,
outerRadius = innerRadius * 1.1;
var fill = d3.scale.ordinal()
.domain(d3.range(array.length-1))
.range(colors);
var svg = d3.select("body").append("svg")
.attr("id", "visual")
.attr("viewBox", viewBoxDimensions)
.attr("preserveAspectRatio", "xMinYMid") // add viewBox and preserveAspectRatio
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var g = svg.selectAll("g.group")
.data(chord.groups)
.enter().append("svg:g")
.attr("class", "group");
g.append("svg:path")
.style("fill", function(d) { return fill(d.index); })
.style("stroke", function(d) { return fill(d.index); })
.attr("id", function(d, i) { return "group" + d.index; })
.attr("d", d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius).startAngle(startAngle).endAngle(endAngle))
.on("mouseover", fade(.02))
.on("mouseout", fade(.80));
////////////////////////////////////////////////////////////
////////////////// Append Names ////////////////////////////
////////////////////////////////////////////////////////////
g.append("svg:text")
.each(function(d) {d.angle = ((d.startAngle + d.endAngle) / 2) + offset; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
.attr("transform", function(d) {
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
+ "translate(" + (innerRadius + 55) + ")"
+ (d.angle > Math.PI ? "rotate(180)" : "");
})
.text(function(d) { return gnames[d.index]; });
svg.append("g")
.attr("class", "chord")
.selectAll("path")
.data(chord.chords)
.enter().append("path")
.attr("d", d3.svg.chord().radius(innerRadius).startAngle(startAngle).endAngle(endAngle))
.style("fill", function(d) { return fill(d.source.index); })
.style("opacity", 1)
.append("svg:title")
.text(function(d) {
return d.source.value + " people from " + gnames[d.source.index] + " migrated to " + gnames[d.target.index];
});
////////////////////////////////////////////////////////////
////////////////// Append Ticks ////////////////////////////
////////////////////////////////////////////////////////////
var ticks = svg.append("svg:g").selectAll("g.ticks")
.data(chord.groups)
.enter().append("svg:g").selectAll("g.ticks")
.attr("class", "ticks")
.data(groupTicks)
.enter().append("svg:g")
.attr("transform", function(d) {
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
+ "translate(" + outerRadius+40 + ",0)";
});
ticks.append("svg:line")
.attr("x1", 1)
.attr("y1", 0)
.attr("x2", 5)
.attr("y2", 0)
.style("stroke", "#000");
ticks.append("svg:text")
.attr("x", 8)
.attr("dy", ".35em")
.attr("transform", function(d) { return d.angle > Math.PI ? "rotate(180)translate(-16)" : null; })
.style("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
.text(function(d) { return d.label; });
// helper functions start here
function startAngle(d) {
return d.startAngle + offset;
}
function endAngle(d) {
return d.endAngle + offset;
}
function extend(a, b) {
for( var i in b ) {
a[ i ] = b[ i ];
}
}
// Returns an event handler for fading a given chord group.
function fade(opacity) {
return function(g, i) {
svg.selectAll(".chord path")
.filter(function(d) { return d.source.index != i && d.target.index != i; })
.transition()
.style("opacity", opacity);
};
}
// Returns an array of tick angles and labels, given a group.
function groupTicks(d) {
var k = (d.endAngle - d.startAngle) / d.value;
return d3.range(0, d.value, 1).map(function(v, i) {
return {
angle: v * k + d.startAngle,
label: i % 5 ? null : v + "p"
};
});
}//groupTicks
window.onresize = function() {
var targetWidth = (window.innerWidth < width)? window.innerWidth : width;
var svg = d3.select("#visual")
.attr("width", targetWidth)
.attr("height", targetWidth / aspect);
}
}
window.onload = function() {
Chord(visual, chord_options, matrix);
}
d3.select(self.frameElement).style("height", "600px");
////////////////////////////////////////////////////////////
////////////////// Extra Functions /////////////////////////
////////////////////////////////////////////////////////////
<!--
// Returns an event handler for fading a given chord group.
function fadeIn(opacity) {
return function(d, i) {
d3.selectAll(".Side")
.transition()
.duration(200)
.style("opacity", 0.0001);
d3.selectAll(".annotate")
.transition()
.duration(200)
.style("opacity", 0.0001);
d3.select("#p0").transition().duration(200).delay(200).style("opacity", 1).text(array[i]);
if(i == 0) { //Chisopani
d3.select("#p1").transition().duration(200).delay(200).style("opacity", 1).text("One third of all emigration from Chisopani is directed towards Kuwait.");
d3.select("#p2").transition().duration(200).delay(200).style("opacity", 1).text("One third of emigration is directed towards India and inside Nepal");
d3.select("#p3").transition().duration(200).delay(200).style("opacity", 1).text("The last part of emigration is directed towards Oman, Malaysia, Korea, Hongkong, and Bahrain");
d3.select("#p4").transition().duration(200).delay(200).style("opacity", 1).text(" ");
d3.select("#p5").transition().duration(200).delay(200).style("opacity", 1).text(" ");
} else if (i == 1) { //Ghatichina
d3.select("#p1").transition().duration(200).delay(200).style("opacity", 1).text("More than half of emigration from Ghatichina is directed towards three countries: Oman, Kuwait, and India");
d3.select("#p2").transition().duration(200).delay(200).style("opacity", 1).text("The other half is distributed in descending order to: Nepal, Japan, Qatar, Hongkong, Dubai, and Bahrain ");
d3.select("#p3").transition().duration(200).delay(200).style("opacity", 1).text("");
d3.select("#p4").transition().duration(200).delay(200).style("opacity", 1).text("");
d3.select("#p5").transition().duration(200).delay(200).style("opacity", 1).text(" ");
} else if (i == 2) { //Lower Shidane
d3.select("#p1").transition().duration(200).delay(200).style("opacity", 1).text("One third of emigration from Lower Shidane stays inside Nepal");
d3.select("#p2").transition().duration(200).delay(200).style("opacity", 1).text("One third of emigration is directed towards India");
d3.select("#p3").transition().duration(200).delay(200).style("opacity", 1).text("And the last third is distributed to Oman, Hongkong, Kuwait, and Bahrain");
d3.select("#p4").transition().duration(200).delay(200).style("opacity", 1).text(" ");
d3.select("#p5").transition().duration(200).delay(200).style("opacity", 1).text(" ");
} else if (i == 3) { //Makwanpur
d3.select("#p1").transition().duration(200).delay(200).style("opacity", 1).text("From Makwanpur more than half of the people move inside Nepal");
d3.select("#p2").transition().duration(200).delay(200).style("opacity", 1).text("More than a quarter emigrates to India");
d3.select("#p3").transition().duration(200).delay(200).style("opacity", 1).text("Another quarter of emigration is directed towards Oman and Kuwait");
d3.select("#p4").transition().duration(200).delay(200).style("opacity", 1).text("");
d3.select("#p5").transition().duration(200).delay(200).style("opacity", 1).text("");
} else if (i == 4) { //Thulakhet
d3.select("#p1").transition().duration(200).delay(200).style("opacity", 1).text("Two thirds of out-migration from Thulakhet stays inside Nepal");
d3.select("#p2").transition().duration(200).delay(200).style("opacity", 1).text("The last third is directed towards Qatar, Oman, Japan, India, Hongkong, and Dubai");
d3.select("#p3").transition().duration(200).delay(200).style("opacity", 1).text("");
d3.select("#p4").transition().duration(200).delay(200).style("opacity", 1).text("");
d3.select("#p5").transition().duration(200).delay(200).style("opacity", 1).text("");
};
svg.selectAll("path.chord")
.filter(function(d) { return d.source.index != i && d.target.index != i; })
.transition()
.style("stroke-opacity", opacity)
.style("fill-opacity", opacity);
};
}//fadeIn
// Returns an event handler for fading a given chord group.
function fadeOut(opacity) {
return function(d, i) {
d3.selectAll(".Side")
.transition()
.duration(500)
.style("opacity", 0.0001);
d3.selectAll(".annotate")
.transition()
.duration(500)
.style("opacity", 1);
d3.select("#p0").transition().duration(200).delay(200).style("opacity", 1).text("Chord Diagram")
d3.select("#p1").transition().duration(200).delay(200).style("opacity", 1).text("This chord diagram shows relationships in terms of migration from Nepali villages to the world.")
d3.select("#p2").transition().duration(200).delay(200).style("opacity", 1).text("The colored parts are the origin and the grey parts are the destinations.")
d3.select("#p3").transition().duration(200).delay(200).style("opacity", 1).text("The numbers show the number of people that migrated")
svg.selectAll("path.chord")
.filter(function(d) { return d.source.index != i && d.target.index != i; })
.transition()
.style("stroke-opacity", opacity)
.style("fill-opacity", opacity);
};
}//fadeOut
// Returns an array of tick angles and labels, given a group.
function groupTicks(d) {
var k = (d.endAngle - d.startAngle) / d.value;
return d3.range(0, d.value, 1).map(function(v, i) {
return {
angle: v * k + d.startAngle,
label: i % 5 ? null : v + "p"
};
});
}//groupTicks
</script>
</body>
</html>
@baderone
Copy link
Author

Until now, the dynamic footer is not working, so the footer (code rows 150-154) should updated by mouse-over accoding to Extras function code rows 383-480

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment