|
<!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); |
|
}); |
|
console.log(data) |
|
|
|
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> |