|
<!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> |