Skip to content

Instantly share code, notes, and snippets.

@agware
Last active March 5, 2017 01:17
Show Gist options
  • Save agware/c9592dfb7dccaf2328fe17003a558cde to your computer and use it in GitHub Desktop.
Save agware/c9592dfb7dccaf2328fe17003a558cde to your computer and use it in GitHub Desktop.
Path to the Grand Final
license: gpl-3.0

Possibly one of the most exciting parts of football is the constant underlying tension of who's going to make it to the Grand Final. This year's women's competition has a particularly novel approach to this with no qualifying finals. Instead, whoever is on top of the ladder at the end of the season will face off.

The goal of this animation is to answer the ever important question, can my team still make the Grand Final?

Credit

All logos used are property of the AFL, please don't sue me.

This piece was partly inspired by Anna Powell-Smith's 23 years of the English Premier League. It's pretty incredible and definitely worth checking out.

Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Interactive Ladder</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
<style>
circle {
fill: #fff;
stroke-width: 2px;
stroke: #000;
}
text {
font-family: 'Open Sans', sans-serif;
font-size: 18px;
fill: #000;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 10px;
}
.interactive {
cursor: pointer;
}
.uncertainGame {
stroke-opacity: 0.7;
opacity: 0.7;
}
.uncertain {
opacity: 0.5;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script type="text/javascript" src="utils.js"></script>
<script type="text/javascript" src="rounds.js"></script>
<script>
const height = 900;
const width = 1100;
const margin = {'x': 140, 'y': 80, 'gapX': 90, 'gapY': 100};
const teams = ['Adelaide', 'Brisbane', 'Bulldogs', 'Carlton', 'Collingwood', 'Fremantle', 'GWS', 'Melbourne'];
const colour = {
'Adelaide': '#ffe303',
'Brisbane': '#b15928',
'Bulldogs': '#1f78b4',
'Carlton': '#253494',
'Collingwood': '#000',
'Fremantle': '#6a3d9a',
'GWS': '#ff7f00',
'Melbourne': '#e31a1c'
};
const logo = {
'Adelaide': 'Adelaide.svg',
'Brisbane': 'Brisbane.svg',
'Bulldogs': 'Bulldogs.svg',
'Carlton': 'Carlton.svg',
'Collingwood': 'Collingwood.svg',
'Fremantle': 'Fremantle.svg',
'GWS': 'GWS.svg',
'Melbourne': 'Melbourne.svg'
};
const fixture = [
[['Carlton','Collingwood'],['Adelaide','GWS'],['Bulldogs','Fremantle'],['Melbourne','Brisbane']],
[['Bulldogs','Adelaide'],['Carlton','GWS'],['Collingwood','Melbourne'],['Fremantle','Brisbane']],
[['GWS','Fremantle'],['Brisbane','Collingwood'],['Bulldogs','Melbourne'],['Adelaide','Carlton']],
[['Melbourne','Carlton'],['Brisbane','GWS'],['Bulldogs','Collingwood'],['Fremantle','Adelaide']],
[['GWS','Melbourne'],['Carlton','Bulldogs'],['Fremantle','Collingwood'],['Adelaide','Brisbane']],
[['Fremantle','Carlton'],['Brisbane','Bulldogs'],['Adelaide','Melbourne'],['Collingwood','GWS']],
[['Melbourne','Fremantle'],['GWS','Bulldogs'],['Collingwood','Adelaide'],['Carlton','Brisbane']]
];
const certainWin = [
['Adelaide', 'Brisbane', 'Bulldogs', 'Carlton'],
['Adelaide', 'Brisbane', 'Carlton', 'Melbourne'],
['Adelaide', 'Brisbane', 'Melbourne'],
['Adelaide', 'Brisbane', 'Collingwood', 'Melbourne'],
['Brisbane', 'Carlton', 'Collingwood', 'GWS']
];
const certainDraw = [[],[],['Fremantle', 'GWS'],[],[]];
let points = {'Adelaide': 0, 'Brisbane': 0, 'Bulldogs': 0, 'Carlton': 0, 'Collingwood': 0, 'Fremantle': 0, 'GWS': 0, 'Melbourne': 0};
let userChosen = [];
let svg = d3.select('body').append('svg')
.attr('height', height)
.attr('width', width);
let background = svg.append('g');
// grey behind top two teams
background.append('rect')
.attr('x', margin.x - 40)
.attr('y', margin.y + 50)
.attr('width', 170)
.attr('height', 680)
.style('fill', '#e8e8ee');
let ladder = svg.append('g');
let sidebar = svg.append('g')
.attr('transform', 'translate(' + 830 + ',' + 55 + ')');
let roundNum = 0;
listClub();
generateRound(roundNum);
d3.selectAll('.round' + roundNum).classed('uncertainGame', false);
listRound(roundNum);
roundNum += 1;
generateCertainRounds(roundNum, certainWin, certainDraw);
let uncertainWins = initUncertainRounds(certainWin.length + 1);
generateUncertainRounds(certainWin.length + 1);
generateSidebar(certainWin.length + 1);
generatePath();
function generateCertainRounds(roundNum, certainWin, certainDraw) {
while(roundNum <= certainWin.length) {
addWin(certainWin[roundNum-1]);
addDraw(certainDraw[roundNum-1]);
sortByPoints();
generateRound(roundNum);
listRound(roundNum);
d3.selectAll('.round' + roundNum).classed('uncertainGame', false);
roundNum += 1;
}
}
function initUncertainRounds(roundNum) {
let ret = [];
while(roundNum <= fixture.length) {
let round = fixture[roundNum-1];
let winner = [];
for(let i = 0; i < round.length; i++) {
let team1 = round[i][0];
let team2 = round[i][1];
let temp = points[team1] >= points[team2] ? team1 : team2;
winner.push(temp);
}
ret.push(winner);
roundNum += 1;
}
return ret;
}
function generateUncertainRounds(roundNum) {
while(roundNum <= fixture.length) {
addWin(uncertainWins[roundNum - certainWin.length - 1]);
sortByPoints();
generateRound(roundNum);
listRound(roundNum);
roundNum += 1;
}
}
function generateSidebar(roundNum) {
const sidebarDim = {'height': 700, 'width': 250};
while(roundNum <= fixture.length) {
let round = fixture[roundNum-1];
let sidebarG = sidebar.append('g')
.attr('transform', 'translate(0,' + ((roundNum - certainWin.length - 1) * sidebarDim.height / 2) + ')');
sidebarG.append('rect')
.attr('height', sidebarDim.height / 2)
.attr('width', sidebarDim.width)
.style('fill', '#fff')
.style('stroke', '#000')
.style('stroke-width', '2px');
sidebarG.append('text')
.attr('x', 90)
.attr('y', 40)
.text('Round ' + roundNum)
.style('font-size', '20px')
.style('font-weight', 'bold');
for(let i = 0; i < round.length; i++) {
let team1 = round[i][0];
let team2 = round[i][1];
sidebarG.append('image')
.attr('xlink:href', logo[team1])
.attr('x', 10)
.attr('y', 60 + 70*i)
.datum(roundNum.toString() + i.toString())
.attr('id', team1 + roundNum.toString() + i.toString())
.classed('round' + roundNum.toString() + i.toString(), true)
.classed('interactive', true)
.classed('uncertain', true)
.on('click', selectTeam);
sidebarG.append('text')
.attr('x', 118)
.attr('y', 90 + 70*i)
.text('vs')
.style('font-weight', 'bold');
sidebarG.append('image')
.attr('xlink:href', logo[team2])
.attr('x', 160)
.attr('y', 60 + 70*i)
.datum(roundNum.toString() + i.toString())
.attr('id', team1 + roundNum.toString() + i.toString())
.classed('round' + roundNum.toString() + i.toString(), true)
.classed('interactive', true)
.classed('uncertain', true)
.on('click', selectTeam);
}
roundNum += 1;
}
}
function generatePath() {
background.selectAll('path').remove();
for (let x = 0; x < teams.length; x++) {
let fixedData = ripData(teams[x], 0, fixture.length);
background.append('path')
.datum(fixedData)
.classed('line', true)
.style('stroke', colour[teams[x]])
.attr('d', d3.line()
.curve(d3.curveMonotoneY)
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
);
}
}
function selectTeam() {
let certainty = d3.select(this).classed('uncertain');
let matchUp = d3.select(this).datum().toString();
let round = parseInt(matchUp.substring(0, matchUp.length - 1));
let game = matchUp.substring(matchUp.length - 1, matchUp.length);
let gameTeams = fixture[round-1][game];
d3.selectAll('.round' + matchUp).classed('uncertain', certainty);
d3.select(this).classed('uncertain', !certainty);
let winner = d3.select('#' + gameTeams[0] + matchUp).classed('uncertain') ? gameTeams[1] : gameTeams[0];
let loser = (winner == gameTeams[1]) ? gameTeams[0] : gameTeams[1];
// remove old rounds
for(let i = round; i <= fixture.length; i++) {
deleteRound(i);
removeWin(uncertainWins[i - certainWin.length - 1]);
let x = userChosen.indexOf(winner + round);
if (!(x >= 0)) {
userChosen.push(winner + round);
userChosen.push(loser + round);
}
console.log(userChosen);
}
// update with new data
let tempRound = round - certainWin.length - 1;
let index = uncertainWins[tempRound].indexOf(loser);
if (index >= 0) {
uncertainWins[tempRound].splice(index, 1);
uncertainWins[tempRound].push(winner);
}
generateUncertainRounds(round);
for(let j = 0; j < userChosen.length; j++) {
d3.select('#' + userChosen[j]).classed('uncertainGame', false);
d3.select('#' + userChosen[j] + 'text').classed('uncertainGame', false);
}
generatePath();
}
</script>
</body>
</html>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
/**
* Created by Alex on 27/02/2017.
*/
function generateRound(roundNum) {
ladder.append('g')
.attr('id', 'round' + roundNum)
.selectAll('circle')
.data(teams)
.enter().append('circle')
.attr('cx', function(d,i) {return i*margin.gapX + margin.x; })
.attr('cy', margin.gapY*roundNum + margin.y)
.attr('r', 20)
.attr('id', function(d) {return d + roundNum; })
.style('stroke', function(d) {return colour[d]; })
.classed('uncertainGame', true)
.classed('round' + roundNum, true);
ladder.select('#round' + roundNum).selectAll('text')
.data(teams)
.enter().append('text')
.attr('x', function(d,i) {return i*margin.gapX + margin.x - (points[d].toString().length == 1 ? 5 : 11); })
.attr('y', margin.gapY*roundNum + margin.y + 6)
.attr('id', function(d) {return d + roundNum + 'text'})
.text(function(d) {return points[d]; })
.classed('uncertainGame', true)
.classed('round' + roundNum, true);
}
function deleteRound(i) {
d3.select('#round' + i).remove();
}
function listRound(roundNum) {
ladder.select('#round' + roundNum).append('text')
.attr('x', 30)
.attr('y', margin.gapY*roundNum + margin.y + 5)
.text('Rd ' + roundNum)
.style('font-weight', 'bold');
}
/**
* Created by Alex on 27/02/2017.
*/
function listClub() {
svg.append('g')
.selectAll('text')
.data(teams)
.enter().append('text')
.attr('x', function(d,i) {return margin.x + margin.gapX*i - 3*(d.length+2); })
.attr('y', 40)
.text(function(d) {return d; })
.style('font-weight', 'bold')
.style('font-size', '15px');
}
function addWin(win) {
for (let i = 0; i < win.length; i++) {
points[win[i]] += 4;
}
}
function removeWin(win) {
for (let i = 0; i < win.length; i++) {
points[win[i]] -= 4;
}
}
function addDraw(draw) {
for (let i = 0; i < draw.length; i++) {
points[draw[i]] += 2;
}
}
function sortByPoints() {
let i = 1;
while (i < teams.length) {
let highest = true;
let count = i - 1;
while (highest && count >= 0) {
if (points[teams[i]] > points[teams[count]]) {
swapTeams(i, count);
i -= 1;
} else {
highest = false;
}
count -= 1;
}
i += 1;
}
}
function swapTeams(indexA, indexB) {
let temp = teams[indexA];
teams[indexA] = teams[indexB];
teams[indexB] = temp;
}
function ripData(team, startNum, roundNum) {
let ret = [];
for (let i = startNum; i <= roundNum; i++) {
let el = d3.select('#' + team + i);
let row = {'x': el.attr('cx'), 'y': el.attr('cy')};
ret.push(row);
}
return ret;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment