Skip to content

Instantly share code, notes, and snippets.

@gcalmettes
Last active August 20, 2017 17:58
Show Gist options
  • Save gcalmettes/67d67ec96092e3d607dd36818c0a7b91 to your computer and use it in GitHub Desktop.
Save gcalmettes/67d67ec96092e3d607dd36818c0a7b91 to your computer and use it in GitHub Desktop.
D3.unconf() badge
license: gpl-3.0

Modifying Beating heart chord diagram to fit the requirements of the D3.unconf() badge generation tool.


Experimenting transitions with the chord diagram layout to show blood flow between the four chambers of the heart during a contraction cycle.

The heart consists of four chambers in which blood flows. Deoxygenated blood (blue color) enters the right atrium and passes through the right ventricle. The right ventricle pumps the blood to the lungs where it becomes oxygenated (red color). The oxygenated blood is brought back to the heart by the pulmonary veins which enter the left atrium. From the left atrium blood flows into the left ventricle. The left ventricle pumps the blood to the aorta which will distribute the oxygenated blood to all parts of the body and become deoxygenated again.

Notes:

  • I applied linear svg gradients for the color transition as explained by Nadieh in this post

  • I also used her trick for inverting the text of the bottom half of the chord diagram (see this post)

<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 20px sans-serif;
background: #000;
}
svg { background: #000; }
.ribbons {
fill-opacity: 0.67;
}
</style>
<svg width="1050" height="1500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// Note: Cannot pass index to sortSubgroups() so need to keep
// sign of differences the same so ribbons won't switch order
// systole
var systole = [
[ 0, 19, 20, 0], // left atria
[ 27, 0, 0, 23], // left ventricle
[ 27, 0, 0, 23], // right ventricle
[ 0, 19, 20, 0] // right atria
];
// diastole
var diastole = [
[ 0, 9, 10, 0], // left atria
[ 33, 0, 0, 30], // left ventricle
[ 31, 0, 0, 29], // right ventricle
[ 0, 9, 10, 0] // right atria
];
// Red = oxygenated blood, Blue = deoxygenated blood
var heartChambers = ["Left Atria","Left Ventricle","Right Ventricle","Right Atria"],
colors = ["#e85151", "#e85151", "#51aae8", "#51aae8"];
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
outerRadius = Math.min(width, height) * 0.5 - 100,
innerRadius = outerRadius - 30;
var chord = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending);
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var ribbon = d3.ribbon()
.radius(innerRadius);
var color = d3.scaleOrdinal()
.domain(d3.range(4))
.range(colors);
var chordG = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 1.87 + ")")
.datum(chord(systole));
// .datum(chords);
var heartGroup = chordG.append("g")
.selectAll("g")
.data(function(chords) {return chords.groups; })
.enter()
.append("g")
.attr("class", "arcG");
//Create the heart chambers arcs
//see https://www.visualcinnamon.com/2015/09/placing-text-on-arcs.html
heartGroup.append("path")
.attr("class", "visible")
.attr("id", function(d, i){return "heartChamber-" + i;})
.style("fill", function(d) { return color(d.index); })
.style("stroke", function(d) { return d3.rgb(color(d.index)).darker(); })
.attr("d", arc)
// and also the invisible arcs for the text
heartGroup.append("path")
.attr("class", "hiddenArcs")
.attr("id", function(d, i) {return "hiddenArc-"+i})
.attr("d", simplerArc)
.style("fill", "none");
heartGroup.append("text")
//Move the labels below the arcs for those slices of bottom half of the chord diagram
.attr("dy", function(d,i) { return ((d.endAngle > 90 * Math.PI/180 && d.endAngle < 315 * Math.PI/180) ? 55 : -20); })
.append("textPath")
.attr("startOffset","50%")
.style("text-anchor","middle")
.attr("xlink:href",function(d,i){return "#hiddenArc-" + i;})
.text(function(d,i){return heartChambers[i];})
.style("font-size", 55)
.style("fill", function(d,i){return colors[i];});
//Create a gradient definition for each chord
//see https://www.visualcinnamon.com/2016/06/orientation-gradient-d3-chord-diagram.html
var grads = svg.append("defs").selectAll("linearGradient")
.data(chord(systole))
.enter().append("linearGradient")
//Create a unique gradient id per chord: e.g. "chordGradient-0-4"
.attr("id", function(d) { return "chordGradient-" + d.source.index + "-" + d.target.index; })
//Instead of the object bounding box, use the entire SVG for setting locations
//in pixel locations instead of percentages (which is more typical)
.attr("gradientUnits", "userSpaceOnUse")
//The full mathematical formula to find the x and y locations of the Avenger's source chord
.attr("x1", function(d,i) {
return innerRadius*Math.cos((d.source.endAngle-d.source.startAngle)/2+d.source.startAngle-Math.PI/2);
})
.attr("y1", function(d,i) {
return innerRadius*Math.sin((d.source.endAngle-d.source.startAngle)/2+d.source.startAngle-Math.PI/2);
})
//Find the location of the target Avenger's chord
.attr("x2", function(d,i) {
return innerRadius*Math.cos((d.target.endAngle-d.target.startAngle)/2+d.target.startAngle-Math.PI/2);
})
.attr("y2", function(d,i) {
return innerRadius*Math.sin((d.target.endAngle-d.target.startAngle)/2+d.target.startAngle-Math.PI/2);
});
//Set the starting color (at 0%)
grads.append("stop")
.attr("offset", "0%")
.attr("stop-color", function(d){ return colors[d.source.index]; });
//Set the ending color (at 100%)
grads.append("stop")
.attr("offset", "100%")
.attr("stop-color", function(d){ return colors[d.target.index]; });
chordG.append("g")
.attr("class", "ribbons")
.selectAll("path")
.data(function(chords) { return chords; })
.enter().append("path")
.attr("d", ribbon)
//Change the fill to reference the unique gradient ID of the source-target combination
.style("fill", function(d){ return "url(#chordGradient-" + d.source.index + "-" + d.target.index + ")"; })
function simplerArc(d, i) {
var chamber = heartGroup.select("#heartChamber-" + i)
var firstArcSection = /(^.+?)L/;
//The [1] gives back the expression between the () (thus not the L as well)
//which is exactly the arc statement
var newArc = firstArcSection.exec( chamber.node().getAttribute("d") )[1];
//Replace all the comma's so that IE can handle it -_-
//The g after the / is a modifier that "find all matches rather than stopping after the first match"
newArc = newArc.replace(/,/g , " ");
//If the end angle lies in the bottom half of the circle
//flip the end and start position so text will be inverted
if (d.endAngle > 90 * Math.PI/180 && d.endAngle < 315 * Math.PI/180) {
var startLoc = /M(.*?)A/, //Everything between the capital M and first capital A
middleLoc = /A(.*?)0 0 1/, //Everything between the capital A and 0 0 1
endLoc = /0 0 1 (.*?)$/; //Everything between the 0 0 1 and the end of the string (denoted by $)
//Flip the direction of the arc by switching the start and end point (and sweep flag)
var newStart = endLoc.exec( newArc )[1];
var newEnd = startLoc.exec( newArc )[1];
var middleSec = middleLoc.exec( newArc )[1];
//Build up the new arc notation, set the sweep-flag to 0
newArc = "M" + newStart + "A" + middleSec + "0 0 0 " + newEnd;
}//if
return newArc
}
//animate contraction by transitioning between the two states, diastole/systole
var count = 0
var dataset = {0: diastole, 1: systole}
var last_chord = chord(systole)
update()
d3.interval(update, 4000)
function update() {
data = dataset[count%2]
// update arcs
chordG.selectAll(".arcG .visible")
.data(chord(data).groups)
.transition()
.duration(4000)
.attrTween("d", arcTween(last_chord))
// update chords
chordG.select(".ribbons")
.selectAll("path")
.data(chord(data))
.transition()
.duration(4000)
.attrTween("d", chordTween(last_chord))
last_chord = chord(data);
count+=1;
}
function arcTween(chord) {
return function(d,i) {
var i = d3.interpolate(chord.groups[i], d);
return function(t) {
return arc(i(t));
}
}
}
function simpleTween(chord) {
return function(d,i) {
var i = d3.interpolate(chord.groups[i], d);
return function(t) {
var visibleArc = arc(i(t))
var firstArcSection = /(^.+?)L/;
var newArc = firstArcSection.exec(visibleArc)[1];
newArc = newArc.replace(/,/g , " ");
if (d.endAngle > 90 * Math.PI/180 && d.endAngle < 315 * Math.PI/180) {
var startLoc = /M(.*?)A/, //Everything between the capital M and first capital A
middleLoc = /A(.*?)0 0 1/, //Everything between the capital A and 0 0 1
endLoc = /0 0 1 (.*?)$/; //Everything between the 0 0 1 and the end of the string (denoted by $)
//Flip the direction of the arc by switching the start and end point (and sweep flag)
var newStart = endLoc.exec( newArc )[1];
var newEnd = startLoc.exec( newArc )[1];
var middleSec = middleLoc.exec( newArc )[1];
//Build up the new arc notation, set the sweep-flag to 0
newArc = "M" + newStart + "A" + middleSec + "0 0 0 " + newEnd;
}//if
return newArc
}
}
}
function chordTween(chord) {
return function(d,i) {
var i = d3.interpolate(chord[i], d);
return function(t) {
return ribbon(i(t));
}
}
}
// function simpleTween(d) {
// return function(d,i) {
// console.log(d)
// var oldPath = d3.select("#hiddenArc-"+i)
// ._groups[0][0].getAttribute("d")
// console.log(oldPath)
// var firstArcSection = /(^.+?)L/;
// var newPath = firstArcSection.exec(this.getAttribute("d"))[1];
// newPath = newPath.replace(/,/g , " ");
// if (d.endAngle > 90 * Math.PI/180 && d.endAngle < 315 * Math.PI/180) {
// var startLoc = /M(.*?)A/, //Everything between the capital M and first capital A
// middleLoc = /A(.*?)0 0 1/, //Everything between the capital A and 0 0 1
// endLoc = /0 0 1 (.*?)$/; //Everything between the 0 0 1 and the end of the string (denoted by $)
// //Flip the direction of the arc by switching the start and end point (and sweep flag)
// var newStart = endLoc.exec( newPath )[1];
// var newEnd = startLoc.exec( newPath )[1];
// var middleSec = middleLoc.exec( newPath )[1];
// //Build up the new arc notation, set the sweep-flag to 0
// newPath = "M" + newStart + "A" + middleSec + "0 0 0 " + newEnd;
// }//if
// var i = d3.interpolate(newPath, oldPath);
// return function(t) {
// return i(t);
// }
// }
// }
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment