As the AFL Women's league has gotten up and running, there's been a lot of talk about players transitioning from other sports to play AFL. I wanted to practice making force directed graphs whilst also investigating just how prevalent code jumping is.
Overall I think the main thing I learnt is that a disproportionately large number of women have some sort of background in javelin. If anyone has any theories about why that is, I'm really curious.
For a player to be linked with AFL they had to have played more than 5 matches, at least one of which had to be before 2016. They also had to have played at least one game in the previous 3 years.
Any mistakes in the data are almost certainly my own, please feel free to contact me if you notice anything.
The force directed graph was modelled on that created by Mike Bostock.
All information was gathered from the AFL player profiles and data available on SportsTG.
Colours used are slightly modified versions of colour palettes available on Colour Brewer 2.0.
xxxxxxxxxx
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Code Jumping in WAFL</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
const width = 960;
const height = 500;
const club = ['adelaide', 'gws', 'melbourne', 'brisbane', 'bulldogs', 'carlton', 'fremantle', 'collingwood'];
const contract = ['marquee', 'rookie', 'priority', 'free agent', 'regular'];
// contract type is colorbrewer2
const color = {
'marquee': '#c7e9b4',
'rookie': '#7fcdbb',
'priority': '#1d91c0',
'free agent': '#12578c',
'regular': '#081d58',
'adelaide': '#ffe303',
'brisbane': '#b15928',
'carlton': '#253494',
'collingwood': '#000',
'fremantle': '#6a3d9a',
'gws': '#ff7f00',
'melbourne': '#e31a1c',
'bulldogs': '#1f78b4'
};
const buttons = [
{name: 'Contract Type', id: 'contract', state: true, y: 0},
{name: 'Club', id: 'club', state: false, y: 60}
];
const buttonsDims = {r: 15, ringOffset: 6, textOffset: 5,
xOffset: 200, yOffset: 80, xLegendOffset: 400, yLegendOffset: 120};
let svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
let simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(function(d) { return d.id; }))
.force('charge', d3.forceManyBody());
d3.json('sports.json', function(error, graph) {
if (error) throw error;
const nodeR = {player: 5, sport: 9};
let link = svg.append('g')
.attr('class', 'link')
.selectAll('line')
.data(graph.links)
.enter().append('line');
let node = svg.append('g')
.classed('node', true)
.selectAll('circle')
.data(graph.nodes)
.enter().append('circle')
.attr('r', function(d) { return d.type == 'player' ? nodeR.player : nodeR.sport; })
.attr('id', function(d) { return d.id.replace(/\s+/g, ''); })
.classed('player', function(d) { return d.type == 'player'; })
.classed('sport', function(d) { return d.type == 'sport'; })
.on('mouseover', mouseOverNode)
.on('mouseout', mouseOut)
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
let players = d3.selectAll('.player')
.style('fill', function(d) { return color[d.contract]; });
for (let i = 0; i < contract.length; i++) {
players.classed(contract[i].replace(/\s+/g, ''), function(d) {return d.contract == contract[i]; });
}
for (let i = 0; i < club.length; i++) {
players.classed(club[i].replace(/\s+/g, ''), function(d) {return d.club == club[i]; });
}
node.append('title')
.text(function(d) { return d.id; });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force('link')
.links(graph.links);
function ticked() {
link
.attr('x1', function(d) { return d.source.x; })
.attr('y1', function(d) { return d.source.y; })
.attr('x2', function(d) { return d.target.x; })
.attr('y2', function(d) { return d.target.y; });
node
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
}
});
svg.append('g')
.attr('transform', 'translate(' + (width - buttonsDims.xOffset) + ',' + buttonsDims.yOffset + ')')
.attr('id', 'buttons')
.selectAll('circle')
.data(buttons)
.enter().append('circle')
.attr('cy', function(d) {return d.y; })
.attr('r', buttonsDims.r + buttonsDims.ringOffset)
.attr('id', function(d) {return d.id + 'Ring'; })
.classed('active', function(d) {return d.state; })
.classed('ring', true);
d3.select('#buttons').append('g')
.selectAll('circle')
.data(buttons)
.enter().append('circle')
.attr('cy', function(d) {return d.y; })
.attr('r', buttonsDims.r)
.classed('click', true)
.on('click', clicked);
d3.select('#buttons').selectAll('text')
.data(buttons)
.enter().append('text')
.attr('x', buttonsDims.r + buttonsDims.ringOffset + buttonsDims.textOffset)
.attr('y', function(d) {return d.y + buttonsDims.textOffset; })
.text(function(d) {return d.name});
const legendTitles = [{id: 'contract', state: true}, {id: 'club', state: false}];
d3.select('#buttons').append('g')
.attr('transform', 'translate(' + buttonsDims.xLegendOffset + ',' + buttonsDims.yLegendOffset + ')')
.attr('id', 'legend');
d3.select('#legend').selectAll('g')
.data(legendTitles)
.enter().append('g')
.attr('transform', function(d) {return 'translate(' + (d.state ? -(buttonsDims.xLegendOffset + 30) : 0) + ',0)'; })
.attr('id', function(d) {return d.id + 'G'});
createLegend(legendTitles[0].id, contract);
createLegend(legendTitles[1].id, club);
function createLegend(title, data) {
const legendDims = {width: 50, height: 30, gap: 35, round: 5, xText: 60, yText: 20};
d3.select('#' + title + 'G').selectAll('rect')
.data(data)
.enter().append('rect')
.attr('width', legendDims.width)
.attr('height', legendDims.height)
.attr('y', function(d,i) {return legendDims.gap*i; })
.attr('rx', legendDims.round)
.attr('ry', legendDims.round)
.style('fill', function(d,i) {return color[data[i]]; })
.on('mouseover', mouseOverLegend)
.on('mouseout', mouseOut);
d3.select('#' + title + 'G').selectAll('text')
.data(d3.range(data.length))
.enter().append('text')
.attr('x', legendDims.xText)
.attr('y', function(d) {return legendDims.gap*d + legendDims.yText; })
.text(function(d) {return data[d]; });
}
function mouseOverNode(d2) {
// lower link opacity if it doesn't link to the sport
let links = svg.selectAll('line');
links
.classed('fadeLink', function(d) {return !(d.source === d2 || d.target === d2)})
.classed('connectedLink', function(d) {return (d.source === d2 || d.target === d2); });
d3.selectAll('.sport').classed('fadeNode', true);
d3.selectAll('.player').classed('fadeNode', true);
d3.select(this).classed('fadeNode', false);
let linkData = d3.selectAll('.connectedLink').data();
if (d2.type == 'sport') {
for (let i = 0; i < linkData.length; i++) {
let source = linkData[i].source;
let id = source.id;
id = id.replace(/\s+/g, '');
d3.select('#' + id).classed('fadeNode', false);
}
} else {
for (let i = 0; i < linkData.length; i++) {
let target = linkData[i].target;
let id = target.id;
id = id.replace(/\s+/g, '');
d3.select('#' + id).classed('fadeNode', false);
}
}
}
function mouseOverLegend(d2) {
let id = d2.replace(/\s+/g, '');
d3.selectAll('.player').classed('fadeNode', true);
d3.selectAll('.' + id).classed('fadeNode', false);
d3.selectAll('line')
.classed('fadeLink', function(d) {return !(d.source.contract === d2 || d.source.club === d2)})
.classed('connectedLink', function(d) {return (d.source.contract === d2 || d.source.club === d2)});
d3.selectAll('.sport').classed('fadeNode', true);
let linkData = d3.selectAll('.connectedLink').data();
for (let i = 0; i < linkData.length; i++) {
let target = linkData[i].target;
let id = target.id;
id = id.replace(/\s+/g, '');
d3.select('#' + id).classed('fadeNode', false);
}
}
function mouseOut() {
d3.selectAll('.fadeNode').classed('fadeNode', false);
d3.selectAll('.connectedLink').classed('connectedLink', false);
d3.selectAll('.fadeLink').classed('fadeLink', false);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
if (d.type != 'sport') {
d.fx = null;
d.fy = null;
}
}
function clicked(d2) {
d3.select('#buttons').selectAll('circle').classed('active', false);
d3.select('#' + d2.id + 'Ring').classed('active', true);
d3.selectAll('.player').style('fill', function(d) {return color[d[d2.id]];});
d3.select('#legend').selectAll('g')
.transition()
.duration(2000)
.attr('transform', function(d) {return 'translate(' + (d.id == d2.id ? -(buttonsDims.xLegendOffset + 30) : 0) + ',0)'; })
.style('fill-opacity', function(d) {return d.id == d2.id ? 1 : 0});
}
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js