Skip to content

Instantly share code, notes, and snippets.

@cjimmy
Last active July 14, 2017 12:31
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cjimmy/2e226a6e88d02f743679 to your computer and use it in GitHub Desktop.
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
//-- 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');
}
});
})();
//-- 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;
}
//-- 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;
}
});
})();
//-- 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