/*** Define parameters and tools ***/ var width = 760, height = 820, outerRadius = Math.min(width, height) / 2 - 120,//100, innerRadius = outerRadius - 10; var dataset = "foursix.json"; //string url for the initial data set //would usually be a file path url, here it is the id //selector for the
element storing the data //create number formatting functions var formatPercent = d3.format("%"); var numberWithCommas = d3.format("0,f"); //create the arc path data generator for the groups var arc = d3.svg.arc() .innerRadius(innerRadius) .outerRadius(outerRadius); //create the chord path data generator for the chords var path = d3.svg.chord() .radius(innerRadius - 4);// subtracted 4 to separate the ribbon //define the default chord layout parameters //within a function that returns a new layout object; //that way, you can create multiple chord layouts //that are the same except for the data. function getDefaultLayout() { return d3.layout.chord() .padding(0.03) .sortSubgroups(d3.descending) .sortChords(d3.ascending); } var last_layout; //store layout between updates var regions; //store neighbourhood data outside data-reading function /*** Initialize the visualization ***/ var g = d3.select("#chart_placeholder").append("svg") .attr("width", width) .attr("height", height) .append("g") .attr("id", "circle") .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); //the entire graphic will be drawn within thiselement, //so all coordinates will be relative to the center of the circle g.append("circle") .attr("r", outerRadius); //this circle is set in CSS to be transparent but to respond to mouse events //It will ensure that the responds to all mouse events within //the area, even after chords are faded out. /*** Read in the neighbourhoods data and update with initial data matrix ***/ //normally this would be done with file-reading functions //d3. and d3.json and callbacks, //instead we're using the string-parsing functions //d3.csv.parse and JSON.parse, both of which return the data, //no callbacks required. d3.csv("regionsfish.csv", function(error, regionData) { if (error) {alert("Error reading file: ", error.statusText); return; } regions = regionData; //store in variable accessible by other functions //regions = d3.csv.parse(d3.select("#regions").text()); //instead of d3.csv updateChords(dataset); //call the update method with the default dataset }); //end of d3.csv function /* Create OR update a chord layout from a data matrix */ function updateChords( datasetURL ) { d3.json(datasetURL, function(error, matrix) { if (error) {alert("Error reading file: ", error.statusText); return; } //var matrix = JSON.parse( d3.select(datasetURL).text() ); // instead of d3.json /* Compute chord layout. */ layout = getDefaultLayout(); //create a new layout object layout.matrix(matrix); /* Create/update "group" elements */ var groupG = g.selectAll("g.group") .data(layout.groups(), function (d) { return d.index; //use a key function in case the //groups are sorted differently }); groupG.exit() .transition() .duration(1500) .attr("opacity", 0) .remove(); //remove after transitions are complete var newGroups = groupG.enter().append("g") .attr("class", "group"); //the enter selection is stored in a variable so we can //enter the , , and elements as well //Create the title tooltip for the new groups newGroups.append("title"); //Update the (tooltip) title text based on the data groupG.select("title") .text(function(d, i) { return numberWithCommas(d.value) + " x (10\u00B3) in USD exports from " + regions[i].name; }); //create the arc paths and set the constant attributes //(those based on the group index, not on the value) newGroups.append("path") .attr("id", function (d) { return "group" + d.index; //using d.index and not i to maintain consistency //even if groups are sorted }) .style("fill", function (d) { return regions[d.index].color; }); //update the paths to match the layout groupG.select("path") .transition() .duration(1500) //.attr("opacity", 0.5) //optional, just to observe the transition//////////// .attrTween("d", arcTween( last_layout )) // .transition().duration(100).attr("opacity", 1) //reset opacity////////////// ; //create the group labels newGroups.append("svg:text") .attr("xlink:href", function (d) { return "#group" + d.index; }) .attr("dy", ".35em") .attr("color", "#fff") .text(function (d) { return regions[d.index].name; }); //position group labels to match layout groupG.select("text") .transition() .duration(1500) .attr("transform", function(d) { d.angle = (d.startAngle + d.endAngle) / 2; //store the midpoint angle in the data object return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" + " translate(" + (innerRadius + 26) + ")" + (d.angle > Math.PI ? " rotate(180)" : " rotate(0)"); //include the rotate zero so that transforms can be interpolated }) .attr("text-anchor", function (d) { return d.angle > Math.PI ? "end" : "begin"; }); /* Create/update the chord paths */ var chordPaths = g.selectAll("path.chord") .data(layout.chords(), chordKey ); //specify a key function to match chords //between updates //create the new chord paths var newChords = chordPaths.enter() .append("path") .attr("class", "chord"); // Add title tooltip for each new chord. newChords.append("title"); // Update all chord title texts chordPaths.select("title") .text(function(d) { if (regions[d.target.index].name !== regions[d.source.index].name) { return [numberWithCommas(d.source.value), " exports from ", regions[d.source.index].name, " to ", regions[d.target.index].name, "\n", numberWithCommas(d.target.value), " exports from ", regions[d.target.index].name, " to ", regions[d.source.index].name ].join(""); //joining an array of many strings is faster than //repeated calls to the '+' operator, //and makes for neater code! } else { //source and target are the same return numberWithCommas(d.source.value) + " exports ended in " + regions[d.source.index].name; } }); //handle exiting paths: chordPaths.exit().transition() .duration(1500) .attr("opacity", 0) .remove(); //update the path shape chordPaths.transition() .duration(1500) //.attr("opacity", 0.5) //optional, just to observe the transition .style("fill", function (d) { return regions[d.source.index].color; }) .attrTween("d", chordTween(last_layout)) //.transition().duration(100).attr("opacity", 1) //reset opacity ; //add the mouseover/fade out behaviour to the groups //this is reset on every update, so it will use the latest //chordPaths selection groupG.on("mouseover", function(d) { chordPaths.classed("fade", function (p) { //returns true if *neither* the source or target of the chord //matches the group that has been moused-over return ((p.source.index != d.index) && (p.target.index != d.index)); }); }); //the "unfade" is handled with CSS :hover class on g#circle //you could also do it using a mouseout event: /* g.on("mouseout", function() { if (this == g.node() ) //only respond to mouseout of the entire circle //not mouseout events for sub-components chordPaths.classed("fade", false); }); */ last_layout = layout; //save for next update }); //end of d3.json } function arcTween(oldLayout) { //this function will be called once per update cycle //Create a key:value version of the old layout's groups array //so we can easily find the matching group //even if the group index values don't match the array index //(because of sorting) var oldGroups = {}; if (oldLayout) { oldLayout.groups().forEach( function(groupData) { oldGroups[ groupData.index ] = groupData; }); } return function (d, i) { var tween; var old = oldGroups[d.index]; if (old) { //there's a matching old group tween = d3.interpolate(old, d); } else { //create a zero-width arc object var emptyArc = {startAngle:d.startAngle, endAngle:d.startAngle}; tween = d3.interpolate(emptyArc, d); } return function (t) { return arc( tween(t) ); }; }; } 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' } function chordTween(oldLayout) { //this function will be called once per update cycle //Create a key:value version of the old layout's chords array //so we can easily find the matching chord //(which may not have a matching index) var oldChords = {}; if (oldLayout) { oldLayout.chords().forEach( function(chordData) { oldChords[ chordKey(chordData) ] = chordData; }); } return function (d, i) { //this function will be called for each active chord var tween; var old = oldChords[ chordKey(d) ]; if (old) { //old is not undefined, i.e. //there is a matching old chord value //check whether source and target have been switched: if (d.source.index != old.source.index ){ //swap source and target to match the new data old = { source: old.target, target: old.source }; } tween = d3.interpolate(old, d); } else { //create a zero-width chord object ///////////////////////////////////////////////////////////in the copy //////////////// if (oldLayout) { var oldGroups = oldLayout.groups().filter(function(group) { return ( (group.index == d.source.index) || (group.index == d.target.index) ) }); old = {source:oldGroups[0], target:oldGroups[1] || oldGroups[0] }; //the OR in target is in case source and target are equal //in the data, in which case only one group will pass the //filter function if (d.source.index != old.source.index ){ //swap source and target to match the new data old = { source: old.target, target: old.source }; } } else old = d; ///////////////////////////////////////////////////////////////// var emptyChord = { source: { startAngle: old.source.startAngle, endAngle: old.source.startAngle}, target: { startAngle: old.target.startAngle, endAngle: old.target.startAngle} }; tween = d3.interpolate( emptyChord, d ); } return function (t) { //this function calculates the intermediary shapes return path(tween(t)); }; }; } /* Activate the buttons and link to data sets */ d3.select("#foursix").on("click", function () { updateChords( "foursix.json" ); //replace this with a file url as appropriate //enable other buttons, disable this one // disableButton(this); }); d3.select("#sevennine").on("click", function() { updateChords( "sevennine.json" ); // disableButton(this); }); d3.select("#tentwelve").on("click", function() { updateChords( "tentwelve.json" ); //disableButton(this); }); d3.select("#thirteenfiveteen").on("click", function() { updateChords( "thirteenfiveteen.json" ); //disableButton(this); }); /* function disableButton(buttonNode) { d3.selectAll("button") .attr("disabled", function(d) { return this === buttonNode? "true": null; }); } */