// Check breakpoint function breakCalc(x){ x <= 480 ? y = 'xs' : y = 'md'; return y; } var breakpoint = breakCalc($(window).width()); $(window).resize(function(){ var breakpoint = breakCalc($(window).width()); }) // change the height of the chart depending on the breakpoint function breakHeight(bp){ bp == 'xs' ? y = 250 : y = 500; return y; } // function to group by multiple properties in underscore.js _.groupByMulti = function (obj, values, context) { if (!values.length) return obj; var byFirst = _.groupBy(obj, values[0], context), rest = values.slice(1); for (var prop in byFirst) { byFirst[prop] = _.groupByMulti(byFirst[prop], rest, context); } return byFirst; }; // function to decide whether to pluralize the word "award" in the tooltip function awardPlural(x){ x == 1 ? y = 'award' : y = 'awards'; return y; } // funciton to determine the century of the datapoint when displaying the tooltip function century(x){ x<100 ? y = '19'+x : y = '20'+(x.toString().substring(1)); return y; } // function to ensure the tip doesn't hang off the side function tipX(x){ var winWidth = $(window).width(); var tipWidth = $('.tip').width(); if (breakpoint == 'xs'){ x > winWidth - tipWidth - 20 ? y = x-tipWidth : y = x; } else { x > winWidth - tipWidth - 30 ? y = x-45-tipWidth : y = x+10; } return y; } // function to create the chart function chart(column, filterBy, groupBy) { // basic chart dimensions var margin = {top: 20, right: 1, bottom: 30, left: 0}; var width = $('.chart-wrapper').width() - margin.left - margin.right; var height = breakHeight(breakpoint) - margin.top - margin.bottom; // chart top used for placing the tooltip var chartTop = $('.chart.'+groupBy+'.'+filterBy).offset().top; // tooltip var tooltip = d3.select("body") .append("div") .attr("class", "tip") .style("position", "absolute") .style("z-index", "20") .style("visibility", "hidden") .style("top", 40+chartTop+"px"); // scales: // x is a time scale, for the horizontal axis // y is a linear (quantitative) scale, for the vertical axis // z is in ordinal scale, to determine the colors (see var colorrange, below) var x = d3.time.scale() .range([0, width]); var y = d3.scale.linear() .range([height-10, 0]); // color range provided by colorbrewer // i just added a bunch of grays at the end so that the categories grouped as other all appear gray. // there's definitely a better way to do this var colorrange = ['#66c2a5','#fc8d62','#8da0cb','#e78ac3','#a6d854','#ffd92f','#e5c494','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3','#b3b3b3']; var z = d3.scale.ordinal() .range(colorrange); // the x-axis. note that the ticks are years, and we'll show every 5 years var xAxis = d3.svg.axis() .scale(x) .orient("bottom") .ticks(d3.timeYears, 5); // stacked layout. the order is reversed to get the largest value on top // if you change the order to inside-out, the streams get all mixed up and look cool // but the graph is harder to read. reversed order ensures that the streams are in the // same order as the legend, which improves readability in lieu of directly labelling // the streams (which is another programming challenge entirely) var stack = d3.layout.stack() .offset("silhouette") .order("reverse") .values(function(d) { return d.values; }) .x(function(d) { return d.date; }) .y(function(d) { return d.value; }); var nest = d3.nest() .key(function(d) { return d.key; }); // there are some ways other than "basis" to interpolate the area between data points // for example, you can use "cardinal", which makes the streams a little more wiggly. // the drawback with that approach is that if you have years where there is no data, // you won't see a flat line across the center of the chart. instead, it will look all bumpy. // ultimately, "cardinal" interpolation is more likely to give an inaccurate represenation of the data, // which is anyway a danger with any type of interpolation, including "basis" var area = d3.svg.area() .interpolate("basis") .x(function(d) { return x(d.date); }) .y0(function(d) { return y(d.y0)-.2; }) // -.2 to create a little space between the layers .y1(function(d) { return y(d.y0 + d.y)+.2; }); // +.2, likewise var svg = d3.select(".chart."+groupBy+'.'+filterBy).append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // generate a legend function legend(layers){ // generate the legend title function titler(filter,group){ if (group == 'place') { if (filter == 'india'){ return "State"; } else { return "Country"; } } } $('.chart.'+groupBy+'.'+filterBy).prepend('
'+titler(filterBy,groupBy)+'
'); $('.legend').hide(); var legend = [] layers.forEach(function(d,i){ var obj = {}; if (i<7){ obj.key = d.key; obj.color = colorrange[i]; legend.push(obj); } }); // others if (layers.length>7){legend.push({key: "Other",color: "#b3b3b3"});} legend.forEach(function(d,i){ $('.chart.'+groupBy+'.'+filterBy+' .legend').append('
'+d.key+'
'); }); $('.legend').fadeIn(); }// end legend function // parse the data function parse(data){ // this filters and groups the data // based on the filters provided in the .chart div (see the html file) var filter; var searchObj = {}; searchObj[column] = filterBy; if (column=="none"){ filter=data; } else { filter = _.where(data,searchObj); } var categories = _.chain(filter) .countBy(groupBy) .pairs() .sortBy(1).reverse() .pluck(0) .value(); var sort = _.sortBy(filter,categories); // group by var group = _.groupByMulti(sort, ['year', groupBy]) var newData = []; // it is necessary to add an extra year to the data (as well as duplicate the data for the final year) // so that the chart does not get cut off on the right side for (var i = 1954;i<2018;i++){ var currYear = group[i]; // no data for a year if (currYear == undefined) { currYear = {}; } categories.forEach(function(area){ var obj = {}; if (currYear[area] == undefined){ // if the year does not have any in a particular category obj.key = area; obj.value = 0; obj.date = moment(i.toString())._d; } else { obj.key = currYear[area][0][groupBy]; obj.value = currYear[area].length; obj.date = moment(currYear[area][0].year)._d; } newData.push(obj); }); } data = newData;// you could just return newData, but this way seems cleaner to me return data; } // now we call the data, as the rest of the code is dependent upon data d3.csv("awards.csv", function(data) { // parse the data (see parsing function, above) data = parse(data); // generate our layers var layers = stack(nest.entries(data)); // our legend is based on our layers legend(layers); // set the domains x.domain(d3.extent(data, function(d) { return d.date; })); y.domain([0, d3.max(data, function(d) { return d.y0 + d.y; })]); // and now we're on to the data joins and appending svg.selectAll(".layer") .data(layers) .enter().append("path") .attr("class", "layer") .attr("d", function(d) { return area(d.values); }) .style("fill", function(d, i) { return z(i); }); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); // abbreviate axis tick text on small screens if (breakpoint == 'xs') { $('.x.axis text').each(function(){ var curTxt = $(this).text(); var newTxt = "'"+curTxt.substr(2); $(this).text(newTxt); }); } // user interaction with the layers svg.selectAll(".layer") .attr("opacity", 1) .on("mouseover", function(d, i) { svg.selectAll(".layer").transition() .duration(100) .attr("opacity", function(d, j) { return j != i ? 0.6 : 1; })}) .on("mousemove", function(d, i) { var color = d3.select(this).style('fill'); // need to know the color in order to generate the swatch mouse = d3.mouse(this); mousex = mouse[0]; var invertedx = x.invert(mousex); var xDate = century(invertedx.getYear()); d.values.forEach(function(f){ var year = (f.date.toString()).split(' ')[3]; if (xDate == year){ tooltip .style("left", tipX(mousex) +"px") .html( "
" + year + "
 
