var width = 500, height = 200, margin = 50, multipleWidth = 150; var secondsPerHour = 60 * 60; // sum the splits and round down to the hour var bucketByHour = function(splits) { var totalTime = d3.sum(splits); var hours = totalTime / secondsPerHour; return Math.floor(hours); }; var histGenerator = d3.histogram() .domain([-20,20]) .thresholds(20); // compute the split ratio for an array of split times, this is the time for the second // half of the race minus the first half. Negative numbers mean they ran faster in the // second half of the race var splitRatio = function(s) { return (d3.sum(s.slice(14, 27)) - d3.sum(s.slice(0, 13))) / 60; }; var inRange = function(range) { return function (value) { return value < range[0] && value > range[1]; } }; // compute the number of splits that are negative var proportionNegative = function(splitRatios) { return splitRatios.filter(function(d) { return d < 0; }).length / splitRatios.length; } var findMode = function(buckets) { var modeIndex = d3.scan(buckets, function(a, b) { return b.length - a.length; }); return buckets[modeIndex]; } d3.text('splits.csv', function(text) { var splits = d3.csvParseRows(text, function(d) { return d.map(Number); }); // group splits by finish time (in hours) var grouped = d3.nest() .key(bucketByHour) .entries(splits) .sort(function(a, b) { return d3.ascending(+a.key, +b.key); }); var yDomain = [20, -20]; var yScale = d3.scaleLinear() .domain(yDomain) .range([height, 0]); var xExtent = fc.extentLinear() .accessors([function(d) { return d.length; }]); // render the first 4 groups for (var i = 1; i < 5; i++) { // compute the split ratios var splitRatios = grouped[i].values.map(splitRatio); // bucket to create a histogram var bucketedSplits = histGenerator(splitRatios.filter(inRange(yDomain))); var xScale = d3.scaleLinear() .domain(xExtent(bucketedSplits)) .range([0, multipleWidth - 20]); var bar = fc.seriesSvgBar() .crossValue(function(d) { return (d.x0 + d.x1) / 2; }) .mainValue(function(d) { return d.length; }) .barWidth(fc.seriesFractionalBarWidth(1.0)) .orient('horizontal') .xScale(xScale) .yScale(yScale) .decorate(function(sel) { sel.attr('fill', function(d) { return d.x0 >= 0 ? '#fc8d59' : '#91cf60'; }); }); var ratio = proportionNegative(splitRatios); var modeBucket = findMode(bucketedSplits); var modeSplit = (modeBucket.x0 + modeBucket.x1) / 2; var g = d3.select('#chart') .append('g') .attr('transform', 'translate(' + ((i - 1) * multipleWidth + margin) +', ' + margin + ')') g.datum(bucketedSplits) .call(bar) g.append('text') .attr('transform', 'translate(10, -10)') .text((grouped[i].key) + ' - ' + (+grouped[i].key + 1) + ' hours'); g.append('text') .attr('transform', 'translate(10, 230)') .text('ratio: ' + d3.format(".0%")(ratio)); g.append('text') .attr('transform', 'translate(10, 250)') .text('mode: ' + (modeSplit) + ' min'); } d3.select('#chart') .append('g') .attr('transform', 'translate(' + (margin - 5) + ', ' + margin + ')') .call(d3.axisLeft(yScale)); });