D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
saadkhalid90
Full window
Github gist
Stretched chord based on Visual Cinnamon example
<!DOCTYPE html> <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