" + f.key + "
" + f.value + " " + awardPlural((f.value)) + "
" ) .style("visibility", "visible"); } }); }) .on("mouseout", function(d, i) { svg.selectAll(".layer").transition() .duration(100) .attr("opacity", '1'); tooltip.style("visibility", "hidden"); }); // vertical line to help orient the user while exploring the streams var vertical = d3.select(".chart."+groupBy+'.'+filterBy) .append("div") .attr("class", "remove") .style("position", "absolute") .style("z-index", "19") .style("width", "2px") .style("height", "460px") .style("top", "10px") .style("bottom", "30px") .style("left", "0px") .style("background", "#fcfcfc"); d3.select(".chart."+groupBy+'.'+filterBy) .on("mousemove", function(){ mousex = d3.mouse(this); mousex = mousex[0] + 5; vertical.style("left", mousex + "px" )}) .on("mouseover", function(){ mousex = d3.mouse(this); mousex = mousex[0] + 5; vertical.style("left", mousex + "px")}); // Add 'curtain' rectangle to hide entire graph var curtain = svg.append('rect') .attr('x', -1 * width) .attr('y', -1 * height) .attr('height', height) .attr('width', width) .attr('class', 'curtain') .attr('transform', 'rotate(180)') .style('fill', '#fcfcfc') // Create a shared transition for anything we're animating var t = svg.transition() .delay(100) .duration(1500) .ease('exp') .each('end', function() { d3.select('line.guide') .transition() .style('opacity', 0) .remove() }); t.select('rect.curtain') .attr('width', 0); t.select('line.guide') .attr('transform', 'translate(' + width + ', 0)'); }); } // get the various arguments from the chart div attributes // if you're making one chart, this approach is unnecessary // however, for several stream graphs on one page, this approach is useful // it allows you to decide how to query the data in the html by assigning various // attributes to the chart div. var column = $('.chart').attr("column"); var groupBy = $('.chart').attr("groupBy"); var filterBy = $('.chart').attr("filterBy"); $('.chart').addClass(groupBy).addClass(filterBy); chart(column,filterBy,groupBy);