Last active
July 14, 2017 12:31
-
-
Save cjimmy/2e226a6e88d02f743679 to your computer and use it in GitHub Desktop.
D3.js code to create the data visualizations seen on http://jimmychion.com/lavaman2015.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//-- author: IDEO | Jimmy Chion | 2013 | |
//-- license: Creative Commons SA-3.0 | |
(function(){ | |
//------------------------------------ | |
//----- setting up the graph, axis | |
//------------------------------------ | |
var margin = {top: 20, right: 5, bottom: 100, left: 60}, | |
width = 0.58*($(document).width()) - margin.left - margin.right, | |
height = 600 - margin.top - margin.bottom; | |
if ( width > 0.75*1140 ) { width = 0.75*1140-margin.left-margin.right; } //-- max-width in js | |
if ( $(document).width() < 1100 ) { width = 0.9*$(document).width() - margin.right - margin.left;} | |
var x = d3.scale.linear() | |
.range([0, width]); | |
var y = d3.scale.linear() | |
.range([0, height]); | |
var color = d3.scale.ordinal() | |
.domain(['me (Jimmy)','passed','passed by']) | |
.range(['#ff0000', '#BBB', '#000']); | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient("bottom"); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.orient("left") | |
.tickValues([1, 250, 500, 750, 1000]) | |
.tickFormat(function(d) { | |
if (d%10 == 1) { | |
return (d+"st"); | |
} else { | |
return (d+"th"); | |
} | |
}); | |
var race = d3.select("#raceviz").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 + ")"); | |
//------------------------------------ | |
//----- importing the data | |
//------------------------------------ | |
d3.csv("static/data/lavaman2015/results.csv", function(d) { | |
return { | |
place: +d.place, | |
// divTot: d.divTot, | |
bib: +d.bib, | |
// cat: d.cat, | |
firstName: d.firstName, | |
lastName: d.lastName, | |
age: +d.age, | |
sex: d.sex, | |
div: d.div, | |
swimRank: +d.swimRank, | |
swimTime: +d.swimTime, | |
// swimPace: +d.swimPace, | |
t1Time: +d.t1Time, | |
bikeRank: +d.bikeRank, | |
bikeTime: +d.bikeTime, | |
// bikeSpeed: +d.bikeSpeed, | |
// t2Rank: +d.t2Rank, | |
t2Time: +d.t2Time, | |
runRank: +d.runRank, | |
runTime: +d.runTime, | |
// runPace: +d.runPace, | |
totalTime: +d.totalTime, | |
wave: +d.wave, | |
passedSwim: +d.wavePassedSwim, | |
passedT1: +d.wavePassedT1, | |
passedBike: +d.wavePassedBike, | |
passedT2: +d.wavePassedT2, | |
passedRun: +d.wavePassedRun, | |
startPassedT1: +d.startPassedT1, | |
startPassedBike: +d.startPassedBike, | |
startPassedT2: +d.startPassedT2, | |
startPassedRun: +d.startPassedRun, | |
estimatedT1Rank: +d.estimatedT1Rank | |
}; | |
}, function(error, data) { | |
x.domain(d3.extent(data, function(d) { | |
return d.totalTime; | |
})).nice(); | |
y.domain(d3.extent(data, function(d) { | |
return d.place; //-- start with overall rank as y-axis | |
})).nice(); | |
//-- adding to prototype to convert s to HH:MM:SS. thanks stack overflow | |
Number.prototype.toHHMMSS = function () { | |
var sec_num = parseInt(this, 10); // don't forget the second param | |
var hours = Math.floor(sec_num / 3600); | |
var minutes = Math.floor((sec_num - (hours * 3600)) / 60); | |
var seconds = sec_num - (hours * 3600) - (minutes * 60); | |
if (hours < 10) {hours = hours;} | |
if (minutes < 10) {minutes = "0"+minutes;} | |
if (seconds < 10) {seconds = "0"+seconds;} | |
var time = hours+':'+minutes+':'+seconds; | |
return time; | |
} | |
//-- math to proportionally split up the triathlon into a linear piece | |
//-- based on 500th place, making that as smooth as possible | |
var divis = 92; //-- num of divisions | |
var t1Marker = 2*(divis-2)/9*width/divis; | |
var bikeMarker = (2*(divis-2)/9+1)*width/divis; | |
var t2Marker = (6*(divis-2)/9+1)*width/divis; | |
var runMarker = (6*(divis-2)/9+2)*width/divis; | |
var currentHighlightedBibNum = 122; | |
//------------------------------------ | |
//----- lines and labels | |
//------------------------------------ | |
race.append("g") | |
.attr("class", "y axis") | |
.call(yAxis) | |
.append("text") | |
.attr("class", "y axis") | |
.attr("y", 16) | |
.attr("dy", ".71em") | |
.attr("dx", "-0.71em") | |
.style("text-anchor", "end") | |
.text("place") | |
//-- add marker for start | |
race.append("text") | |
.attr("class", "markerLabel") | |
.attr("transform", "translate( 3 " + (height+5) + ") rotate(-90)") | |
.style("text-anchor", "end") | |
.text("Start"); | |
//-- text label for swim | |
race.append("text") | |
.attr("class", "markerLabel") | |
.attr("transform", "translate(" + t1Marker/2 + " " + (height+12) + ")") | |
.style("text-anchor", "middle") | |
.text("Swim 1.5km (1mi)"); | |
//-- add marker for t-1 | |
race.append("line") | |
.attr("x1",t1Marker ) | |
.attr("y1",0) | |
.attr("x2",t1Marker ) | |
.attr("y2",height) | |
.attr("stroke-width", 0.5) | |
.attr("stroke", "gray") | |
.style("stroke-dasharray", ("3,3")); | |
race.append("text") | |
.attr("class", "markerLabel") | |
.attr("transform", "translate(" + (t1Marker+bikeMarker)/2 + " " + (height+12) + ") rotate(0)") | |
.style("text-anchor", "middle") | |
.text("T-1"); | |
//-- add marker for bike | |
race.append("line") | |
.attr("x1",bikeMarker) | |
.attr("y1",0) | |
.attr("x2",bikeMarker) | |
.attr("y2",height) | |
.attr("stroke-width", 0.5) | |
.attr("stroke", "gray") | |
.style("stroke-dasharray", ("3,3")); | |
race.append("text") | |
.attr("class", "markerLabel") | |
.attr("transform", "translate(" + (bikeMarker+t2Marker)/2 + " " + (height+12) + ") rotate(0)") | |
.style("text-anchor", "middle") | |
.text("Bike 40km (25mi)"); | |
//-- add marker for t-2 | |
race.append("line") | |
.attr("x1", t2Marker) | |
.attr("y1",0) | |
.attr("x2", t2Marker ) | |
.attr("y2",height) | |
.attr("stroke-width", 0.5) | |
.attr("stroke", "gray") | |
.style("stroke-dasharray", ("3,3")); | |
race.append("text") | |
.attr("class", "markerLabel") | |
.attr("transform", "translate(" + (t2Marker+runMarker)/2 + " " + (height+12) + ") rotate(0)") | |
.style("text-anchor", "middle") | |
.text("T-2"); | |
//-- add marker for run | |
race.append("line") | |
.attr("x1", runMarker) | |
.attr("y1",0) | |
.attr("x2", runMarker ) | |
.attr("y2",height) | |
.attr("stroke-width", 0.5) | |
.attr("stroke", "gray") | |
.style("stroke-dasharray", ("3,3")); | |
race.append("text") | |
.attr("class", "markerLabel") | |
.attr("transform", "translate(" + (runMarker+width)/2 + " " + (height+12) + ") rotate(0)") | |
.style("text-anchor", "middle") | |
.text("Run 10km (6mi)"); | |
//-- add marker for finish | |
race.append("line") | |
.attr("x1",width) | |
.attr("y1",0) | |
.attr("x2", width ) | |
.attr("y2",height) | |
.attr("stroke-width", 0.5) | |
.attr("stroke", "black"); | |
race.append("text") | |
.attr("class", "markerLabel") | |
.attr("transform", "translate(" + (width+2) + " " + (height+5) + ") rotate(-90)") | |
.style("text-anchor", "end") | |
.text("Finish"); | |
//------------------------------------ | |
//----- create the dots | |
//------------------------------------ | |
var athletes = race.selectAll(".dot") | |
.data(data) | |
.enter().append("circle") | |
.attr("class", "dot") | |
.attr("r", 2.5) | |
.attr("cx", 0) | |
.attr("cy", function(d) { return y(d.place); }); | |
// draw legend | |
var legend = race.selectAll(".legend") | |
updateLegend(); | |
//------------------------------------ | |
//----- coloring the dots | |
//------------------------------------ | |
var passColorsUpdateTimer; | |
function initPassColors() { | |
color.domain([highlightedName,'passed','passed by']) | |
.range(['#ff0000', '#BBB', '#000']); | |
passColorsUpdateTimer = setInterval(updateDotsPassColors, 100); | |
} | |
function stopPassColors() { | |
window.clearInterval(passColorsUpdateTimer); | |
} | |
function updateDotsPassColors() { | |
var highlightedX = d3.select("#highlightAthlete").attr("cx"); | |
var nInFront = 0; | |
d3.selectAll(".dot").each( function (d, i) { | |
var xloc = Number(d3.select(this).attr('cx')); | |
if (xloc < width) { //-- 'this' is still racing | |
if (xloc < highlightedX) { | |
var thisStyle = d3.select(this).style("fill"); | |
if ( thisStyle == '#ff0000' || thisStyle == 'rgb(255, 0, 0)') { //-- if red, after the first cycle go back to gray | |
d3.select(this).style("fill", d3.rgb("#bbbbbb")); | |
} else if ( thisStyle != '#bbbbbb' && thisStyle != 'rgb(187, 187, 187)' ) { //-- when first passing, color specially | |
d3.select(this).style("fill", d3.rgb("#ff0000")); | |
} | |
} else if (xloc != highlightedX) { | |
nInFront++; | |
d3.select(this).style("fill", d3.rgb("#333")); | |
} | |
} else { //-- maintain the count of people in front | |
nInFront++; | |
} | |
}); | |
d3.select("#highlightAthlete").style("fill", d3.rgb("#ff0000")); | |
if ( highlightedX < width ) { //-- only change colors while the highlighted person is racing | |
d3.select("#current-rank").text(nInFront); | |
} | |
}; | |
function changeDotsToSexColors() { | |
color.domain(['M','F']) | |
.range(['#336699','#FF5050']); | |
athletes.style("fill", function(d) { | |
if (d.sex == 'M') { | |
return d3.rgb('#336699'); | |
} else { | |
return d3.rgb('#FF5050'); | |
} | |
}); | |
} | |
function changeDotsToWaveColors() { | |
color.domain(['Pro & Relays','M 0-44','M 45-54', 'M 55+', 'F 0-39', 'F 40-49', 'F 50+']) | |
.range(['#FF1A4B','#D70CE8','#791AFF','#0C20E8','#1A9EFF','#03E8D3','#0AFF65']); | |
athletes.style("fill", function(d) { | |
switch (d.wave) { | |
case 1: | |
return d3.rgb("#FF1A4B"); | |
break; | |
case 2: | |
return d3.rgb("#D70CE8"); | |
break; | |
case 3: | |
return d3.rgb("#791AFF"); | |
break; | |
case 4: | |
return d3.rgb("#0C20E8"); | |
break; | |
case 5: | |
return d3.rgb("#1A9EFF"); | |
break; | |
case 6: | |
return d3.rgb("#03E8D3"); | |
break; | |
case 7: | |
return d3.rgb("#0AFF65"); | |
break; | |
default: | |
return d3.rgb("#888"); | |
} | |
}); | |
} | |
function resetColors() { | |
var colSel = $('input[name=colorType]:checked').attr('id'); //-- get the radio selection | |
if (colSel == 'pass') { | |
initPassColors(); | |
} else if (colSel == 'sex') { | |
stopPassColors(); | |
changeDotsToSexColors(); | |
} else if (colSel == 'waves') { | |
stopPassColors(); | |
changeDotsToWaveColors(); | |
} | |
updateLegend(); | |
} | |
//-- controller for the input field to highlight a certain athlete | |
//-- returns whether something was found | |
function highlightBibNumber(bib) { | |
var found = false; | |
var highlighted = athletes.filter(function(d) { | |
if (d.bib == bib) { | |
found = true; | |
highlightedName = d.firstName; | |
return d; | |
} | |
}); | |
color.domain([highlightedName,'passed','passed by']) | |
.range(['#ff0000', '#BBB', '#000']); | |
$('#pass:checked').val(); | |
if ($('#pass:checked').val()) { | |
updateLegend(); | |
} //-- only update if checked. | |
//-- remove previous highlighted athlete | |
if ( found ) { | |
d3.select("#highlightAthlete") | |
.attr('id', '') | |
.style('stroke', 'none') | |
.attr('r', 2.5); | |
} | |
//-- highlight new one | |
highlighted.attr("id", "highlightAthlete") | |
.style('fill', d3.rgb("#FF5050")) | |
.style('stroke', d3.rgb("#333")) | |
.attr('r', 5); | |
return found; | |
} | |
//---------------------------------------------------------------- | |
function changeStatsForBibNum( startType ) { | |
athletes.filter(function(d) { | |
if (d.bib == currentHighlightedBibNum) { | |
d3.selectAll('.athlete-first-name').text(d.firstName); | |
d3.selectAll('.athlete-last-name').text(d.lastName); | |
if ( startType == 'waveStart' ){ | |
$('#swim-caveat-footnote').hide(); | |
d3.select('#ppl-passed-swim').html(Math.abs(d.passedSwim) + '</span><sup><span class=\'footnote-link\' id=\'swim-wave\'>[i]</span></sup>').style('color', getSignColor(d.passedSwim)); | |
d3.select('#ppl-passed-t1').text(Math.abs(d.passedT1)).style('color', getSignColor(d.passedT1)); | |
d3.select('#ppl-passed-bike').text(Math.abs(d.passedBike)).style('color', getSignColor(d.passedBike)); | |
d3.select('#ppl-passed-t2').text(Math.abs(d.passedT2)).style('color', getSignColor(d.passedT2)); | |
d3.select('#ppl-passed-run').text(Math.abs(d.passedRun)).style('color', getSignColor(d.passedRun)); | |
d3.select('#ppl-passed-total').text(Math.abs(d.passedSwim+d.passedT1+d.passedBike+d.passedT2+d.passedRun)).style('color', getSignColor(d.passedSwim+d.passedT1+d.passedBike+d.passedT2+d.passedRun)); | |
$('#swim-wave').on('click',function( event ) { | |
$('#' + event.target.id + '-footnote').slideToggle(350); | |
}); | |
} else { | |
$('#swim-wave-footnote').hide(); | |
d3.select('#ppl-passed-swim').html('<span class=\'stats-num-exception\'>n/a</span><sup><span class=\'footnote-link\' id=\'swim-caveat\'>[i]</span></sup>').style('color', d3.rgb('#999')); | |
d3.select('#ppl-passed-t1').text(Math.abs(d.startPassedT1)).style('color', getSignColor(d.startPassedT1)); | |
d3.select('#ppl-passed-bike').text(Math.abs(d.startPassedBike)).style('color', getSignColor(d.startPassedBike)); | |
d3.select('#ppl-passed-t2').text(Math.abs(d.startPassedT2)).style('color', getSignColor(d.startPassedT2)); | |
d3.select('#ppl-passed-run').text(Math.abs(d.startPassedRun)).style('color', getSignColor(d.startPassedRun)); | |
d3.select('#ppl-passed-total').text(Math.abs(d.startPassedT1+d.startPassedBike+d.startPassedT2+d.startPassedRun)).style('color', getSignColor(d.startPassedT1+d.startPassedBike+d.startPassedT2+d.startPassedRun)); | |
$('#swim-caveat').on('click',function( event ) { | |
$('#' + event.target.id + '-footnote').slideToggle(350); | |
}); | |
} | |
//-- show 'you passed' or 'passed you' this is brutish, but I couldn't figure out how to do the selection in d3 | |
var legs = ['swim','t1','bike','t2','run','total']; | |
for (var i = 0; i < legs.length; ++i ) { | |
$('#you-passed-'+ legs[i] + '-label').css('opacity', '0'); | |
$('#passed-you-'+ legs[i] + '-label').css('opacity', '0'); | |
if ( $('#ppl-passed-' + legs[i]).css("color") == "rgb(17, 136, 17)" ) { | |
$('#you-passed-'+ legs[i] + '-label').css('opacity', '1'); | |
} else if ($('#ppl-passed-' + legs[i]).css("color") == "rgb(255, 80, 80)" ) { | |
$('#passed-you-'+ legs[i] + '-label').css('opacity', '1'); | |
} | |
} | |
return d; | |
} | |
}); | |
} | |
//-- returns red or green depending on sign, used for changeStatsForBibNum | |
function getSignColor(pplPassed) { | |
if(pplPassed > 0) { | |
return d3.rgb("#118811"); //--green | |
} else if (pplPassed < 0) { | |
return d3.rgb("#FF5050"); //--red | |
} else { | |
return d3.rgb('#000'); | |
} | |
} | |
//-- I think there's a better way to do this. can't figure it out right now | |
function updateLegend() { | |
legend.remove(); | |
legend = race.selectAll('.legend') | |
.data(color.domain()) | |
.enter().append("g") | |
.attr("class", "legend") | |
.attr("transform", function(d, i) { return "translate(" + (30) + "," + (i * 20) + ")"; }); | |
legend.append("circle") | |
.attr("cx", 0) | |
.attr("r", 5) | |
.style("fill", color); | |
// draw legend text | |
legend.append("text") | |
.attr("x", 10) | |
.attr("dy", ".35em") | |
.style("fill", "#999") | |
.style("text-anchor", "beginning") | |
.text(function(d) { | |
return d; | |
}); | |
} | |
//-- chain the different speeds together through chaining transitions | |
//-- could also listen for end, but animations don't end synchronously | |
function raceAnimation(isWave) { | |
var timeMultiplier = 20; | |
athletes.attr("cx", 0); //-- reset them to the beginning | |
swimPortion = athletes.transition() | |
.attr("cx", t1Marker) | |
.duration( function(d) { return x(d.swimTime*timeMultiplier); } ) // total time to complete race is your time in (s * 10)/1000 | |
.ease('linear') | |
.delay( function(d) { //-- wave start delay | |
if (isWave) { | |
return x((d.wave-1) * 300 * timeMultiplier); | |
} else { | |
return 0; | |
} | |
}); | |
t1Portion = swimPortion.transition() | |
.attr("cx",bikeMarker) | |
.duration( function(d) {return x(d.t1Time*timeMultiplier); } ); | |
bikePortion = t1Portion. transition() | |
.attr("cx",t2Marker) | |
.duration( function(d) {return x(d.bikeTime*timeMultiplier); } ); | |
t2Portion = bikePortion.transition() | |
.attr("cx",runMarker) | |
.duration( function(d) {return x(d.t2Time*timeMultiplier); } ); | |
runPortion = t2Portion.transition() | |
.attr("cx",width) | |
.duration( function(d) {return x(d.runTime*timeMultiplier); } ) | |
.call(endall, function() { raceAnimation(isWave); }); | |
} | |
function resetAllToStart(haveDelay) { | |
athletes.transition() //-- return atheletes to start | |
.attr("cx", 0) | |
.duration(300) | |
.delay( function() { return haveDelay ? 1200 : 0; }) | |
.call(endall, function() { //-- when all are back, call animation again | |
raceAnimation( $('#waveStart:checked').val()?true:false ); | |
}); | |
} | |
//-- thank you SO. allows to execute callback when all elements are done with the transition | |
function endall(transition, callback) { | |
if (transition.size() === 0) { callback() } | |
var n = 0; | |
transition | |
.each(function() { ++n; }) | |
.each("end", function() { if (!--n) callback.apply(this, arguments); }); | |
} | |
//-- actually do stuff | |
highlightBibNumber( currentHighlightedBibNum ); //-- highlight me first | |
changeStatsForBibNum( currentHighlightedBibNum ); | |
raceAnimation( $('#waveStart:checked').val()?true:false ); //-- begin animation | |
$('#wave-start-text').hide(); //-- hide a paragraph | |
resetColors(); //-- color appropriately | |
//------------------------------------ | |
//----- inputs for interactive elements | |
//------------------------------------ | |
$('input').on('change', inputChanged); //-- event handler. can't select with d3 unfortuntately. | |
//-- controllers for graph controls | |
function inputChanged() { | |
//-- if people passed and start together is checked, show estimated rank | |
if ( $('#startTogether').is(':checked') && $('#pass').is(':checked')) { | |
$('#estimated-rank').show(200); | |
} else { | |
$('#estimated-rank').hide(200); | |
} | |
if (this.name == "startType") { | |
resetAllToStart(false); | |
if (this.id == 'waveStart') { | |
changeStatsForBibNum('waveStart'); | |
$('#wave-start-text').show();//-- change text | |
$('#start-together-text').hide(); | |
} else if (this.id == 'startTogether') { | |
changeStatsForBibNum('startTogether'); | |
$('#wave-start-text').hide();//-- change text | |
$('#start-together-text').show(); | |
} | |
} | |
else if (this.name == 'colorType') { | |
resetColors(); | |
} | |
else if (this.name == 'rankBy') { | |
if (this.id == 'run') { | |
athletes.transition() | |
.delay(function(d) { | |
return d.place; | |
}) | |
.duration(300) | |
.attr("cy", function(d) { | |
return y(d.runRank); | |
}); | |
} | |
else if (this.id == 'overall') { | |
athletes.transition() | |
.delay(function(d) { | |
return d.place; | |
}) | |
.duration(300) | |
.attr("cy", function(d) { | |
return y(d.place); | |
}); | |
} | |
else if (this.id == 'swim') { | |
athletes.transition() | |
.delay(function(d) { | |
return d.place; | |
}) | |
.duration(300) | |
.attr("cy", function(d) { | |
return y(d.swimRank); | |
}); | |
} | |
else if (this.id == 'bike') { | |
athletes.transition() | |
.delay(function(d) { | |
return d.place; | |
}) | |
.duration(300) | |
.attr("cy", function(d) { | |
return y(d.bikeRank); | |
}); | |
} | |
resetAllToStart(true);//-- if changing rank, return to start | |
} | |
} | |
//-- on enter of text input field and on press of Find | |
$('#highlightBibTextInput').keydown(function(event) { | |
if (event.keyCode == 13) { handleFindBib(); } | |
}); | |
$('button[name=highlightBtn]').on("click", handleFindBib); | |
function handleFindBib(event) { | |
var requestedNum = $('#highlightBibTextInput').val(); | |
var bDidFindNum = highlightBibNumber(requestedNum); | |
if ( !bDidFindNum ) { //-- if can't find number, show error | |
if(d3.selectAll('.input-error')[0].length > 0) { //-- if there's already an error message, modify it | |
d3.select('.input-error').html("Sorry, couldn't find bib number - " + requestedNum + ". Athletes who had incomplete results (e.g. from a malfunctioning ankle band) are not included.") | |
} else { //-- otherwise, create it | |
d3.select('#highlight-form').append("div") | |
.attr("class", "input-error") | |
.html("Sorry, couldn't find that bib number " + requestedNum + ". Athletes who had incomplete data (e.g. from a malfunctioning ankle band) are not included.") | |
} | |
} else { //-- found a match | |
d3.select('.input-error').remove(); //-- found the num, remove old warnings | |
currentHighlightedBibNum = requestedNum; | |
changeStatsForBibNum( $('#waveStart:checked').val()?'waveStart':'startTogether' ); | |
resetAllToStart(false); | |
} | |
$('#highlightBibTextInput').val(''); //-- clear text input field on every enter | |
return false; | |
} | |
//-- for the little [i] caveats | |
$('.footnote-link').not('#swim-caveat').on('click',function( event ) { | |
$('#' + event.target.id + '-footnote').slideToggle(300); | |
}); | |
//-- for vertical button group -> horizontal | |
if ($(window).width() < 1200) { | |
$('.responsive-btn-vertical').removeClass('btn-group-vertical'); | |
$('.responsive-btn-vertical').addClass('btn-group'); | |
} else { | |
$('.responsive-btn-vertical').addClass('btn-group-vertical'); | |
$('.responsive-btn-vertical').removeClass('btn-group'); | |
} | |
}); | |
})(); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//-- author: IDEO | Jimmy Chion | 2013 | |
//-- license: Creative Commons SA-3.0 | |
(function(){ | |
// var margin = {top: 23, right: 5, bottom: 30, left: 80}, | |
// width = .75 * (3*$(document).width()/4 - margin.left - margin.right), | |
// height = 600 - margin.top - margin.bottom; | |
// if(width > 870) {width = 870-margin.left;} | |
var margin = {top: 50, right: 20, bottom: 30, left: 80}, | |
width = 0.76*($(document).width()) - margin.left - margin.right, | |
height = 600 - margin.top - margin.bottom; | |
if ( width > 1140 ) { width = 1140-margin.left-margin.right; } //-- max-width in js | |
var x = d3.scale.linear() | |
.range([0, width]); | |
var y = d3.scale.linear() | |
.range([0, height]); | |
var color = d3.scale.category10(); | |
color.range(['#336699', '#FF5050']); //-- colors of the graph blue red | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient("top"); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.orient("left") | |
.tickValues( [7200,10800,14400,18000]) | |
.tickFormat( function(d){ | |
return d.toHHMMSS(); | |
}); | |
var tip = d3.tip() | |
.attr('class', 'd3-tip') | |
.offset([-10, 0]) | |
.html(function(d) { | |
return ("<em>" + d.firstName + " " + d.lastName + "</em>, " + d.age + "<br>Time: " + d.totalTime.toHHMMSS() + "<br>Place: " + d.place); | |
}); | |
var scatterplot = d3.select("#scatterplot").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 + ")"); | |
scatterplot.call(tip); | |
d3.csv("static/data/lavaman2015/results.csv", function(d) { | |
return { | |
place: +d.place, | |
// divTot: d.divTot, | |
// bib: +d.bib, | |
// cat: d.cat, | |
firstName: d.firstName, | |
lastName: d.lastName, | |
age: +d.age, | |
sex: d.sex, | |
// div: d.div, | |
// swimRank: +d.swimRank, | |
// swimTime: +d.swimTime, | |
// swimPace: +d.swimPace, | |
// t1Time: +d.t1Time, | |
// bikeRank: +d.bikeRank, | |
// bikeTime: +d.bikeTime, | |
// bikeSpeed: +d.bikeSpeed, | |
// t2Rank: +d.t2Rank, | |
// t2Time: +d.t2Time, | |
// runRank: +d.runRank, | |
// runTime: +d.runTime, | |
// runPace: +d.runPace, | |
totalTime: +d.totalTime, | |
// wave: +d.wave, | |
// passedSwim: +d.passedSwim, | |
// passedT1: +d.passedT1, | |
// passedBike: +d.passedBike, | |
// passedT2: +d.passedT2, | |
// passedRun: +d.passedRun, | |
// estimatedT1Rank: +d.estimatedT1Rank | |
}; | |
}, function(error, data) { | |
x.domain(d3.extent(data, function(d) { | |
return d.age; | |
})); | |
y.domain(d3.extent(data, function(d) { | |
return d.totalTime; | |
})).nice(); | |
scatterplot.append("g") | |
.attr("class", "x axis") | |
.call(xAxis) | |
.append("text") | |
.attr("class", "label") | |
.attr("x", width) | |
.attr("y", 16) | |
.style("text-anchor", "end") | |
.text("age"); | |
scatterplot.append("g") | |
.attr("class", "y axis") | |
.call(yAxis) | |
.append("text") | |
.attr("class", "label") | |
.attr("transform", "translate(0," + (height-30) + ") rotate(-90)") | |
.attr("y", 6) | |
.attr("dy", ".71em") | |
.text("completion time"); | |
var scatterplotDots = scatterplot.selectAll(".age-vs-time-dot") | |
.data(data) | |
.enter().append("circle") | |
.attr("class", "age-vs-time-dot") | |
.attr("r", 3) | |
.attr("cx", function(d) { return x(d.age); }) | |
.attr("cy", function(d) { return y(d.totalTime)}) | |
.on('mouseover', tip.show) | |
.on('mouseout', tip.hide) | |
.style("fill", function(d) { return color(d.sex); }); | |
// draw legend | |
var legend = scatterplot.selectAll(".legend") | |
.data(color.domain()) | |
.enter().append("g") | |
.attr("class", "legend") | |
.attr("transform", function(d, i) { return "translate(0," + (i * 20 + 50) + ")"; }); | |
// draw legend colored rectangles | |
legend.append("circle") | |
.attr("cx", width - 14) | |
.attr("cy", 9) | |
.attr("r", 6) | |
.style("fill", color); | |
// draw legend text | |
legend.append("text") | |
.attr("x", width - 24) | |
.attr("y", 9) | |
.attr("dy", ".35em") | |
.style("text-anchor", "end") | |
.text(function(d) { | |
if (d == 'M') return 'Male'; | |
else return 'Female'; | |
}); | |
// get the x and y values for least squares | |
var xSeries_overall = data.map(function(d) { return parseFloat(d.age); }); | |
var ySeries_overall = data.map(function(d) { return parseFloat(d.totalTime); }); | |
var leastSquaresCoeffOverall = leastSquares(xSeries_overall, ySeries_overall); | |
var calculateTrendData = function(xSeries, leastSquaresArr){ | |
var x1 = d3.min(xSeries); | |
var y1 = leastSquaresCoeffOverall[0] * x1 + leastSquaresCoeffOverall[1]; | |
var x2 = d3.max(xSeries); | |
var y2 = leastSquaresCoeffOverall[0] * x2 + leastSquaresCoeffOverall[1]; | |
return [[x1,y1,x2,y2]]; | |
} | |
//-- apply the reults of the least squares regression | |
var overallTrendData = calculateTrendData(xSeries_overall, leastSquaresCoeffOverall); | |
var trendline = scatterplot.selectAll(".trendline") | |
.data(overallTrendData); | |
trendline.enter() | |
.append("line") | |
.attr("class", "trendline") | |
.attr("x1", function(d) { return x(d[0]); }) | |
.attr("y1", function(d) { return y(d[1]); }) | |
// .attr("x2", function(d) { return x(d[0]); }) | |
// .attr("y2", function(d) { return y(d[1]); }) | |
.attr("stroke", "black") | |
.attr("stroke-width", 1.0) | |
.style("stroke-dasharray", ("3,3")) | |
// .transition() | |
// .delay(500) | |
// .duration(500) | |
.attr("x2", function(d) { return x(d[2]); }) | |
.attr("y2", function(d) { return y(d[3]); }); | |
//-- uncomment to see trend data on female vs male. spoiler: it's not exciting | |
var males = scatterplotDots.filter( function(d) { return (d.sex === 'M'); }); | |
var females = scatterplotDots.filter( function(d) { return (d.sex === 'F'); }); | |
var malesSeries = data.filter(function(d) {return d.sex == 'M';}); | |
var xSeries_male = malesSeries.map(function(d) { return parseFloat(d.age); }); | |
var ySeries_male = malesSeries.map(function(d) { return parseFloat(d.totalTime); }); | |
var femalesSeries = data.filter(function(d) { return d.sex == 'F';}); | |
var xSeries_female = femalesSeries.map(function(d) { return parseFloat(d.age); }); | |
var ySeries_female = femalesSeries.map(function(d) { return parseFloat(d.totalTime); }); | |
var leastSquaresCoeffMale = leastSquares(xSeries_male, ySeries_male); | |
var leastSquaresCoeffFemale = leastSquares(xSeries_female, ySeries_female); | |
var maleTrendData = calculateTrendData(xSeries_male, leastSquaresCoeffMale); | |
var femaleTrendData = calculateTrendData(xSeries_female, leastSquaresCoeffFemale); | |
trendline_male = scatterplot.selectAll(".trendline_male") | |
.data(maleTrendData); | |
trendline_male.enter() | |
.append("line") | |
.attr("class", "trendline") | |
.attr("x1", function(d) { return x(d[0]); }) | |
.attr("y1", function(d) { return y(d[1]); }) | |
.attr("x2", function(d) { return x(d[0]); }) | |
.attr("y2", function(d) { return y(d[1]); }) | |
.attr("stroke", "blue") | |
.attr("stroke-width", 1) | |
.style("stroke-dasharray", ("3,3")) | |
// .transition() | |
// .delay(1000) | |
// .duration(500) | |
// .attr("x2", function(d) { return x(d[2]); }) | |
// .attr("y2", function(d){ return y(d[3]); }); | |
trendline_female = scatterplot.selectAll(".trendline_female") | |
.data(femaleTrendData); | |
trendline_female.enter() | |
.append("line") | |
.attr("class", "trendline") | |
.attr("x1", function(d) { return x(d[0]); }) | |
.attr("y1", function(d) { return y(d[1]); }) | |
.attr("x2", function(d) { return x(d[0]); }) | |
.attr("y2", function(d) { return y(d[1]); }) | |
.attr("stroke", "red") | |
.attr("stroke-width", 1) | |
.style("stroke-dasharray", ("3,3")) | |
// .transition() | |
// .delay(1500) | |
// .duration(500) | |
// .attr("x2", function(d) { return x(d[2]); }) | |
// .attr("y2", function(d) { return y(d[3]); }); | |
//-- display equation on the chart | |
// scatterplot.append("text") | |
// .attr("class", "text-label") | |
// .attr("x", width-200) | |
// .attr("y", height-80) | |
// .text("eq: " + decimalFormat(leastSquaresCoeff[0]) + "x + " + decimalFormat(leastSquaresCoeff[1])); | |
//-- display r-square on the chart | |
// decimalFormat = d3.format("0.3f"); | |
// scatterplot.append("text") | |
// .attr("class", "text-label") | |
// .attr("x", width-200) | |
// .attr("y", height-60) | |
// .text("r-sq: " + decimalFormat(leastSquaresCoeffOverall[2])); | |
//------------------------------------ | |
//----- inputs for interactive elements | |
//------------------------------------ | |
$('input[name=scatterAge]').on('change', inputHandler); //- | |
function inputHandler() { | |
if (this.id == 'scatterFemale') { | |
females.transition() | |
.attr('r', 3) | |
.duration(300); | |
males.transition() | |
.attr('r', 0) | |
.duration(500) | |
.delay(100); | |
trendline_male.attr("x2", function(d) { return x(d[0]); }) | |
.attr("y2", function(d){ return y(d[1]); }); | |
trendline.attr("x2", function(d) { return x(d[0]); }) | |
.attr("y2", function(d){ return y(d[1]); }); | |
trendline_female.transition() | |
.delay(500) | |
.duration(500) | |
.attr("x2", function(d) { return x(d[2]); }) | |
.attr("y2", function(d){ return y(d[3]); }); | |
} else if (this.id == 'scatterMale') { | |
males.transition() | |
.attr('r', 3) | |
.duration(300); | |
females.transition() | |
.attr('r', 0) | |
.duration(500) | |
.delay(100); | |
trendline_female.attr("x2", function(d) { return x(d[0]); }) | |
.attr("y2", function(d){ return y(d[1]); }); | |
trendline.attr("x2", function(d) { return x(d[0]); }) | |
.attr("y2", function(d){ return y(d[1]); }); | |
trendline_male.transition() | |
.delay(500) | |
.duration(500) | |
.attr("x2", function(d) { return x(d[2]); }) | |
.attr("y2", function(d){ return y(d[3]); }); | |
} else { | |
males.transition() | |
.attr('r', 3) | |
.duration(300); | |
females.transition() | |
.attr('r', 3) | |
.duration(300); | |
trendline_male.attr("x2", function(d) { return x(d[0]); }) | |
.attr("y2", function(d){ return y(d[1]); }); | |
trendline_female.attr("x2", function(d) { return x(d[0]); }) | |
.attr("y2", function(d){ return y(d[1]); }); | |
trendline.transition() | |
.delay(500) | |
.duration(500) | |
.attr("x2", function(d) { return x(d[2]); }) | |
.attr("y2", function(d){ return y(d[3]); }); | |
} | |
} | |
}); | |
})(); | |
// returns slope, intercept and r-square of the line | |
function leastSquares(xSeries, ySeries) { | |
var reduceSumFunc = function(prev, cur) { return prev + cur; }; | |
var xBar = xSeries.reduce(reduceSumFunc) * 1.0 / xSeries.length; | |
var yBar = ySeries.reduce(reduceSumFunc) * 1.0 / ySeries.length; | |
var ssXX = xSeries.map(function(d) { return Math.pow(d - xBar, 2); }) | |
.reduce(reduceSumFunc); | |
var ssYY = ySeries.map(function(d) { return Math.pow(d - yBar, 2); }) | |
.reduce(reduceSumFunc); | |
var ssXY = xSeries.map(function(d, i) { return (d - xBar) * (ySeries[i] - yBar); }) | |
.reduce(reduceSumFunc); | |
var slope = ssXY / ssXX; | |
var intercept = yBar - (xBar * slope); | |
var rSquare = Math.pow(ssXY, 2) / (ssXX * ssYY); | |
return [slope, intercept, rSquare]; | |
} | |
//-- function to convert s to HH:MM:SS. thanks stack overflow | |
Number.prototype.toHHMMSS = function () { | |
var sec_num = parseInt(this, 10); // don't forget the second param | |
var hours = Math.floor(sec_num / 3600); | |
var minutes = Math.floor((sec_num - (hours * 3600)) / 60); | |
var seconds = sec_num - (hours * 3600) - (minutes * 60); | |
if (hours < 10) {hours = hours;} | |
if (minutes < 10) {minutes = "0"+minutes;} | |
if (seconds < 10) {seconds = "0"+seconds;} | |
var time = hours+':'+minutes+':'+seconds; | |
return time; | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//-- author: IDEO | Jimmy Chion | 2013 | |
//-- license: Creative Commons SA-3.0 | |
(function(){ | |
//------------------------------------ | |
//----- setting up the graph, axis | |
//------------------------------------ | |
var margin = {top: 30, right: 0, bottom: 33, left: (($('#rankingviz').width())-400)/2 }, | |
width = 400, | |
height = 180 - margin.top - margin.bottom; | |
if ($(document).width() < 500) { | |
width = 250; | |
margin.left = $(document).width()*.12; | |
} | |
var x = d3.scale.linear() | |
.domain([0,1]) | |
.range([0, width]); | |
var y = d3.scale.ordinal() | |
// .domain(["Overall","Swim","T-1","Bike","T-2","Run"]) //-- uncomment to see T-1 (estimated) and T-2 percentiles | |
.domain(["Overall","Swim","Bike","Run"]) | |
.rangeBands([0, height]); | |
// var color = d3.scale.category10(); | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient("bottom"); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.orient("left"); | |
var rankings = d3.select("#rankingviz").append("svg") | |
.attr("width", width) | |
.attr("height", height + margin.top + margin.bottom) | |
.style("border", "none") | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
//------------------------------------ | |
//----- importing the data | |
//------------------------------------ | |
d3.csv("static/data/lavaman2015/results.csv", function(d) { | |
return { | |
place: +d.place, | |
divTot: d.divTot, | |
bib: +d.bib, | |
// cat: d.cat, | |
firstName: d.firstName, | |
lastName: d.lastName, | |
age: +d.age, | |
sex: d.sex, | |
div: d.div, | |
swimRank: +d.swimRank, | |
// swimTime: +d.swimTime, | |
// swimPace: +d.swimPace, | |
// t1Time: +d.t1Time, | |
bikeRank: +d.bikeRank, | |
// bikeTime: +d.bikeTime, | |
// bikeSpeed: +d.bikeSpeed, | |
t2Rank: +d.t2Rank, | |
// t2Time: +d.t2Time, | |
runRank: +d.runRank, | |
// runTime: +d.runTime, | |
// runPace: +d.runPace, | |
totalTime: +d.totalTime, | |
wave: +d.wave, | |
// passedSwim: +d.wavePassedSwim, | |
// passedT1: +d.wavePassedT1, | |
// passedBike: +d.wavePassedBike, | |
// passedT2: +d.wavePassedT2, | |
// passedRun: +d.wavePassedRun, | |
// startPassedT1: +d.startPassedT1, | |
// startPassedBike: +d.startPassedBike, | |
// startPassedT2: +d.startPassedT2, | |
// startPassedRun: +d.startPassedRun, | |
estimatedT1Rank: +d.estimatedT1Rank | |
}; | |
}, function(error, data) { | |
var dataPoints = getPercentileDataForBib(122); | |
var barWidth = 20; | |
var backbars = rankings.selectAll("backbars") | |
.data(dataPoints) | |
.enter().append("rect") | |
.attr("y", function(d) { return y(d.label); }) | |
.attr("x", function(d) { return x(d.percentile) }) | |
.attr("width", function(d) { return width-x(d.percentile); }) | |
.attr("height", barWidth) | |
.attr("fill", '#eee'); | |
var bars = rankings.selectAll("percentile-bars") | |
.data(dataPoints) | |
.enter().append("rect") | |
.attr("y", function(d) { return y(d.label); }) | |
.attr("width", function(d) { return x(d.percentile); }) | |
.attr("height", barWidth) | |
.attr("fill", '#333'); | |
var textLabels = rankings.selectAll("labels") | |
.data(dataPoints) | |
.enter().append("text") | |
.attr('class', 'ranking-labels') | |
.attr('x', 5) | |
.attr('y', function(d) { return y(d.label) + barWidth/2 + 4; }) | |
.attr('fill', 'white') | |
.attr('text-anchor', 'beginning') | |
.attr('font-family', 'Helvetica') | |
.attr('font-weight', 'bold') | |
.attr('font-size', '8pt') | |
.text(function(d) { return d.label + " time"; }); | |
var percentiles = rankings.selectAll("percentiles") | |
.data(dataPoints) | |
.enter().append("text") | |
.attr('class', 'percentile-labels') | |
.attr('x', function(d) { return x(d.percentile) + 5; }) | |
.attr('y', function(d) { return y(d.label) + barWidth/2 + 4; }) | |
.attr('fill', '#999') | |
.attr('text-anchor', 'beginning') | |
.attr('font-family', 'Helvetica') | |
.attr('font-weight', 'bold') | |
.attr('font-size', '8pt') | |
// .attr('textLength', 4) | |
.text(function(d) { return (d.percentile*100).toPrecision(2) + "%ile"; }); | |
rankings.append("text") | |
.attr('id', 'ranking-viz-caption') | |
.attr('font-family', 'Helvetica') | |
.attr('font-size', '10pt') | |
.attr('fill', '#999') | |
.attr('y', height + 30) | |
.attr('x', width/2) | |
.attr('text-anchor', 'middle') | |
.text("My swim time was in the bottom 15% of all competitors.") | |
//-- controller for the input field to highlight a certain athlete | |
//-- returns a set of data that conforms to the x and y domain | |
//-- specified above. It has a | |
function getPercentileDataForBib(bib) { | |
var found = false; | |
var highlighted = data.filter(function(d) { | |
if (d.bib == bib) { | |
found = true; | |
dataToGraph = [{label:"Overall", percentile: 1-parseFloat(d.place)/1074}, | |
{label:"Swim", percentile: 1-parseFloat(d.swimRank)/1074}, | |
// {label:"T-1", percentile: 1-parseFloat(d.estimatedT1Rank/1023)}, | |
{label:"Bike", percentile: 1-parseFloat(d.bikeRank)/1074}, | |
// {label:"T-2", percentile:1-parseFloat(d.t2Rank)/1074}, | |
{label:"Run", percentile:1-parseFloat(d.runRank)/1074}]; | |
} | |
}); | |
if (found == true) { | |
return dataToGraph; | |
} else { | |
empty = []; | |
return empty; | |
} | |
} | |
function getNameForBib(bib) { | |
var highlighted = data.filter(function(d) { | |
if (d.bib == bib) { | |
name = (String(d.firstName) + " " + String(d.lastName)); | |
} | |
}); | |
return name; | |
} | |
//-- on enter of text input field and on press of Find | |
$('#percentileInputField').keydown(function(event) { | |
if (event.keyCode == 13) { changePercentileForBibNum(); } | |
}); | |
$('button[name=percentileBtn]').on("click", changePercentileForBibNum); | |
function changePercentileForBibNum(event) { | |
var requestedNum = $('#percentileInputField').val(); | |
var dataToGraph = getPercentileDataForBib(requestedNum); | |
if ( dataToGraph.length == 0 ) { //-- if can't find number, show error | |
if(d3.selectAll('.ranking-input-error')[0].length > 0) { //-- if there's already an error message, modify it | |
d3.select('.ranking-input-error').html("Sorry, couldn't find bib number - " + requestedNum + ". Athletes who had incomplete results (e.g. from a malfunctioning ankle band) are not included.") | |
} else { //-- otherwise, create it | |
d3.select('#ranking-form').append("div") | |
.attr("class", "ranking-input-error") | |
.html("Sorry, couldn't find that bib number " + requestedNum + ". Athletes who had incomplete data (e.g. from a malfunctioning ankle band) are not included.") | |
} | |
} else { //-- found a match | |
d3.select('.ranking-input-error').remove(); //-- found the num, remove old warnings | |
d3.select('#ranking-viz-caption').html("Showing percentiles for " + String(getNameForBib(requestedNum)) + ". Data calculated from official results [2].");// T-1 percentile is approximate; they were not officially reported."); | |
//-- change the bars | |
backbars.data(dataToGraph) | |
.attr("y", function(d) { return y(d.label); }) | |
.transition() | |
.duration(400) | |
.attr("x", function(d) { return x(d.percentile) }) | |
.attr("width", function(d) { return width-x(d.percentile); }); | |
bars.data(dataToGraph) | |
.attr("y", function(d) { return y(d.label); }) | |
.transition() | |
.duration(400) | |
.attr("width", function(d) { return x(d.percentile); }); | |
textLabels.data(dataToGraph) | |
.attr('y', function(d) { return y(d.label) + barWidth/2 + 4; }) | |
.text(function(d) { return d.label + " time"; }); | |
percentiles.data(dataToGraph) | |
.transition().duration(450) | |
.attr('x', function(d) { return x(d.percentile) + 5; }) | |
.attr('y', function(d) { return y(d.label) + barWidth/2 + 4; }) | |
.text(function(d) { return (d.percentile*100).toPrecision(2) + "%ile"; }); | |
} | |
// } | |
$('#percentileInputField').val(''); //-- clear text input field on every enter | |
return false; | |
} | |
}); | |
})(); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//-- author: IDEO | Jimmy Chion | 2013 | |
//-- license: Creative Commons SA-3.0 | |
(function(){ | |
var margin = {top: 70, right: 20, bottom: 60, left: 0.25*($(document).width())-50}; | |
var width = 0.5*($(document).width()); | |
if ( width > 550 ) { | |
width = 550; | |
margin.left = .25*1140; | |
} //-- max-width in js | |
var height = width; | |
var x = d3.scale.linear() | |
.range([0, width]); | |
var y = d3.scale.linear() | |
.range([0, height]); | |
var aboveLabel = 'ranked in leg higher than overall rank' | |
var belowLabel = 'ranked in leg lower than overall rank' | |
var color = d3.scale.ordinal() | |
.domain([aboveLabel,belowLabel]) | |
.range(['#F26755','#4BA5A8']); //-- colors of the graph green gold | |
// .range(['#DE4124', '#A180D9']); //-- colors of the graph blue red | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient("top") | |
.tickValues([1, 250, 500, 750, 1000]) | |
.tickFormat(function(d) { return (d%10 == 1)?(d+'st'):(d+'th'); }); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.orient("left") | |
.tickValues([1, 250, 500, 750, 1000]) | |
.tickFormat(function(d) { return (d%10 == 1)?(d+'st'):(d+'th'); }); | |
var tip = d3.tip() | |
.attr('class', 'd3-tip') | |
.offset([-10, 0]) | |
.html(function(d) { | |
return ("<em>" + d.firstName + " " + d.lastName + "</em>, " + d.age + "<br>Time: " + d.totalTime.toHHMMSS() + "<br>Place: " + d.place); | |
}); | |
var differencePlot = d3.select("#sport").append("svg") | |
.attr("width", width + margin.left) | |
.attr("height", height + margin.top + margin.bottom) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
d3.csv("static/data/lavaman2015/results.csv", function(d) { | |
return { | |
place: +d.place, | |
// divTot: d.divTot, | |
// bib: +d.bib, | |
// cat: d.cat, | |
// firstName: d.firstName, | |
// lastName: d.lastName, | |
age: +d.age, | |
sex: d.sex, | |
// div: d.div, | |
swimRank: +d.swimRank, | |
// swimTime: +d.swimTime, | |
// swimPace: +d.swimPace, | |
// t1Time: +d.t1Time, | |
bikeRank: +d.bikeRank, | |
// bikeTime: +d.bikeTime, | |
// bikeSpeed: +d.bikeSpeed, | |
t2Rank: +d.t2Rank, | |
// t2Time: +d.t2Time, | |
runRank: +d.runRank, | |
// runTime: +d.runTime, | |
// runPace: +d.runPace, | |
// totalTime: +d.totalTime, | |
// wave: +d.wave, | |
// passedSwim: +d.passedSwim, | |
// passedT1: +d.passedT1, | |
// passedBike: +d.passedBike, | |
// passedT2: +d.passedT2, | |
// passedRun: +d.passedRun, | |
estimatedT1Rank: +d.estimatedT1Rank | |
}; | |
}, function(error, data) { | |
x.domain(d3.extent(data, function(d) { | |
return d.place; | |
})); | |
y.domain(d3.extent(data, function(d) { | |
return d.swimRank; | |
})).nice(); | |
differencePlot.append("g") | |
.attr("class", "x axis") | |
.call(xAxis); | |
var xaxisLabel = differencePlot.append("text") | |
.attr("class", "label") | |
.attr("x", width) | |
.attr("y", 16) | |
.style("text-anchor", "end") | |
.text("Overall rank"); | |
differencePlot.append("g") | |
.attr("class", "y axis") | |
.call(yAxis); | |
var yaxisLabel = differencePlot.append("text") | |
.attr("class", "label") | |
.attr("transform", "translate(0," + (height-30) + ") rotate(-90)") | |
.attr("y", 6) | |
.attr("dy", ".71em") | |
.text("Swim rank"); | |
var differencePlotLines = differencePlot.selectAll(".age-vs-time-line") | |
.data(data) | |
.enter().append("line") | |
.attr("class", "age-vs-time-line") | |
.attr("x1", function(d){ return x(d.place) }) | |
.attr("y1", function(d){ return y(d.place) }) | |
.attr("x2", function(d){ return x(d.place) }) | |
.attr("y2", function(d){ return y(d.swimRank) }) | |
.attr("stroke-width", 1.5) | |
.style("stroke", function(d) { | |
return (d.swimRank <= d.place )?color(aboveLabel):color(belowLabel); | |
}); | |
// draw legend | |
var legend = differencePlot.selectAll(".legend") | |
.data(color.domain()) | |
.enter().append("g") | |
.attr("class", "legend") | |
.attr("transform", function(d, i) { return "translate(0," + (i * 20 + 50) + ")"; }); | |
// draw legend colored rectangles | |
legend.append("circle") | |
.attr("cx", 10) | |
.attr("cy", height -30) | |
.attr("r", 6) | |
.style("fill", color); | |
// draw legend text | |
legend.append("text") | |
.attr("x", 20) | |
.attr("y", height - 30) | |
.attr("dy", ".35em") | |
.style("text-anchor", "front") | |
.style("fill", "#AAA") | |
.text(function(d) { return d; }); | |
//------------------------------------ | |
//----- inputs for interactive elements | |
//------------------------------------ | |
$('input[name=sportSelect]').on('change', inputHandler); //- | |
function inputHandler() { | |
if (this.id == 'Swim') { | |
differencePlotLines.transition() | |
.delay(function(d) { return d.age*10; }) | |
.duration(300) | |
.attr("y2", function(d) { return y(d.swimRank); }); | |
differencePlotLines.style("stroke", function(d) { | |
return (d.swimRank <= d.place )?color(aboveLabel):color(belowLabel); | |
}); | |
yaxisLabel.transition().delay(800).text('Swim Rank'); | |
} else if (this.id == 'Bike') { | |
differencePlotLines.transition() | |
.delay(function(d) { return d.age*10; }) | |
.duration(300) | |
.attr("y2", function(d) { return y(d.bikeRank); }); | |
differencePlotLines.style("stroke", function(d) { | |
return (d.bikeRank <= d.place )?color(aboveLabel):color(belowLabel); | |
}); | |
yaxisLabel.transition().delay(800).text('Bike Rank') | |
.transition().duration(0).style("fill", d3.rgb("#DE4124")) | |
} else if (this.id == 'Run') { | |
differencePlotLines.transition() | |
.delay(function(d) { return d.age*10; }) | |
.duration(300) | |
.attr("y2", function(d) { return y(d.runRank); }); | |
differencePlotLines.style("stroke", function(d) { | |
return (d.runRank <= d.place )?color(aboveLabel):color(belowLabel); | |
}); | |
yaxisLabel.transition().delay(800).text('Run Rank') | |
.transition().duration(0).style("fill", d3.rgb("#DE4124")) | |
} else if (this.id == 'T-1') { | |
differencePlotLines.transition() | |
.delay(function(d) { return d.age*10; }) | |
.duration(300) | |
.attr("y2", function(d) { return y(d.estimatedT1Rank); }); | |
differencePlotLines.style("stroke", function(d) { | |
return (d.estimatedT1Rank <= d.place )?color(aboveLabel):color(belowLabel); | |
}); | |
yaxisLabel.transition().delay(800).text('T-1 Rank') | |
.transition().duration(0).style("fill", d3.rgb("#DE4124")) | |
} else if (this.id == 'T-2') { | |
differencePlotLines.transition() | |
.delay(function(d) { return d.age*10; }) | |
.duration(300) | |
.attr("y2", function(d) { return y(d.t2Rank); }); | |
differencePlotLines.style("stroke", function(d) { | |
return (d.t2Rank <= d.place )?color(aboveLabel):color(belowLabel); | |
}); | |
yaxisLabel.transition().delay(800).text('T-2 Rank') | |
.transition().duration(0).style("fill", d3.rgb("#DE4124")) | |
} | |
} | |
}); | |
})(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment