Another riff on /cjhin/11ffe8165207693d513ad67f85d43251
This version uses d3 lines + d3 circles/points to create something similar to a bump chart.
How To:
Sources:
xxxxxxxxxx
<meta charset="utf-8">
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
///////////////////////
// Parse the Data
d3.csv("data.csv", function(data) {
// For each row, calculate the "finishing" position
// first sort, year, then points, then goals_for
data.sort(function(a, b) {
if(b['year'] != a['year']) {
return b['year'] - a['year'];
}
if(b['points'] != a['points']) {
return b['points'] - a['points'];
}
if(b['goals_for'] != a['goals_for']) {
return b['goals_for'] - a['goals_for'];
}
});
// Then add the position with a simple integer increment
// now that the data is "in order"
var pos = 1;
data[0].position = pos;
for(var i=1; i<data.length; i++) {
// this is a new year, so start over
if(data[i - 1].year != data[i].year) {
pos = 1;
} else {
pos++;
}
data[i].position = pos;
}
// 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']; }).reverse())
.rangeRound([25, width - 15]);
var y = d3.scaleLinear()
.domain([d3.min(data, function(d) { return d['position'] }), d3.max(data, function(d) { return d['position']; })])
.range([20, height - 30]);
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(-"+ x.bandwidth()/2.0 +"," + height + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
///////////////////////
// Title
chart.append("text")
.text('MLS: Position 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('Final Position')
.attr("text-anchor", "middle")
.attr("class", "graph-title")
.attr("y", -35)
.attr("x", width / -4.0)
.attr("transform", "rotate(-90)");
///////////////////////
// Lines
var clubs = d3.map(data, function(d) {
return d['club'];
}).keys();
clubs.forEach(function(club) {
var currData = data.filter(function(d) {
if(d['club'] == club) {
return d;
}
});
var line = d3.line()
.x(function(d) { return x(d['year']); })
.y(function(d) { return y(d['position']); });
chart.append("path")
.datum(currData)
.attr("class", club.toLowerCase().replace(/ /g, '-').replace(/\./g,'') )
.attr("style", "fill:none !important")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 2)
.attr("stroke-opacity", 0.1)
.attr("d", line);
});
///////////////////////
// Nodes
var node = chart.append("g")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("class", "point")
.attr("cx", function(d) { return x(d['year']); })
.attr("cy", function(d) { return y(d['position']); })
.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", 6)
//.attr("r", function(d) { return size(d['goals_for']) })
.attr("stroke-width", 1.5)
.attr('opacity', '0.6');
///////////////////////
// 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, .active {
opacity: 1.0;
stroke-opacity: 1.0;
z-index: 1000;
/*r: 8;*/
}
path.click-active {
stroke-width: 3.0;
}
path.active {
stroke-width: 3.0;
}
.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 */
/* https://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>
https://d3js.org/d3.v4.min.js