xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
background-color: #EEEEEE;
display: flex;
flex-direction: column;
}
h3 {
font: 16px sans-serif;
}
.groups text{
font: 10px sans-serif;
}
.group_title {
font: 18px sans-serif;
text-anchor: middle;
text-decoration: underline;
}
#VizContain {
display: flex;
justify-content: center;
}
.group-tick line {
stroke: #000;
}
.ribbons {
fill-opacity: 0.75;
fill: #B0BEC5
}
.Dummy {
opacity: 0.0
}
#ButtonContain {
display: flex;
justify-content: center;
}
</style>
<h3 align="center">Relationship between 'birth attendants' and 'place of birth' of 15,971 mothers across Punjab (Punjab Health Survey Round II)</h3>
<div id=ButtonContain>
<button id="urbanButton">Urban</button>
<button id="ruralButton">Rural</button>
<button id="AllButton" disabled="true" >All</button>
</div>
<div id=VizContain>
<svg width="960" height="720"></svg>
</div>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="d3_stretched_chord.js"></script>
<script src="d3.layout.chord.sort.js"></script>
<script src="data.js"></script>
<script>
// by default, we use matrix of th entire data (both urban and rural)
var early_matrix = early_matrix;
// names of categories in group 1 and group 2 (group 2 in opposite order)
var group1_names = ["Private hospital", "Government hospital", "Govt Mother & Child Health center", "Private clinic/ maternity home", "Respondent's home", "Other home", "Other public", "Rural Health Center", "Basic Health Unit", "Other_","CMW home/birth station","Other private medical"];
var group2_names = ["Other","No one","Relatives/ Friends","Nurse","Lady Health Vistor","Traditional Birth Attendant","Community Midwife","Doctor"];
// combining group 1 and group 2 and including dummies between the two
var group_names = group1_names.concat("Dummy", group2_names, "Dummy");
// Total number of groups
total_no = early_matrix[1].length;
// stating empty area percentage and offset degrees
empty_area_perc = 0.4;
empty_am = total_no * empty_area_perc;
var offset_angle = ((empty_am)/(empty_am + total_no)) * Math.PI/ 2;
var offset_degrees = offset_angle * (180/ Math.PI);
// functions to correct start and end angles with a specified offset
function startAngle(d) {return d.startAngle + offset_angle};
function endAngle(d) {return d.endAngle + offset_angle};
// function to add dummy arrays (extra zeros) in the matrix for stretched chord
function add_dummies_to_matrix(matrix, empty_area_perc){
// create a single array from the input matrix
var matrix_arr = matrix.reduce(function(result, elem){return result.concat(elem)})
// take the sum of the above array and divide by 2 (to get the total sum)
var total_sum = matrix_arr.reduce(function(result, elem) { return result + elem })/ 2;
// gettimg the number of sub groups in group 1 and total number of subgroups
var group2_no = group2_names.length;
var group1_no = group1_names.length;
var total_no = matrix[1].length;
// adding zeros for dummys
for (var i = 0; i < matrix.length; i++){
//early_matrix[i] = early_matrix[i].push(0);
var group1_vals = matrix[i].slice(0, group1_no);
var group2_vals = matrix[i].slice(group1_no,total_no);
matrix[i] = group1_vals.concat(0, group2_vals, 0);
}
// getting empty/ dummy no through the percentage of empty/ dummy arc
var empty_no = empty_area_perc * total_sum;
// adding the empty_no into the dummy arrays
zeros1 = a = new Array(total_no + 2).fill(0);
zeros1[total_no + 2 - 1] = empty_no;
zeros1 = [zeros1];
zeros2 = new Array(total_no + 2).fill(0);
zeros2[group1_no] = empty_no;
zeros2 = [zeros2];
// cutting the matrix into two halfs to put in dummy arrays
matrix_1 = matrix.slice(0, group1_no);
matrix_2 = matrix.slice(group1_no, total_no);
// concatenating dummy arrays with the matrix halves
var matrix_with_dummy = matrix_1.concat(zeros1, matrix_2, zeros2);
return matrix_with_dummy;
}
// adding dummy arrays to the three data sets (All, urban and rural)
var matrix = add_dummies_to_matrix(early_matrix, empty_area_perc);
var matrix_urban = add_dummies_to_matrix(early_matrix_urban, empty_area_perc);
var matrix_rural = add_dummies_to_matrix(early_matrix_rural, empty_area_perc);
// getting the attributes of the svg from the html
// calculating inner and outer radii with that info
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
outerRadius = Math.min(width, height) * 0.5 - 85 , //205
arc_width = 13
innerRadius = outerRadius - arc_width;
// using d3v3 to init function that will produce data for chords from matrix
var chord = customChordLayout()
.padding(.015)
.sortChords(d3.descending)
// init function that will create paths for group arcs
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(startAngle)
.endAngle(endAngle);
// stating pullOutSize (separation for the stretched chord)
var pullOutSize = 40;
// init function that will create paths for chord ribbons
var ribbon = stretchedChord()
.radius(innerRadius)
.startAngle(startAngle)
.endAngle(endAngle)
.pullOutSize(pullOutSize);
// group at the center of SVG
var g = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
// groups mapped to each category
var group = g.append("g")
.attr("class", "groups")
.selectAll("g")
.data(chord.matrix(matrix).groups())
.enter().append("g")
.attr("class", function(d) { return group_names[d.index]})
.classed("holder_group", true);
// filtering non dummy groups
var non_dummy_grps = d3.selectAll(".groups")
.selectAll("g :not(.Dummy)");
// laying the group arcs
group.append("path")
.style("fill", "#6699ff")
.attr("class", function(d) { return group_names[d.index] } ) // each arc classed as group's name
.classed("group_arc", true)
.attr("d", arc)
.attr("transform",function(d, i){
// putting in pulloutsize in the bound data and using translate to pull out group arcs
d.pullOutSize = pullOutSize * ((d.startAngle + d.endAngle)/2 > Math.PI ? -1 : 1);
return "translate(" + d.pullOutSize + "," + 0 + ")";
})
// holder group consists of group arcs and text labels
d3.selectAll('.holder_group')
.on("mouseover", fade_group(0.05, 1, 1))
.on("mouseout", fade_group(0.75, 0.1, 0));
// try adding the group labels
var label_pad = 10;
group.append("text")
.attr("class", function(d) { return group_names[d.index] } )
.each(function(d) { d.angle = (d.startAngle + d.endAngle) / 2; })
.text(function(d, i) { return group_names[d.index] } )
.attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
.attr("transform", function(d) {
// pullout size depends on whether the groups lie to the left or right
d.pullOutSize = pullOutSize * (d.angle > Math.PI ? -1 : 1);
return "translate(" + d.pullOutSize + ") "
+ "rotate(" + ((d.angle + offset_angle) * 180 / Math.PI - 90) + ")"
+ "translate(" + (outerRadius + label_pad) + ")"
+ (d.angle > Math.PI ? "rotate(180)" : "");
})
.style("opacity", function(d, i) { return (d.value/ 100) < 0.01 ? 0.1 : 1 });
// adding the ribbons for chords
g.append("g")
.attr("class", "ribbons")
.selectAll("path")
.data(chord.matrix(matrix).chords())
.enter().append("path")
.attr("d", ribbon)
.attr("class", function(d, i) { return d.target.index == total_no + 1 ? "Dummy" : "NonDummy"; })
.classed("ribbon_path", true);
// Returns an event handler for fading a given chord group.
function fade(opacity1, opacity2, text_opacity) {
return function(d, i) {
var group_idx = d.index
var value = d.value;
d3.selectAll(".ribbon_path")
//.filter(function(d) { return d.source.index != group_idx && d.target.index != group_idx; })
.transition()
.style("fill-opacity", function(d, i){
return d.source.index == group_idx || d.target.index == group_idx ? opacity2: opacity1;
opacity1
});
// d3.selectAll(".ribbon_path")
// .filter(function(d) { return d.source.index == group_idx && d.target.index == group_idx; })
// .transition()
// .style("fill-opacity", opacity2);
// remove transparency of little groups when mouse is hovered
d3.select(this).select('text')
.filter(function(d) { return d.value/ 100 < 0.01 })
.transition()
.style("opacity", text_opacity);
};
};
// Returns an event handler for fading a given chord group
function fade_group(opacity, text_opacity, add_title) {
return function(d, i) {
d3.select("g.ribbons").selectAll("path.NonDummy")
.filter(function(d) { return d.source.index !== i && d.target.index !== i; })
.transition()
.style("fill-opacity", opacity);
// remove transparency of little groups when mouse is hovered
d3.select(this).select('text')
.filter(function(d) { return d.value/ 100 < 0.01 })
.transition()
.style("opacity", text_opacity);
if (add_title){
d3.select(this).append("title")
.text(group_names[d.index] + ' - ' + precisionRound(d.value, 2) + '%');
}
else {
d3.select(this).select("title").remove();
}
};
}//fade
// Fade function when hovering over chord
function fadeOnChord(opac1, opac2, add_title) {
return function(d){
var chosen = d;
d3.select("g.ribbons").selectAll("path.NonDummy")
.transition()
.style("fill-opacity", function(d) {
return d.source.index === chosen.source.index && d.target.index === chosen.target.index ? opac1 : opac2;
});
var ind_val = d3.select(this).data()[0].source.value;
console.log(ind_val);
if (add_title){
d3.select(this).append("title")
.text(function(d){
return group_names[d.target.index] + " delivered at " + group_names[d.source.index] + " - (" + precisionRound(ind_val, 2) + "%)";
})
}
else {
d3.select(this).select("title").remove()
}
}
}//fadeOnChord
d3.select("g.ribbons").selectAll("path.NonDummy")
.on("mouseover", fadeOnChord(0.75, 0, 1))
.on("mouseout", fadeOnChord(0.75, 0.75, 0));
// transition duration for switching between categories
var transition_duration = 1000;
// function to update the stretched chord
function update_chords(new_matrix){
// storing the previous datasets (separately for chord and ribbons)
var old_data_groups = g.select('.groups').selectAll('g').data();
var old_data_chords = g.select('.ribbons').selectAll('path').data();
// updating the matrix and chord data
var updated_data = chord.matrix(new_matrix);
// update the data of children groups
group.data(updated_data.groups());
// update data in the texts
group.selectAll("text").data(updated_data.groups())
// apply transitions to text labels for groups
group.select("text")
.each(function(d) { d.angle = (d.startAngle + d.endAngle) / 2; })
.transition()
.duration(transition_duration)
.attr("transform", function(d) {
d.pullOutSize = pullOutSize * (d.angle > Math.PI ? -1 : 1);
return "translate(" + d.pullOutSize + ") "
+ "rotate(" + ((d.angle + offset_angle) * 180 / Math.PI - 90) + ")"
+ "translate(" + (outerRadius + label_pad) + ")"
+ (d.angle > Math.PI ? "rotate(180)" : "");
})
.style("opacity", function(d, i) { return (d.value/ 100) < 0.01 ? 0.1 : 1 });
// making the transition in group arc paths
group.select("path")
.data(updated_data.groups())
.transition()
.duration(transition_duration)
.attrTween("d", arcTween(old_data_groups));
// update data of the group holding ribbons
g.select('.ribbons').data(updated_data.chords());
// make changes to the data bound to the ribbons as data changes
var ribbon_selection = g.select('.ribbons').selectAll('path');
// checking if ribbons are to be added or deleted (for enter or exit in d3)
if (updated_data.length > ribbon_selection.data().length){
ribbon_selection.data(updated_data.chords(), chordKey)
.enter()
.append("path")
}
else {
ribbon_selection.data(updated_data.chords(), chordKey).exit()
.remove();
}
// making transitions for the ribbons in chords
g.select('.ribbons').selectAll('path').transition()
.duration(transition_duration)
.attrTween("d", ribbonTween(old_data_chords));
}
// function for tweening the arcs for groups
function arcTween(prev_data){
return function(d, i) {
return function(t) {
arc_interp = d3.interpolate(prev_data[i], d);
return arc(arc_interp(t));
}
}
};
var index_test = 0;
// function for tweening ribbons
function ribbonTween(prev_data){
return function(d, i){
var old_with_key = prev_data.map(function(old_d){
return {
key: chordKey(old_d),
data: old_d
};
})
// getting the new datum to match with the old (by using chordKey)
var old = old_with_key.filter(function(old_obj_d){
return old_obj_d.key == chordKey(d);
}).map(function(entry) {return entry.data});
// converting the array into object
var old_data = old[0]
// trans_data will hold old datum if a match is made, otherwise it will hold
// empty chord
var trans_data
if (old.length == 1){
trans_data = old_data
}
else {
var emptyChord = {
source: { startAngle: d.source.startAngle,
endAngle: d.source.startAngle},
target: { startAngle: d.target.startAngle,
endAngle: d.target.startAngle}
};
trans_data = emptyChord;
}
//console.log(d);
//console.log(d.source.startAngle);
//console.log(i);
return function(t) {
rib_interp = d3.interpolate( trans_data, d );
index_test ++;
//console.log(t);
//console.log(rib_interp(t));
return ribbon(rib_interp(t));
}
}
};
/* Activate the buttons and link to data sets */
d3.select("#urbanButton").on("click", function () {
update_chords( matrix_urban );
// disable the button if pressed
disableButton(this);
});
d3.select("#ruralButton").on("click", function() {
update_chords( matrix_rural );
disableButton(this);
});
d3.select("#AllButton").on("click", function() {
update_chords( matrix );
disableButton(this);
});
function chordKey(data) {
return (data.source.index < data.target.index) ?
data.source.index + "-" + data.target.index:
data.target.index + "-" + data.source.index;
//create a key that will represent the relationship
//between these two groups *regardless*
//of which group is called 'source' and which 'target'
};
// disable button function
function disableButton(buttonNode) {
d3.selectAll("button")
.attr("disabled", function(d) {
return this === buttonNode? "true": null;
});
};
// function to add titles for the two groups
function add_group_titles(title1, title2, move_to_center){
g.append("text")
.attr("class", "group_title")
.text(title1)
.attr("x", - outerRadius - pullOutSize/2 + move_to_center)
.attr("y", - outerRadius - pullOutSize/4)
g.append("text")
.attr("class", "group_title")
.text(title2)
.attr("x", + outerRadius + pullOutSize/2 - move_to_center)
.attr("y", - outerRadius - pullOutSize/4)
}
function precisionRound(number, precision) {
var factor = Math.pow(10, precision);
return Math.round(number * factor) / factor;
}
// calling the function and adding titles
add_group_titles("Birth Attendant", "Place of Delivery", 0)
</script>
https://d3js.org/d3.v3.min.js