Skip to content

Instantly share code, notes, and snippets.

@StewartNoyce
Last active August 29, 2015 13:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save StewartNoyce/9532973 to your computer and use it in GitHub Desktop.
Save StewartNoyce/9532973 to your computer and use it in GitHub Desktop.
Open Chord Diagram

This example uses the D3 chord layout function to show the commute patterns of people who belong to an affinity group. It gives some insight as to the availability of group members for afterwork events.

The chord layout implements the concept of circular visualization introduced in Circos. This particular example is "open" in that the matrix used to compute the chords has a set of null rows. Affinity group members were commuting to locations both inside and outside of their home region.

Notable features include: responsive image resizing, chord layout rotation, and a generalized chord visualization function with configuration object.

<!DOCTYPE html>
<meta charset="utf-8">
<!-- affinity group data, circle rotation, auto-resize svg image, chord function, configuration -->
<title>Open Chord Example</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;
}
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var visual = document.getElementById("visual");
// persons moving between Marin, Sonoma, Napa, SF, EB, SV and other regions
var matrix = [
[ 198, 11, 3, 330, 32, 35, 59 ],
[ 11, 89, 8, 33, 15, 0, 28 ],
[ 0, 1, 51, 29, 17, 0, 26 ],
[ 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 ]
];
var array = [ "Marin", "Sonoma", "Napa", "San Francisco", "East Bay", "Silicon Valley", "Remote Locations" ];
var rotation = -0.7;
var chord_options = {
"gnames": array,
"rotation": rotation,
"colors": ["#034e7b","#feb24c","#b10026","#238443","#fdbb84","#ffffb2","#fed976"]
};
function Chord(container, options, matrix) {
// initialize the chord configuration variables
var config = {
width: 640,
height: 560,
rotation: 0,
textgap: 26,
colors: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99", "#386cb0", "#f0027f", "#bf5b17", "#666666"]
};
// 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(.05)
.sortSubgroups(d3.descending)
.matrix(matrix);
var innerRadius = Math.min(width, height) * .31,
outerRadius = innerRadius * 1.1;
var fill = d3.scale.ordinal()
.domain(d3.range(matrix.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(.1))
.on("mouseout", fade(1));
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(" + (outerRadius + textgap) + ")"
+ (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] + " commute to " + gnames[d.target.index];
});
// 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);
};
}
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");
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment