Skip to content

Instantly share code, notes, and snippets.

@vkuchinov
Last active February 12, 2018 16:02
Show Gist options
  • Save vkuchinov/212ba0d3b50350da8cfee2a90ff1d118 to your computer and use it in GitHub Desktop.
Save vkuchinov/212ba0d3b50350da8cfee2a90ff1d118 to your computer and use it in GitHub Desktop.
D3.JS Split Chords
border: no
height: 960

Split chord diagram with subchords. Handful, if you want to show relationships with categories filtering, e.g. by transport.

[
{ "name" : "Public Transport", "color": "#E00B27", "tag": "public" },
{ "name" : "Car/Bicycle", "color" : "#2474A6", "tag": "car" },
{ "name" : "Walking", "color" : "#F2A30F", "tag": "walking" }
]
[
{
"name": "Barnet",
"children": [
{
"name": "YES",
"children": [
{
"name": "Public Transport",
"value": 3,
"color": 0
},
{
"name": "Car/Bicycle",
"value": 1,
"color": 1
},
{
"name": "Walking",
"value": 3,
"color": 2
}
],
"value": 0
},
{
"name": "NO",
"children": [
{
"name": "Walking",
"value": 3,
"color": 2
}
],
"value": 0
}
],
"value": 0
},
{
"name": "Croydon",
"children": [
{
"name": "YES",
"children": [
{
"name": "Public Transport",
"value": 7,
"color": 0
},
{
"name": "Car/Bicycle",
"value": 1,
"color": 1
},
{
"name": "Walking",
"value": 1,
"color": 2
}
],
"value": 0
},
{
"name": "NO",
"children": [
{
"name": "Public Transport",
"value": 2,
"color": 0
},
{
"name": "Car/Bicycle",
"value": 2,
"color": 1
}
],
"value": 0
}
],
"value": 0
},
{
"name": "Hampton",
"children": [
{
"name": "YES",
"children": [
{
"name": "Public Transport",
"value": 4,
"color": 0
},
{
"name": "Walking",
"value": 4,
"color": 2
}
],
"value": 0
},
{
"name": "NO",
"children": [
{
"name": "Public Transport",
"value": 2,
"color": 0
},
{
"name": "Walking",
"value": 1,
"color": 2
}
],
"value": 0
}
],
"value": 0
},
{
"name": "Ilford",
"children": [
{
"name": "YES",
"children": [
{
"name": "Car/Bicycle",
"value": 2,
"color": 1
},
{
"name": "Walking",
"value": 6,
"color": 2
}
],
"value": 0
},
{
"name": "NO",
"children": [
{
"name": "Public Transport",
"value": 3,
"color": 0
},
{
"name": "Car/Bicycle",
"value": 3,
"color": 1
},
{
"name": "Walking",
"value": 3,
"color": 2
}
],
"value": 0
}
],
"value": 0
},
{
"name": "Richmond",
"children": [
{
"name": "YES",
"children": [
{
"name": "Public Transport",
"value": 4,
"color": 0
},
{
"name": "Car/Bicycle",
"value": 4,
"color": 1
},
{
"name": "Walking",
"value": 1,
"color": 2
}
],
"value": 0
},
{
"name": "NO",
"children": [
{
"name": "Public Transport",
"value": 1,
"color": 0
},
{
"name": "Car/Bicycle",
"value": 1,
"color": 1
},
{
"name": "Walking",
"value": 1,
"color": 2
}
],
"value": 0
}
],
"value": 0
},
{
"name": "Stanmore",
"children": [
{
"name": "YES",
"children": [
{
"name": "Car/Bicycle",
"value": 6,
"color": 1
},
{
"name": "Walking",
"value": 1,
"color": 2
}
],
"value": 0
},
{
"name": "NO",
"children": [
{
"name": "Public Transport",
"value": 8,
"color": 0
}
],
"value": 0
}
],
"value": 0
},
{
"name": "Sutton",
"children": [
{
"name": "YES",
"children": [
{
"name": "Car/Bicycle",
"value": 4,
"color": 1
},
{
"name": "Walking",
"value": 4,
"color": 2
}
],
"value": 0
},
{
"name": "NO",
"children": [
{
"name": "Car/Bicycle",
"value": 1,
"color": 1
},
{
"name": "Public Transport",
"value": 1,
"color": 0
}
],
"value": 0
}
],
"value": 0
},
{
"name": "Uxbridge",
"children": [
{
"name": "NO",
"children": [
{
"name": "Public Transport",
"value": 2,
"color": 0
},
{
"name": "Car/Bicycle",
"value": 2,
"color": 1
},
{
"name": "Walking",
"value": 3,
"color": 2
}
],
"value": 0
}
],
"value": 0
}
]
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.ribbons { fill-opacity: 0.67; }
</style>
<svg width="960" height="960"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
Array.prototype.insert = function(index_ ) {
index_ = Math.min(index_, this.length);
arguments.length > 1
&& this.splice.apply(this, [index_, 0].concat([].pop.call(arguments)))
&& this.insert.apply(this, arguments);
return this;
};
var theta = 0;
var categories = [];
var data = [];
d3.json("categories.json", function(error_, categories_) {
if (error_) throw error_;
categories_.forEach(function(d) { categories.push(d); });
d3.json("data.json", function(error_, data_) {
if (error_) throw error_;
data_.forEach(function(d) { data.push(d); });
data = parseData(data.reverse());
inits();
});
});
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
outerRadius = Math.min(width, height) * 0.35,
innerRadius = outerRadius * 0.95;
var formatValue = d3.formatPrefix(",.0", 1E3);
function inits(){
var chord = d3.chord()
.padAngle(0.03)
.sortSubgroups(d3.descending);
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var ribbon = d3.ribbon()
.radius(innerRadius);
var extended = extendChords(chord(data.matrix), data);
var g = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + "),rotate(" + theta + ")")
.datum(extended);
var group = g.append("g")
.attr("class", "groups")
.selectAll("g")
.data(function(chords) { return chords.groups; })
.enter().append("g");
group.append("path")
.attr("class", function(d) { return d.tag; })
.attr("d", arc)
.style("fill", function(d) { return d.color; })
.style("stroke", "none")
.on("mouseover", function(d){
var select = d3.select(this).attr("class");
d3.selectAll("." + select).attr("opacity", 1.0);
})
.on("mouseout", function(d){
d3.selectAll(".ribbons").attr("opacity", 0.0);
});
group.append("text")
.attr("dy", ".35em")
.style("font-family", "sans-serif")
.style("font-size", "10px")
.attr("text-anchor", function(d) { d.angle = (d.startAngle + d.endAngle) / 2; return ((d.angle > 0 && d.angle < 2.8) ? "end" : "start"); } )
.attr("transform", function(d,i) {
d.angle = (d.startAngle + d.endAngle) / 2;
return "rotate(" + ((d.angle) * 180 / Math.PI) + ")" + "translate(0," + -1.0 * (outerRadius + 10) + ")" +
((d.angle > 0 && d.angle < 2.8) ? "rotate(90)" : "rotate(-90)");
})
.text(function(d, i) { return d.tag; });
var split = splitChords(extended, data);
g.append("g")
.selectAll("path")
.data(split)
.enter().append("path")
.attr("class", function(d) {
var source = getTagByIndex(data, d.source.index);
var target = getTagByIndex(data, d.target.index);
return "ribbons " + source + " " + target + " " + getKeyByColor(d.color).toUpperCase();
})
.attr("d", ribbon)
.attr("opacity", 0)
.style("fill", function(d, i) { return d.color; })
.style("stroke", "none");
var legend = svg.append("g")
.attr("transform", "translate(" + (width / 2 - 64) + "," + (height * 0.75) + ")")
.selectAll("rect", "text")
.data(categories)
.enter()
.append("rect")
.attr("class", function(d){ return d.tag.toUpperCase(); })
.attr("x", 0)
.attr("y", function(d, i){ return i * 32; })
.attr("width", 24)
.attr("height", 24)
.style("fill", function(d) { return d.color; })
.on("mouseover", function(d){
var select = d3.select(this).attr("class");
d3.selectAll("." + select).attr("opacity", 1.0);
})
.on("mouseout", function(d){
d3.selectAll(".ribbons").attr("opacity", 0.0);
})
.select(function() { return this.parentNode; })
.append("text")
.attr("dx", 40)
.attr("dy", function(d, i){ return i * 32 + 16; })
.text(function(d, i){ return categories[i].name.toUpperCase(); });
}
function parseData(data_){
var output = { left: [], right: [], matrix: [], gaps: [] };
for(var i = 0; i < data_.length; i++){
var obj = { name: data_[i].name, targets: [], value: accumulateValues(data_[i]) }
for(var j = 0; j < data_[i].children.length; j++){
if(!nodeExistsByName(output.right, data_[i].children[j].name)){
var obj1 = { name : data_[i].children[j].name, value : data_[i].children[j].value, targets: [] }
obj1.targets.push( { name : data_[i].name, value: data_[i].children[j].value });
output.right.push(obj1);
} else {
var obj1 = getNodeByName(output.right, data_[i].children[j].name);
obj1.targets.push( { index: getNodeIndexByName(output.right, data_[i].children[j].name), name : data_[i].name, value: data_[i].children[j].value });
}
var group = parseChildren(data_[i].children[j].children);
var child = { index : getNodeIndexByName(output.right, data_[i].children[j].name), name: data_[i].children[j].name , value: data_[i].children[j].value , splits: group };
obj.targets.push(child);
}
output.left.push(obj);
}
buildMatrix(output);
return output;
}
function buildMatrix(data_){
var total = data_.left.reduce((prev_, next_) => prev_ + next_.value, 0),
emptyPerc = 0.5,
emptyStroke = Math.round(total * emptyPerc);
for(var i = 0; i < data_.left.length; i++){
var row = Array.apply(null, Array(data_.left.length + 1)).map(Number.prototype.valueOf, 0);
row = row.concat(getNodesValues(data_.left[i].targets, data_.right));
row.push(0);
data_.matrix.push(row);
}
data_.gaps.push(data_.matrix.length);
data_.matrix.push(Array.apply(null, Array(data_.left.length + data_.right.length + 2)).map(Number.prototype.valueOf, 0));
data_.matrix[data_.matrix.length - 1][data_.left.length + data_.right.length + 1] = emptyStroke;
for(var i = 0; i < data_.right.length; i++){
var row = getNodesValues(data_.right[i].targets, data_.left);
row.push(0);
row = row.concat(Array.apply(null, Array(data_.right.length + 1)).map(Number.prototype.valueOf, 0));
data_.matrix.push(row);
}
data_.gaps.push(data_.matrix.length);
data_.matrix.push(Array.apply(null, Array(data_.left.length + data_.right.length + 2)).map(Number.prototype.valueOf, 0));
data_.matrix[data_.matrix.length - 1][data_.left.length] = emptyStroke;
}
function extendChords(chords_, data_){
var median = { min: Math.PI * 2, max: 0, value: 0 };
for(var i = 0; i < chords_.groups.length; i++){
if(i != data_.gaps[0] && i != data_.gaps[1]){
if(chords_.groups[i].startAngle < median.min) { median.min = chords_.groups[i].startAngle; }
if(chords_.groups[i].endAngle > median.max) { median.max = chords_.groups[i].endAngle; }
if(i < data_.left.length) { chords_.groups[i].tag = data_.left[i].name.toUpperCase(); }
else{ chords_.groups[i].tag = data_.right[ i - data_.left.length - 1].name.toUpperCase(); }
chords_.groups[i].color = "#2474A6";
}else{
chords_.groups[i].tag = "";
chords_.groups[i].color = "none";
}
}
theta = median.min + (median.max - median.min) * 180 / Math.PI - 90 ;
return chords_;
}
function nodeExistsByName(array_, element_){
for(var i = 0; i < array_.length; i++) { if (array_[i].name == element_) { return true; } }
return false;
}
function getNodeByName(array_, element_){
for(var i = 0; i < array_.length; i++) { if (array_[i].name == element_) { return array_[i]; } }
return null;
}
function getNodeIndexByName(array_, element_){
for(var i = 0; i < array_.length; i++) { if (array_[i].name == element_) { return i; } }
return null;
}
function getNodesValues(array_, targets_){
var output = Array.apply(null, Array(targets_.length)).map(Number.prototype.valueOf, 0);
for(var i = 0; i < targets_.length; i++){
for(var j = 0; j < array_.length; j++){
if(targets_[i].name == array_[j].name) { output[i] += array_[j].value; }
}
}
return output;
}
function getTagByIndex(data_, index_){
if(index_ != data.gaps[0] && index_ != data.gaps[1]){
if(index_ < data_.left.length){
return data_.left[index_].name.toUpperCase();
}else{
return data_.right[index_ - data_.left.length - 1].name.toUpperCase();
}
}else{
return ""
}
}
function getKeyByColor(color_){
if(color_ == "none") { return ""; }
return categories.filter(function(item_) { return item_.color == color_; })[0].tag;
}
function accumulateValues(parent_){
if (parent_.children != undefined) {
parent_.value = 0;
for (var i = 0; i < parent_.children.length; i++) { parent_.value += accumulateValues(parent_.children[i]); }
}
return parent_.value;
}
function parseChildren(children_){
var output = [];
for(var i = 0; i < children_.length; i++){
var obj = { value: children_[i].value, tag: children_[i].name, color: getColorByKey(children_[i].name) }
output.push(obj);
}
return output;
}
function getColorByKey(key_){ return categories.filter(function(item_) { return item_.name == key_ })[0].color; }
function splitChords(chords_, data_){
for(var i = 0; i < data_.left.length; i++){
for(var j = 0; j < data_.left[i].targets.length; j++){
splitChord(chords_, i, data_.left.length + data_.left[i].targets[j].index + 1, data_.left[i].targets[j].splits );
}
}
return chords_;
}
function splitChord(chords_, source_, target_, splits_ ){
var chord = chords_.filter(function(item_) { return item_.source.index == source_ && item_.target.index == target_; })[0];
var index = chords_.indexOf(chord);
const total = splits_.reduce((prev_, next_) => prev_ + next_.value, 0);
var sigma = { start: chord.source.startAngle, end: chord.source.endAngle, tau: (chord.source.endAngle - chord.source.startAngle) / total };
var theta = { start: chord.target.startAngle, end: chord.target.endAngle, tau: (chord.target.endAngle - chord.target.startAngle) / total };
parts = [];
var count = 0;
for(var k = 0; k < splits_.length; k++){
var obj = JSON.parse(JSON.stringify(chord));
obj.source.startAngle = sigma.start + count * sigma.tau;
obj.source.endAngle = obj.source.startAngle + splits_[k].value * sigma.tau;
obj.target.endAngle = theta.end - count * theta.tau;
obj.target.startAngle = obj.target.endAngle - splits_[k].value * theta.tau;
obj.source.index = chord.source.index;
obj.target.index = chord.target.index;
obj.source.subindex = chord.source.subindex;
obj.target.subindex = chord.target.subindex;
obj.source.value = splits_[k].value;
obj.target.value = splits_[k].value;
obj.color = splits_[k].color;
parts.push(obj);
count += splits_[k].value;
}
chords_.splice(index, 1);
chords_.insert(index, parts);
chords_[chords_.length - 1].color = "none";
return chords_;
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment