// Inaccurate data representation, ho! // The data we'll be using is stored as a CSV in a text block in the HTML section. // See the comments preceded with TOFIX to see where data is skewed. ////////////////////////////////////////////////////////////////////////////// // Parameters ////////////////////////////////////////////////////////////////////////////// var width = 500; var height = 500; var colorMax = d3.rgb(26, 93, 173); var colorMin = d3.rgb(238, 41, 61); ////////////////////////////////////////////////////////////////////////////// // Derived values ////////////////////////////////////////////////////////////////////////////// // Donut chart parameters: var rMax = Math.min(width / 2, height / 2); var rMin = rMax / 2; var rCenter = rMin / 2; ////////////////////////////////////////////////////////////////////////////// // Helper functions ////////////////////////////////////////////////////////////////////////////// var translate = function(x, y) {return "translate(" + x + "," + y + ")";} var roundTo = function(value, decimals) { var push = Math.pow(10, decimals); var rounded = Math.round(value * push); var pull = rounded / push; return pull; } ////////////////////////////////////////////////////////////////////////////// // Parse data into a JavaScript object ////////////////////////////////////////////////////////////////////////////// // Get the block of text with the ID data (see the HTML). var csv = d3.select("#data").text(); // Treat the block of text as a CSV document. // d3.csv.parse converts the data into an array of JS objects. var data = d3.csv.parse(csv, function(d) { // Note that CSVs are read as text. The unary + converts them into numbers. var obj = { year: +d["Year"], dollars: +d["Nominal Dollars"], percent: +d["Percent"], adjusted: +d["Adjusted for 2014"] // TOFIX: We don't even use the data adjusted for inflation! }; return obj; }); ////////////////////////////////////////////////////////////////////////////// // Generate DOM elements ////////////////////////////////////////////////////////////////////////////// var header = d3.select("body") .append("p") .style("text-align", "center") .text("Amount of US Federal Budget alloted for NASA") ; // Determine the range of values allowed for the slider. var minYear = d3.min(data, function(d){return d.year;}); var maxYear = d3.max(data, function(d){return d.year;}); // Determine the initial value of the slider. var maxPercent = d3.max(data, function(d){return d.percent;}); var defaultObj = data.filter(function(d){return (d.percent === maxPercent);})[0]; var defaultYear = defaultObj.year; // Create the DOM elements for the slider. var sliderContainer = d3.select("body") .append("p") .style("text-align", "center") ; var label = sliderContainer .append("label") .attr("for", "time") // Tag for the slider. .style("display", "block") .text("Year: " + defaultYear) ; var slider = sliderContainer .append("input") .attr("type", "range") .attr("id", "time") // ID for the label. .style("display", "block") // The following three styles center the slider. .style("position", "relative") .style("margin-left", "auto") .style("margin-right", "auto") // TOFIX: We really should use minYear instead of defaultYear, // but we want to make an inaccurate point, no? .attr("min", defaultYear) .attr("max", maxYear) .attr("value", defaultYear) // Whenever the user moves the slider, update the label and graphic. .on("change", function() // Can also use "mouseup" or "input". 5165579 { label.text("Year: " + this.value); update(); }) ; // Prepare a container for the SVG graphic. var svgContainer = d3.select("body") .append("p") .style("text-align", "center") ; var svg = svgContainer.append("svg") .attr("width", width) .attr("height", height) ; var g = svg.append("g") .attr("transform", translate(width / 2, height / 2)) ; ////////////////////////////////////////////////////////////////////////////// // D3 mappers and graphic generators. ////////////////////////////////////////////////////////////////////////////// // Scale the dollar value per datum to a radius value. var rScale = d3.scale.log() // TOFIX: I could use this scale instead, but this would be accurate! // d3.scale.sqrt() .domain(d3.extent(data, function(d){return d.dollars;})) .range([rMin, rMax]); // Scale the percentage value per datum to an angle value. var thetaScale = d3.scale.pow().exponent(2) // TOFIX: I could use this scale instead, but this would be accurate! // d3.scale.linear() .domain(d3.extent(data, function(d){return d.percent;})) // TOFIX: I could use this scale instead, but this would be accurate! // .domain([0, 1]) .range([0, 2 * Math.PI]); // Scale the percentage value per datum to a RGB value. var colorScale = d3.scale.pow().exponent(2) // TOFIX: I could use this scale instead, but this would be accurate! // d3.scale.linear() .domain(d3.extent(data, function(d){return d.percent;})) // TOFIX: I could use this scale instead, but this would be accurate! // .domain([0, 1]) .range([colorMin, colorMax]); // Quick function to get r from the datum. var getR = function(d){return rScale(d.dollars);} // Quick function to get theta from the datum. var getTheta = function(d){return thetaScale(d.percent);} // Quick function to get color from the datum. var getColor = function(d){return colorScale(d.percent);} // Generates an arc corresponding to the datum. var arc = d3.svg.arc() .innerRadius(rCenter) .outerRadius(function(d){return getR(d);}) .startAngle(0) .endAngle(function(d){return getTheta(d);}) ; // Generates the rest of the arc ring corresponding to the datum. var ring = d3.svg.arc() .innerRadius(rCenter) .outerRadius(function(d){return d;}) .startAngle(0) .endAngle(2 * Math.PI) ; ////////////////////////////////////////////////////////////////////////////// // Generate and animate the SVG ////////////////////////////////////////////////////////////////////////////// // Initialize variables to keep track of the previous value and this value, // in order to animate smoothly. var prevYear = +slider.node().value; var year = prevYear; // These values are singleton arrays. // We keep them as singletons because D3 expects arrays as parameters. // (After all, D3 stands for DATA-driven documents, not datum-driven documents.) var prevDatum = data.filter(function(d){return (d.year === prevYear);}); var datum = prevDatum; // This function is called whenever the user moves the slider. var update = function() { // Update the year according to the new slider value. year = +slider.node().value; // The value is initially a string, hence the +. datum = data.filter(function(d){return (d.year === year);}); // Determine how to animate the arc depending on the change of datum. var arcTween = function(d) { var rInterpolator = d3.interpolate(prevDatum[0].dollars, datum[0].dollars); var thetaInterpolator = d3.interpolate(prevDatum[0].percent, datum[0].percent); var tween = function(t) { // Make a dummy object that has the parameters needed for arc. var tweenObject = { dollars: rInterpolator(t), percent: thetaInterpolator(t) } return arc(tweenObject); } return tween; } // Update the data given, using the same index. // We do not provide a selector function for data, since we want to keep the same index. var join = g.selectAll("g").data(datum); // For every new element in the join, we create a group (and more later). var gEnter = join.enter().append("g"); // For every updated element in the join, we do stuff later. var gUpdate = join; // For every element that left the join, we remove it. join.exit().remove(); // Create and update the ring. gEnter .append("path") .attr("id", "ring") .attr("fill", "gray") .attr("d", function(d){return ring(getR(d));}) ; gUpdate.select("#ring").transition() .attr("d", function(d){return ring(getR(d));}); // Create and update the arc. gEnter .append("path") .attr("id", "slice") .attr("stroke", "black") .attr("fill", function(d){return getColor(d);}) .attr("d", function(d){return arc(d);}) ; gUpdate.select("#slice").transition() .attr("fill", function(d){return getColor(d);}) .attrTween("d", arcTween) .each("end", function() { // We update prevYear and prevDatum only when the animation ends, // as otherwise, there is a race condition. prevYear = year; prevDatum = datum; }) ; // Create and update the text for dollars. gEnter .append("text") .attr("id", "dollars") .attr("transform", translate(0, -10)) // arbitrary translation .attr("text-anchor", "middle") .attr("dominant-baseline", "middle") .text(function(d){return "$" + d.dollars + " m";}); gUpdate.select("#dollars") .text(function(d){return "$" + d.dollars + " m";}); // Create and update the text for percent. gEnter .append("text") .attr("id", "percent") .attr("transform", translate(0, 10)) // arbitrary translation .attr("text-anchor", "middle") .attr("dominant-baseline", "middle") .text(function(d){return roundTo(d.percent * 100, 2) + "%";}); gUpdate.select("#percent") .text(function(d){return roundTo(d.percent * 100, 2) + "%";}); } // Render SVG for the first time. update();
<!-- This is the data set as a CSV. --> <pre style = "display:none" id = "data"> Year,Nominal Dollars,Percent,Adjusted for 2014 1958,89,0.0010,732 1959,145,0.0020,1185 1960,401,0.0050,3222 1961,744,0.0090,5918 1962,1257,0.0118,9900 1963,2552,0.0229,19836 1964,4171,0.0352,32002 1965,5092,0.0431,38448 1966,5933,0.0441,43554 1967,5425,0.0345,38633 1968,4722,0.0265,32274 1969,4251,0.0231,27550 1970,3752,0.0192,23000 1971,3382,0.0161,19862 1972,3423,0.0148,19477 1973,3312,0.0135,17742 1974,3255,0.0121,15704 1975,3269,0.0098,14452 1976,3671,0.0099,15345 1977,4002,0.0098,15707 1978,4164,0.0091,15190 1979,4380,0.0087,14349 1980,4959,0.0084,14314 1981,5537,0.0082,14488 1982,6155,0.0083,15170 1983,6853,0.0085,16365 1984,7055,0.0083,16150 1985,7251,0.0077,16028 1986,7403,0.0075,16065 1987,7591,0.0076,15893 1988,9092,0.0085,18280 1989,11036,0.0096,21168 1990,12429,0.0099,22618 1991,13878,0.0105,24235 1992,13961,0.0101,23668 1993,14305,0.0101,23546 1994,13695,0.0094,21979 1995,13378,0.0088,20879 1996,13881,0.0089,21042 1997,14360,0.0090,21280 1998,14194,0.0086,20712 1999,13636,0.0080,19467 2000,13428,0.0075,18547 2001,14095,0.0076,18940 2002,14405,0.0072,19045 2003,14610,0.0068,18885 2004,15152,0.0066,19078 2005,15602,0.0063,19001 2006,15125,0.0057,17844 2007,15861,0.0058,18194 2008,17833,0.0060,19700 2009,17782,0.0057,19714 2010,18724,0.0052,20423 2011,18448,0.0051,17833 2012,17770,0.0050,17471 2013,16865,0.0049,17219 2014,17647,0.0050,17647 </pre>
p { /* Font applied to each paragraph (but not the SVG). */ font-family: "Titillium Web", "Helvetica Neue", Helvetica, Arial, sans-serif; }