Extended split chord diagram with subchords with miltipler, based on https://bl.ocks.org/vkuchinov/212ba0d3b50350da8cfee2a90ff1d118
Last active
February 12, 2018 16:01
-
-
Save vkuchinov/d87b5aa2e2070eb8d5b080babb1a9a36 to your computer and use it in GitHub Desktop.
D3.JS Split Chords With Multiplier
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
border: no | |
height: 960 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ | |
{ "name" : "Public Transport", "color": "#E00B27", "tag": "public" }, | |
{ "name" : "Car/Bicycle", "color" : "#2474A6", "tag": "car" }, | |
{ "name" : "Walking", "color" : "#F2A30F", "tag": "walking" } | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ | |
{ | |
"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 | |
} | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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 multiplier = 0.35; | |
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(); | |
}); | |
}); | |
function 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); | |
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("class", "fullchord") | |
.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 splitted = splitChords(extended, data); | |
g.append("g") | |
.selectAll("path") | |
.data(splitted) | |
.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 + 49) + "," + (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 = getNodesValuesWithMultiplier(data_.right[i].targets, data_.left, multiplier); | |
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 getNodesValuesWithMultiplier(array_, targets_, multiplier_){ | |
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 * multiplier_; } | |
} | |
} | |
return output; | |
} | |
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); | |
//GAP | |
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