An example of a column chart visualising NSS scores per question at Imperial.
This is part of a series of visualisations called My Visual Vocabulary which aims to recreate every visualisation in the FT's Visual Vocabulary from scratch using D3.
xxxxxxxxxx
<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; }
text {
font-family: monospace;
}
.outline {
fill: none;
}
.below-bar {
fill: #d7191c;
}
.eq-bar {
fill: #c9c9c9;
}
.above-bar {
fill: #2c7bb6;
}
.legend-label {
font-size: 11px;
}
</style>
</head>
<body>
<script>
var margin = {top: 100, right: 200, bottom: 100, left: 100};
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 + ")");
function getValues(d) {
for (var i = 1; i <= 27; i++) {
d[i] = +d[i];
}
return d;
}
var englandMode = true;
var x = d3.scaleBand()
.range([0, width])
.padding(0.2);
var y = d3.scaleLinear()
.range([height, 0]);
d3.csv("nss-results.csv", getValues, function(error, results) {
if (error) throw error;
var englandAverage = results[results.length - 2],
imperialAverage = results[results.length - 1];
results = results.slice(0, -2);
results.forEach(d => {
var eng = [],
imp = [];
for (var i = 1; i <= 27; i++) {
eng[i - 1] = (d[i] - englandAverage[i]).toPrecision(2);
imp[i - 1] = (d[i] - imperialAverage[i]).toPrecision(2);
}
d.englandAvgDiff = eng;
d.imperialAvgDiff = imp;
});
var resultsByQuestion = d3.range(27);
resultsByQuestion = resultsByQuestion.map(d => c = {
belowEng: 0,
equalEng: 0,
aboveEng: 0,
belowImp: 0,
equalImp: 0,
aboveImp: 0
});
results.forEach(d => {
d.englandAvgDiff.forEach((x, i) => {
if (x > 0) resultsByQuestion[i].aboveEng++;
if (x < 0) resultsByQuestion[i].belowEng++;
if (x == 0) resultsByQuestion[i].equalEng++;
})
d.imperialAvgDiff.forEach((x, i) => {
if (x > 0) resultsByQuestion[i].aboveImp++;
if (x < 0) resultsByQuestion[i].belowImp++;
if (x == 0) resultsByQuestion[i].equalImp++;
})
});
x.domain(d3.range(27));
y.domain([0, results.length]);
var title = svg.append("g")
.attr("transform", "translate(" + [width / 2, -30] + ")");
title.append("text")
.attr("text-anchor", "middle")
.text("No. of Departments w/ Scores Above or Below England Avg Per Question");
var legend = svg.append("g")
.attr("transform", "translate(" + [width + margin.right / 5, 0] + ")");
legend.selectAll("rect")
.data(["above-bar", "eq-bar", "below-bar"])
.enter().append("rect")
.attr("class", d => d)
.attr("y", (d, i) => (height / 3) * i)
.attr("height", d => height / 3)
.attr("width", x.bandwidth());
legend.append("rect")
.attr("class", "outline")
.attr("height", height)
.attr("width", x.bandwidth());
legend.selectAll("text")
.data(["Above", "Equal", "Below"])
.enter().append("text")
.attr("class", "legend-label")
.attr("y", (d, i) => (height / 3) * i)
.attr("x", x.bandwidth())
.attr("dx", 10)
.attr("dy", height / 6)
.text(d => d)
var yAxisGroup = svg.append("g")
.attr("class", "y-axis-group");
var yAxis = yAxisGroup.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y).tickSize(0));
yAxis.attr("transform", "translate(" + [-5, 0] + ")");
yAxis.select(".domain").style("opacity", 0);
var xAxisGroup = svg.append("g")
.attr("class", "x-axis-group")
.attr("transform", "translate(" + [0, height] + ")")
var xAxis = xAxisGroup.append("g")
.attr("class", "x-axis")
.call(d3.axisBottom(x).tickSize(0).tickFormat(d => d + 1));
xAxis.attr("transform", "translate(" + [0, 5] + ")")
xAxis.select(".domain").style("opacity", 0);
var xLabel = xAxisGroup.append("text")
.attr("transform", "translate(" + [width / 2, margin.bottom / 2] + ")")
.attr("text-anchor", "middle")
.text("Question Number")
var barGroups = svg.append("g")
.attr("class", "bar-groups")
.selectAll("g")
.data(resultsByQuestion)
.enter().append("g")
.attr("class", "bar-group")
.attr("transform", (d, i) => "translate(" + [x(i), 0] + ")")
var aboveBars = barGroups.append("rect")
.attr("class", "above-bar")
.attr("y", d => y(d.belowEng + d.equalEng + d.aboveEng))
.attr("height", d => height - y(d.belowEng + d.equalEng + d.aboveEng))
.attr("width", x.bandwidth());
var eqBars = barGroups.append("rect")
.attr("class", "eq-bar")
.attr("y", d => y(d.equalEng + d.belowEng))
.attr("height", d => height - y(d.equalEng + d.belowEng))
.attr("width", x.bandwidth());
var belowBars = barGroups.append("rect")
.attr("class", "below-bar")
.attr("y", d => y(d.belowEng))
.attr("height", d => height - y(d.belowEng))
.attr("width", x.bandwidth());
var outlines = barGroups.append("rect")
.attr("class", "outline")
.attr("height", d => y(0))
.attr("width", x.bandwidth());
d3.select("svg").on("click", switchBetween);
function switchBetween() {
function newY(d) {
var aY, eY, bY;
if (englandMode) {
aY = y(d.belowImp + d.equalImp + d.aboveImp),
eY = y(d.equalImp + d.belowImp),
bY = y(d.belowImp);
} else {
aY = y(d.belowEng + d.equalEng + d.aboveEng),
eY = y(d.equalEng + d.belowEng),
bY = y(d.belowEng);
}
return [aY, eY, bY];
}
aboveBars
.transition()
.attr("y", d => newY(d)[0])
.attr("height", d => height - newY(d)[0]);
eqBars
.transition()
.attr("y", d => newY(d)[1])
.attr("height", d => height - newY(d)[1]);
belowBars
.transition()
.attr("y", d => newY(d)[2])
.attr("height", d => height - newY(d)[2]);
if (englandMode) {
title.select("text")
.text("No. of Departments w/ Scores Above or Below Imperial Avg Per Question");
} else {
title.select("text")
.text("No. of Departments w/ Scores Above or Below England Avg Per Question");
}
englandMode = !englandMode;
}
});
</script>
</body>
https://d3js.org/d3.v4.min.js