|
<!DOCTYPE html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<style> |
|
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; } |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<div id="chart1"></div> |
|
<script> |
|
d3.csv("csl_foreign_players.csv", function(csv) { |
|
|
|
var screenWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0) * 0.8 |
|
var screenHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) * 0.8 |
|
var canvasDim = { width: screenWidth, height: screenHeight} |
|
|
|
var margin = {top: 20, right: 20, bottom: 20, left: 80} |
|
var width = canvasDim.width - margin.left - margin.right |
|
var height = canvasDim.height - margin.top - margin.bottom |
|
|
|
var countries = ['England', 'Brazil', 'Denmark', 'Portugal', 'Germany', 'Poland', 'France', 'Argentina', 'Spain', 'Peru', 'South Korea', 'Switzerland', 'Uruguay', 'Colombia', 'Croatia', 'Costa Rica', 'Nigeria', 'Iceland', 'Sweden', 'Australia', 'Senegal', 'Iran', 'Serbia', 'Morocco', 'Egypt', 'Tunisia', 'Belgium', 'Japan'] |
|
var axisPad = 6 |
|
|
|
// CREATE DOM ELEMENTS |
|
var svg = d3.select("#chart1").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 tooltip = d3.select("#chart1").append("div") |
|
.attr("id", "tooltip") |
|
.style('position', 'absolute') |
|
.style("background-color", "#D3D3D3") |
|
.style('padding', 10) |
|
.style('display', 'none') |
|
|
|
var nodes = svg.append('g') |
|
.attr('class', 'nodes') |
|
|
|
var x_axis = svg.append("g") |
|
.attr("class", "x_axis") |
|
|
|
var y_axis = svg.append("g") |
|
.attr("class", "y_axis") |
|
|
|
// DATA PROCESSING |
|
var data = csv.map(function(d) { |
|
return { |
|
country: d.Country, |
|
year: d.Year, |
|
duration: parseInt(d.Year.split('-')[1]) - parseInt(d.Year.split('-')[0]), |
|
player: d.Player, |
|
star: d.Star_Player, |
|
club: d.Club_Name_1 + " " + d.Club_Name_2 |
|
} |
|
}) |
|
|
|
var xScale = d3.scaleLinear() |
|
.domain([2004, 2019]) |
|
.rangeRound([0, width]) |
|
|
|
var yScale = d3.scaleBand() |
|
.domain(countries.sort(function(x, y){ |
|
return d3.descending(x, y) |
|
})) |
|
.range([height, 0]) |
|
.padding(10) |
|
|
|
var dataNew = data.map((d,i) => { |
|
if(d.year.split('-')){ |
|
return { |
|
x : xScale(parseInt(d.year.split('-')[0])), |
|
y : yScale(d.country), |
|
country : d.country, |
|
year : d.year, |
|
player : d.player, |
|
star : d.star, |
|
club : d.club |
|
} |
|
} else { |
|
return { |
|
x : xScale(parseInt(d.year.split('-')[1])), |
|
y : yScale(d.country), |
|
country : d.country, |
|
year : d.year, |
|
player : d.player, |
|
star : d.star, |
|
club : d.club |
|
} |
|
} |
|
}) |
|
|
|
// Initialize force simulation |
|
var simulation = d3.forceSimulation() |
|
.force('charge', d3.forceManyBody().strength(1)) |
|
.force("collide", d3.forceCollide(2.5)) |
|
.alphaDecay(0.1) |
|
.velocityDecay(0.4) |
|
.stop() |
|
|
|
simulation |
|
.nodes(dataNew) |
|
.force('x', d3.forceX().strength(0.8).x(d => d.x)) |
|
.force('y', d3.forceY().strength(0.8).y(d => d.y)) |
|
|
|
for (var i = 0; i < 100; ++i) simulation.tick() // start simulation 'in the background' to update node positions before render |
|
|
|
// NODES (each representing an entity) |
|
var gnodes = nodes.selectAll('.node-group').data(dataNew) // Join new data with old elements, if any |
|
|
|
// After merging the entered elements with the update selection, apply operations to both. |
|
var entered_nodes = gnodes.enter().append('g') |
|
.merge(gnodes) |
|
.attr("class", function(d,i) { return "node-group"}) |
|
.attr("transform", function(d,i) { |
|
return "translate(" + d.x + "," + d.y + ")" |
|
}) |
|
.append("circle") |
|
.attr('id', function(d,i) { return "circle-" + d.player.replace(/[^A-Z0-9]+/ig, "_") + "-" + d.club.replace(/[^A-Z0-9]+/ig, "_") }) |
|
.attr('r', 2) |
|
.attr('fill', 'black') |
|
//.attr('fill', function(d) { return color(d.club) || '#fff' }) |
|
.attr('fill-opacity', 1) |
|
.attr('stroke', 'none') |
|
|
|
// CREATE INTERACTIVITY |
|
// can't use gnodes.on() because no nodes have been created yet. |
|
entered_nodes.on('mouseover', function (d,i) { |
|
d3.select(this).style("cursor", "pointer"); |
|
d3.select('#circle-' + d.player.replace(/[^A-Z0-9]+/ig, "_") + "-" + d.club.replace(/[^A-Z0-9]+/ig, "_")) |
|
.attr('r', 3.5) |
|
d3.selectAll("#tooltip") |
|
.style('display', 'block') |
|
}) |
|
.on('mousemove', function(d) { |
|
updateTooltipContent(d) |
|
}) |
|
.on('mouseout', function (d,i) { |
|
d3.select(this).style("cursor", "default"); |
|
d3.select('#circle-' + d.player.replace(/[^A-Z0-9]+/ig, "_") + "-" + d.club.replace(/[^A-Z0-9]+/ig, "_")) |
|
.attr('r', 2) |
|
d3.selectAll("#tooltip") |
|
.style('display', 'none') |
|
}) |
|
|
|
// AXES // |
|
xAxis = d3.axisTop(xScale).tickFormat(d3.format("d")).tickSizeOuter(0).tickSizeInner(0) |
|
yAxis = d3.axisLeft(yScale).tickSize(-width) |
|
|
|
d3.select(".x_axis") |
|
.call(xAxis) |
|
.call(g => { |
|
g.selectAll("text").attr("transform", `translate(0, 0)`) //shift tick labels to middle of interval |
|
.attr("y", axisPad) |
|
.attr('fill', '#A9A9A9') |
|
.style('font-size', 16) |
|
g.selectAll("line") |
|
.attr('stroke', '#A9A9A9') |
|
|
|
g.select(".domain").remove() |
|
|
|
}) |
|
|
|
d3.select(".y_axis") |
|
.call(yAxis) |
|
.call(g => { |
|
g.selectAll("text") |
|
.attr("x", -axisPad*2) |
|
.style("font-weight", "normal") |
|
.attr('fill', '#A9A9A9') |
|
.style("cursor", "pointer") |
|
|
|
g.selectAll("line") |
|
.attr('stroke', '#A9A9A9') |
|
.attr('stroke-width', 0.7) // make horizontal tick thinner and lighter so that line paths can stand out |
|
.attr('opacity', 0.3) |
|
|
|
g.select(".domain").remove() |
|
|
|
}) |
|
|
|
gnodes.exit().remove() // Remove old |
|
|
|
function updateTooltipContent(d) { |
|
|
|
var dup = dataNew.filter(b=>b.player == d.player) |
|
|
|
tooltip.html(d.player) |
|
.attr('class', 'text-' + d.player.replace(/[^A-Z0-9]+/ig, "_") + "-" + d.club.replace(/[^A-Z0-9]+/ig, "_")) |
|
.style("left", (d.x+ 20) + "px") |
|
.style("top", (d.y) + "px") |
|
.attr('text-anchor', 'middle') |
|
.attr('dy', '.35em') |
|
.attr('fill', '#555') |
|
.style('font-size', '10.5px') |
|
.style('font-weight', 'bold') |
|
.style('pointer-events', 'none') |
|
.append('div') |
|
.style('font-size', '9px') |
|
.html(d.club) |
|
|
|
} |
|
|
|
}) |
|
|
|
</script> |
|
</body> |