Simulating the 538 Horsey Race.
forked from bricedev's block: French soccer championship replay
license: mit |
Simulating the 538 Horsey Race.
forked from bricedev's block: French soccer championship replay
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
body { | |
font: 10px sans-serif; | |
} | |
.x.axis path,.x.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
.y.axis path { | |
fill: none; | |
} | |
.y.axis line { | |
fill: none; | |
stroke: #eeeeee; | |
shape-rendering: crispEdges; | |
} | |
.date { | |
font: 500 196px "Helvetica Neue"; | |
fill: #aaa; | |
} | |
</style> | |
<body> | |
<script src="https://d3js.org/d3.v3.min.js"></script> | |
<script> | |
var margin = {top: 20, right: 80, bottom: 0, left: 30}, | |
width = 960 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom; | |
var speed = 200; | |
var x = d3.scale.linear() | |
.range([0, width-70]); | |
var y = d3.scale.ordinal() | |
.rangeRoundBands([height, 0], .1); | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.tickSize(0) | |
.orient("top"); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.tickSize(-width) | |
.tickPadding(10) | |
.orient("left"); | |
var line = d3.svg.line() | |
.x(function(d) { return x(d.matchday); }) | |
.y(function(d) { return y(d.position) + y.rangeBand()/2; }); | |
var svg = d3.select('body').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 + ")"); | |
var clip = svg.append("clipPath") | |
.attr("id", "clip") | |
.append("rect") | |
.attr("width", 0) | |
.attr("height", height); | |
function rand_step(probability){ | |
return Math.random() < probability ? 1: -1 //if the horseys are more lazy than directionally challenged, make this 0 | |
}; | |
function run_race(race_length){ | |
horsey_probabilities = [.52,.54,.56,.58,.60,.62,.64,.66,.68,.70,.72,.74,.76,.78,.80,.82,.84,.86,.88,.90].reverse() | |
horsey_paths = [[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]] | |
winning_position = 0 | |
while (winning_position < race_length) { | |
new_steps = horsey_probabilities.map(function(d){return rand_step(d)}) | |
horsey_paths.map(function (list, idx) { | |
return list.push(new_steps[idx]);}); | |
winning_position = Math.max.apply(Math, horsey_paths.map(function(d){return d.reduce((a, b) => a + b, 0)})) | |
// console.log(winning_position); | |
} | |
return horsey_paths | |
} | |
function compute_rankings(paths){ | |
// paths = [[1,0,1,0],[0,1,1,1]] | |
step_sums = paths.map(function(list){ | |
return list.map(function (i,idx){ | |
return list.slice(0,idx+1).reduce(function(acc, val) { | |
return acc + val; })}) | |
}) | |
// console.log(step_sums) | |
convert_list_to_ranks([5,2,4,2,3,0]); | |
rankings = matrix_transpose( | |
matrix_transpose(step_sums) | |
.map(function (col){ | |
return convert_list_to_ranks(col) | |
})); | |
rank_dict = rankings.map(function (horsey, horsey_idx){ | |
return horsey.map(function (position, day) { | |
return {"matchday": day, "position": position, "steps": step_sums[horsey_idx][day]} | |
}) | |
}); | |
// console.log(rank_dict); | |
horsey_colors = ["#0162A6", "#014689", "#041C42", "#EA0001", "#CD003B", "#E00016", "#F5D30F", "#C91119", "#F58113", "#013893", "#0898D7", "#B0053A", "#EE2722", "#F36F21", "#FFD600", "#D4082D", "#B41309", "#E13328", "#269045", "#6F5B98"] | |
standings_list = rank_dict.map(function (horsey_standings, horsey_number){ | |
return {"teamName": "Horsey"+(horsey_number+1), | |
"color": horsey_colors[horsey_number], | |
"rank": horsey_standings | |
} | |
}); | |
// console.log(standings_list) | |
return standings_list | |
} | |
function matrix_transpose(matrix){ | |
return matrix[0].map(function(col, i) { | |
return matrix.map(function(row) { | |
return row[i]; | |
}); | |
}); | |
} | |
function convert_list_to_ranks(list_of_sums){ | |
output = list_of_sums.map(function (i,idx){return ([idx+1, i]);}) | |
.sort(function(a,b) {return b[1] - a[1];}) | |
.map(function (p,idy){return ([p[0], idy+1])}) | |
.sort(function(a,b) {return a[0] - b[0];}) | |
.map(function (x){return x[1]}) | |
return output | |
} | |
function run_simulation(race_length){ | |
paths = run_race(race_length); | |
// console.log(paths) | |
race_dictionary = { | |
"race": "Lucky Derby", | |
"standing": [] | |
} | |
race_dictionary["standing"] = compute_rankings(paths); | |
return race_dictionary | |
}; | |
function draw(data) { | |
y.domain( | |
d3.range( | |
1,21 | |
).reverse() | |
); | |
x.domain(d3.extent(data.standing[0].rank.map(function(d) { return d.matchday; }))); | |
svg.append("g") | |
.attr("class", "y axis") | |
.call(yAxis); | |
svg.append("g") | |
.attr("class", "x axis") | |
.call(xAxis); | |
var club = svg.selectAll(".club") | |
.data(data.standing) | |
.enter().append("g") | |
.attr("class", "club"); | |
var path = club.append("path") | |
.attr("class", "line") | |
.style("stroke", function(d) { return d.color; }) | |
.style("stroke-width", 3) | |
.style("fill","none") | |
.attr("clip-path", function(d) { return "url(#clip)"; }) | |
.attr("d", function(d) { return line(d.rank); }); | |
var circleStart = club.append("circle") | |
.attr("cx", function(d) { return x(d.rank[0].matchday); }) | |
.attr("cy", function(d) { return y(d.rank[0].position) + y.rangeBand()/2; }) | |
.style("fill", function(d) { return d.color; }) | |
.attr("r", 3) | |
var circleEnd = club | |
.append("circle") | |
.attr("cx", function(d) { return x(d.rank[0].matchday); }) | |
.attr("cy", function(d) { return y(d.rank[0].position) + y.rangeBand()/2; }) | |
.style("fill", function(d) { return d.color; }) | |
.attr("r", 3) | |
var label = club.append("text") | |
.attr("transform", function(d) { return "translate(" + x(d.rank[0].matchday) + "," + (y(d.rank[0].position) + y.rangeBand()/2) + ")"; }) | |
.attr("x", 8) | |
.attr("dy", ".31em") | |
.on("mouseover", function (d) { | |
club.style("opacity",0); | |
club.filter(function(path) {return path.teamName === d.teamName; }).style("opacity",1); | |
}) | |
.on("mouseout", function (d) { club.style("opacity",1); }) | |
.style("cursor","pointer") | |
.style("fill", function(d) { return d.color; }) | |
.style("font-weight", "bold") | |
.text(function(d) { return "๐ #"+ d.rank[0].position + " " + d.teamName + " Steps:" + d.rank[0].steps; }); | |
var matchday = 1; | |
var transition = d3.transition() | |
.duration(speed) | |
.each("start", function start() { | |
label.transition() | |
.duration(speed) | |
.ease('linear') | |
.attr("transform", function(d) { return "translate(" + x(d.rank[matchday].matchday) + "," + (y(d.rank[matchday].position) + y.rangeBand()/2) + ")"; }) | |
.text(function(d) { return "๐ #"+ d.rank[matchday].position + " " + d.teamName + " Steps:" + d.rank[matchday].steps; }); | |
circleEnd.transition() | |
.duration(speed) | |
.ease('linear') | |
.attr("cx", function(d) { return x(d.rank[matchday].matchday); }) | |
.attr("cy", function(d) { return y(d.rank[matchday].position) + y.rangeBand()/2; }); | |
clip.transition() | |
.duration(speed) | |
.ease('linear') | |
.attr("width", x(matchday+1)) | |
.attr("height", height); | |
matchday+=1; | |
if (matchday !== data.standing[0].rank.length) transition = transition.transition().each("start", start); | |
}); | |
}; | |
draw(run_simulation(200)); | |
</script> |