Skip to content

Instantly share code, notes, and snippets.

@tlfrd
Last active January 1, 2020 08:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tlfrd/1eaaf63bf8110247a6ff88cd5601a9b7 to your computer and use it in GitHub Desktop.
Save tlfrd/1eaaf63bf8110247a6ff88cd5601a9b7 to your computer and use it in GitHub Desktop.
Rank Chart
license: mit

A chart showing how the intra-university NSS ranks of Imperial departments have changed since 2012. Uses a multi-line voronoi for the tooltip.

Customisable starting animation which focuses on departments included in an array.

Inspired by Nadieh Bremer's Baby Name chart.

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400, 700" rel="stylesheet">
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
.x-axis path, .y-axis path {
display: none;
}
.rank-line {
fill: none;
stroke: black;
stroke-linejoin: round;
stroke-linecap: round;
}
.start-dot, .end-dot {
fill: grey;
}
.tooltip text {
font-size: 14px;
font-weight: 700;
fill: black;
}
.x-axis text {
font-size: 20px;
font-weight: 700;
}
.y-axis text {
font-weight: 700;
}
text {
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
font-family: 'Open Sans', sans-serif;
opacity: 0.7;
font-size: 18px;
}
.y-label {
fill: black;
font-size: 18px;
font-weight: 700;
}
.voronoi path {
fill: none;
pointer-events: all;
}
.grid-line {
stroke: black;
opacity: 0.2;
stroke-dasharray: 2,2;
}
.end-label {
font-size: 14px;
font-weight: 700;
fill: black;
fill-opacity: 0.7;
/* text-anchor: middle; */
}
</style>
</head>
<body>
<script>
var margin = {top: 50, right: 200, bottom: 100, left: 125};
var width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var svg = d3.select("body").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 cfg = {
strokeWidth: 10
};
var colour = d3.scaleOrdinal(d3.schemeCategory20);
// Use indexOf to fade in one by one
var highlight = ["Geology", "Mechanical Engineering", "Civil Engineering", "Aero", "Chemistry", "Physics"];
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height + cfg.strokeWidth);
var x = d3.scaleLinear()
.range([0, width]);
var y = d3.scaleLinear()
.range([0, height]);
var voronoi = d3.voronoi()
.x(d => x(d.year))
.y(d => y(d.rank))
.extent([[-margin.left / 2, -margin.top / 2], [width + margin.right / 2, height + margin.bottom / 2]]);
var line = d3.line()
.x(d => x(d.year))
.y(d => y(d.rank))
// Uncomment this to use monotone curve
// .curve(d3.curveMonotoneX);
d3.csv("nss-ranking.csv", function(error, data) {
if (error) throw error;
var parsedData = [];
data.forEach((d) => {
var dObj = {department: d.department, ranks: []};
for (var year in d) {
if (year != "department") {
if (d[year] != 0) {
dObj.ranks.push({year: +year, rank: +d[year], department: dObj});
}
}
}
parsedData.push(dObj);
});
var xTickNo = parsedData[0].ranks.length;
x.domain(d3.extent(parsedData[0].ranks, d => d.year));
colour.domain(data.map(d => d.department));
// Ranks
var ranks = 16;
y.domain([0.5, ranks]);
var axisMargin = 20;
var xAxis = d3.axisBottom(x)
.tickFormat(d3.format("d"))
.ticks(xTickNo)
.tickSize(0);
var yAxis = d3.axisLeft(y)
.ticks(ranks)
.tickSize(0);
var xGroup = svg.append("g");
var xAxisElem = xGroup.append("g")
.attr("transform", "translate(" + [0, height + axisMargin * 1.2] + ")")
.attr("class", "x-axis")
.call(xAxis);
xGroup.append("g").selectAll("line")
.data(x.ticks(xTickNo))
.enter().append("line")
.attr("class", "grid-line")
.attr("y1", 0)
.attr("y2", height + 10)
.attr("x1", d => x(d))
.attr("x2", d => x(d));
var yGroup = svg.append("g");
var yAxisElem = yGroup.append("g")
.attr("transform", "translate(" + [-axisMargin, 0] + ")")
.attr("class", "y-axis")
.call(yAxis);
yAxisElem.append("text")
.attr("class", "y-label")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90) translate(" + [-height / 2, -margin.left / 3] + ")")
.text("Intra-University Ranking");
yGroup.append("g").selectAll("line")
.data(y.ticks(ranks))
.enter().append("line")
.attr("class", "grid-line")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", d => y(d))
.attr("y2", d => y(d));
var lines = svg.append("g")
.selectAll("path")
.data(parsedData)
.enter().append("path")
.attr("class", "rank-line")
.attr("d", function(d) { d.line = this; return line(d.ranks)})
.attr("clip-path", "url(#clip)")
.style("stroke", d => colour(d.department))
.style("stroke-width", cfg.strokeWidth)
.style("opacity", 0.1)
.transition()
.duration(500)
.delay(d => (highlight.indexOf(d.department) + 1) * 500)
.style("opacity", d => highlight.includes(d.department) ? 1 : 0.1);
var endLabels = svg.append("g")
.attr("class", "end-labels")
.selectAll("text")
.data(parsedData.filter(d => highlight.includes(d.department)))
.enter().append("text")
.attr("class", "end-label")
.attr("x", d => x(d.ranks[d.ranks.length - 1].year))
.attr("y", d => y(d.ranks[d.ranks.length - 1].rank))
.attr("dx", 20)
.attr("dy", cfg.strokeWidth / 2)
.text(d => d.department)
.style("opacity", 0)
.transition()
.duration(500)
.delay(d => (highlight.indexOf(d.department) + 1) * 500)
.style("opacity", 1);
var endDots = svg.append("g")
.selectAll("circle")
.data(parsedData.filter(d => highlight.includes(d.department)))
.enter().append("circle")
.attr("class", "end-circle")
.attr("cx", d => x(d.ranks[d.ranks.length - 1].year))
.attr("cy", d => y(d.ranks[d.ranks.length - 1].rank))
.attr("r", cfg.strokeWidth)
.style("fill", d => colour(d.department))
.style("opacity", 0)
.transition()
.duration(500)
.delay(d => (highlight.indexOf(d.department) + 1) * 500)
.style("opacity", 1);
var tooltip = svg.append("g")
.attr("transform", "translate(-100, -100)")
.attr("class", "tooltip");
tooltip.append("circle")
.attr("r", cfg.strokeWidth);
tooltip.append("text")
.attr("class", "name")
.attr("y", -20);
var voronoiGroup = svg.append("g")
.attr("class", "voronoi");
voronoiGroup.selectAll("path")
.data(voronoi.polygons(d3.merge(parsedData.map(d => d.ranks))))
.enter().append("path")
.attr("d", function(d) { return d ? "M" + d.join("L") + "Z" : null; })
.on("mouseover", mouseover)
.on("mouseout", mouseout);
svg.selectAll(".rank-line")
.each(d => highlight.includes(d.department) ? d.line.parentNode.appendChild(d.line) : 0);
svg.select("g.end-labels").raise();
function mouseover(d) {
// Hide labels and dots from initial animation
svg.selectAll(".end-label").style("opacity", 0);
svg.selectAll(".end-circle").style("opacity", 0);
svg.selectAll(".rank-line").style("opacity", 0.1);
d3.select(d.data.department.line).style("opacity", 1);
d.data.department.line.parentNode.appendChild(d.data.department.line);
tooltip.attr("transform", "translate(" + x(d.data.year) + "," + y(d.data.rank) + ")")
.style("fill", colour(d.data.department.department))
tooltip.select("text").text(d.data.department.department)
.attr("text-anchor", d.data.year == x.domain()[0] ? "start" : "middle")
.attr("dx", d.data.year == x.domain()[0] ? -10 : 0)
}
function mouseout(d) {
svg.selectAll(".rank-line").style("opacity", d => highlight.includes(d.department) ? 1 : 0.1);
svg.selectAll(".end-label").style("opacity", 1);
svg.selectAll(".end-circle").style("opacity", 1);
tooltip.attr("transform", "translate(-100,-100)");
}
});
</script>
</body>
department 2012 2013 2014 2015 2016 2017
Geology 1 1 1 1 1 1
Mechanical Engineering 4 5 10 1 7 2
Civil Engineering 12 4 5 4 1 3
Bioengineering 3 3 9 4
EEE 2 5 7 8 4 5
Computer Science 4 2 3 5 6 6
Chemical Engineering 9 3 2 5 3 7
Materials 9 8 5 8 11 8
Medicine 7 14 9 7 7 8
Biology 15 5 13 14 11 10
Molecular Biology, Biophysics, Biochemisty 13 15 16 12 15 11
Maths 11 13 8 11 5 12
Biomedical Sciences 7 10 15 15 13 12
Aero 3 10 10 8 14 14
Chemistry 13 10 13 16 10 15
Physics 6 8 10 13 15 16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment