|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
|
|
<script src="//d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
|
|
|
|
/////////////////////// |
|
// Parse the Data |
|
d3.csv("data.csv", function(inData) { |
|
|
|
// NOTE: temp hack workaround, theres too much data now :( |
|
data = inData.filter(function(d) { |
|
if(parseFloat(d['year']) > 2005) { |
|
return d; |
|
} |
|
}); |
|
|
|
// add a css safe class for use in hover interactions and coloring |
|
data.forEach(function(d) { |
|
d['class'] = d['club'].toLowerCase().replace(/ /g, '-').replace(/\./g,''); |
|
}) |
|
|
|
/////////////////////// |
|
// Chart Size Setup |
|
var margin = { top: 35, right: 0, bottom: 30, left: 70 }; |
|
|
|
var width = 960 - margin.left - margin.right; |
|
var height = 500 - margin.top - margin.bottom; |
|
|
|
var chart = d3.select(".chart") |
|
.attr("width", 960) |
|
.attr("height", 500) |
|
.append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
/////////////////////// |
|
// Scales |
|
var x = d3.scaleBand() |
|
.domain(data.map(function(d) { return d['year']; })) |
|
.rangeRound([25, width - 15]); |
|
|
|
var y = d3.scaleLinear() |
|
.domain([d3.min(data, function(d) { return d['points'] }) / 1.1, d3.max(data, function(d) { return d['points']; }) * 1.05]) |
|
.range([height, 0]); |
|
|
|
var size = d3.scaleLinear() |
|
.domain(d3.extent(data, function(d) { return d['goals_for']; })) |
|
.range([3, 10]); |
|
|
|
/////////////////////// |
|
// Axis |
|
var xAxis = d3.axisBottom(x); |
|
|
|
var yAxis = d3.axisLeft(y); |
|
|
|
chart.append("g") |
|
.attr("class", "x axis") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(xAxis); |
|
|
|
chart.append("g") |
|
.attr("class", "y axis") |
|
.call(yAxis); |
|
|
|
/////////////////////// |
|
// Title |
|
chart.append("text") |
|
.text('MLS: Points per Season (hover over a dot to focus, click to keep focus)') |
|
.attr("text-anchor", "middle") |
|
.attr("class", "graph-title") |
|
.attr("y", -10) |
|
.attr("x", width / 2.0); |
|
|
|
chart.append("text") |
|
.text('Points Per Season') |
|
.attr("text-anchor", "middle") |
|
.attr("class", "graph-title") |
|
.attr("y", -35) |
|
.attr("x", width / -4.0) |
|
.attr("transform", "rotate(-90)"); |
|
|
|
/////////////////////// |
|
// Points |
|
|
|
data.forEach(function(d) { |
|
d.radius = size(d['goals_for']); |
|
d.x = x(d['year']) + x.bandwidth() / 2.0; |
|
d.y = y(d['points']); |
|
}); |
|
|
|
// do each year separately so the forces don't effect other years and cause the |
|
// chart to "bulge" out |
|
x.domain().forEach(function(year) { |
|
|
|
var currData = data.filter(function(d) { |
|
if(d['year'] == year) { |
|
return d; |
|
} |
|
}); |
|
|
|
var node = chart.append("g") |
|
.attr("class", "year-" + year) |
|
.selectAll("circle") |
|
.data(currData) |
|
.enter().append("circle") |
|
.attr("class", "point") |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }) |
|
.attr('fill', 'blue') |
|
// replace spaces with - and remove '.' (from d.c. united) |
|
.attr("class", function(d) { return d['club'].toLowerCase().replace(/ /g, '-').replace(/\./g,'') }) |
|
.attr("r", function(d) { return size(d['goals_for']) }) |
|
.attr('opacity', '0.3'); |
|
|
|
// Apply default forces to simulation |
|
var simulation = d3.forceSimulation() |
|
.force("charge", d3.forceManyBody().strength(function(d) { |
|
// base it on the radius of the node |
|
var multiplier = -0.04; |
|
return Math.pow((d.radius), 1.5) * multiplier; |
|
})); |
|
|
|
// Add the nodes to the simulation, and specify how to draw |
|
simulation.nodes(currData) |
|
.on("tick", function() { |
|
// The d3 force simulation updates the x & y coordinates |
|
// of each node every tick/frame, based on the various active forces. |
|
// It is up to us to translate these coordinates to the screen. |
|
node.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }); |
|
}); |
|
}); |
|
|
|
/////////////////////// |
|
// Tooltips |
|
var tooltip = d3.select("body").append("div") |
|
.attr("class", "tooltip"); |
|
|
|
chart.selectAll("circle") |
|
.on("mouseover", function(d) { |
|
chart.selectAll('.' + d['class']) |
|
.classed('active', true); |
|
|
|
var tooltip_str = "Club: " + d['club'] + |
|
"<br/>" + "Year: " + d['year'] + |
|
"<br/>" + "Points: " + d['points'] + |
|
"<br/>" + "W/L/T: " + d['wins'] + " / " + d['losses'] + " / " + d['ties'] + |
|
"<br/>" + "Goals F/A: " + d['goals_for'] + " / " + d['goals_against']; |
|
|
|
if(d['alias'] != '') { |
|
tooltip_str += "<br/>(aka: " + d['alias'] + ")"; |
|
} |
|
|
|
tooltip.html(tooltip_str) |
|
.style("visibility", "visible"); |
|
}) |
|
.on("mousemove", function(d) { |
|
tooltip.style("top", event.pageY - (tooltip.node().clientHeight + 5) + "px") |
|
.style("left", event.pageX - (tooltip.node().clientWidth / 2.0) + "px"); |
|
}) |
|
.on("mouseout", function(d) { |
|
chart.selectAll('.'+d['class']) |
|
.classed('active', false); |
|
|
|
tooltip.style("visibility", "hidden"); |
|
}) |
|
.on('click', function(d) { |
|
chart.selectAll('.' + d['class']) |
|
.classed('click-active', function(d) { |
|
// toggle state |
|
return !d3.select(this).classed('click-active'); |
|
}); |
|
}) |
|
|
|
}); |
|
</script> |
|
|
|
<style> |
|
|
|
.click-active { |
|
opacity: 1.0; |
|
z-index: 1000; |
|
r: 8; |
|
} |
|
|
|
.active { |
|
opacity: 1.0; |
|
z-index: 1000; |
|
r: 8; |
|
} |
|
|
|
.axis text { |
|
font: 10px sans-serif; |
|
} |
|
|
|
.axis path, |
|
.axis line { |
|
fill: none; |
|
stroke: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
.x.axis path { |
|
display: none; |
|
} |
|
|
|
.tooltip { |
|
position: absolute; |
|
padding: 10px; |
|
font: 12px sans-serif; |
|
background: #222; |
|
color: #fff; |
|
border: 0px; |
|
border-radius: 8px; |
|
pointer-events: none; |
|
opacity: 0.9; |
|
visibility: hidden; |
|
} |
|
|
|
/* soccer team colors */ |
|
/* http://teamcolors.arc90.com/ */ |
|
.chicago-fire { |
|
fill: #AF2626; |
|
stroke: #0A174A; |
|
} |
|
.colorado-rapids { |
|
fill: #91022D; |
|
stroke: #85B7EA; |
|
} |
|
.columbus-crew-sc { |
|
fill: #FFDB00; |
|
stroke: #000000; |
|
} |
|
.dc-united { |
|
fill: #DD0000; |
|
stroke: #000000; |
|
} |
|
.fc-dallas { |
|
fill: #CF0032; |
|
stroke: #07175C; |
|
} |
|
.houston-dynamo { |
|
fill: #F36600; |
|
stroke: #85b7EA; |
|
} |
|
.la-galaxy { |
|
fill: #00245D; |
|
stroke: #FFD200; |
|
} |
|
.montreal-impact { |
|
fill: #122089; |
|
stroke: #7A878F; |
|
} |
|
.new-england-revolution { |
|
fill: #0A2141; |
|
stroke: #D80016; |
|
} |
|
.new-york-city-fc { |
|
fill: #6CADDF; |
|
stroke: #00285E; |
|
} |
|
.new-york-red-bulls { |
|
fill: #FFFFFF; |
|
stroke: #D50031; |
|
} |
|
.orlando-city-sc { |
|
fill: #633492; |
|
stroke: #FDE192; |
|
} |
|
.philadelphia-union { |
|
fill: #B18500; |
|
stroke: #348AE1; |
|
} |
|
.portland-timbers { |
|
fill: #004812; |
|
stroke: #EBE72B; |
|
} |
|
.real-salt-lake { |
|
fill: #F2D11A; |
|
stroke: #A50531; |
|
} |
|
.san-jose-earthquakes { |
|
fill: #0051BA; |
|
stroke: #000000; |
|
} |
|
.seattle-sounders-fc { |
|
fill: #4F8A10; |
|
stroke: #11568C; |
|
} |
|
.sporting-kansas-city { |
|
fill: #91B0D5; |
|
stroke: #002B5C; |
|
} |
|
.toronto-fc { |
|
fill: #D80016; |
|
stroke: #313F49; |
|
} |
|
.vancouver-whitecaps-fc { |
|
fill: #12264C; |
|
stroke: #85B7EA; |
|
} |
|
|
|
/* defunct teams :( */ |
|
.tampa-bay-mutiny { /* Using tampa bay rays colors */ |
|
fill: #092C5C; |
|
stroke: #8FBCE6; |
|
} |
|
.miami-fusion { /* Using miami marlins colors */ |
|
fill: #0077C8; |
|
stroke: #FFD100; |
|
} |
|
.cd-chivas-usa { |
|
fill: #FFF; |
|
stroke: #0A2141; /* blue of new england and ny red bulls */ |
|
} |
|
|
|
</style> |
|
|
|
<body> |
|
<svg class="chart"></svg> |
|
</body> |