<body>
<h1>REFViewer in D3</h1>
<p>Using the <code>d3-rs-bars</code> component, this <a href="https://www.staff.city.ac.uk/~jwo/refviewer/">re-implements some of the excellent REFViewer</a> by Jo Wood of the giCentre at City University, London.</p>
<p><a href="https://www.ref.ac.uk/">Research Excellence Framework (REF)</a> scores represent the quality of research at each UK institution using the 2008 assessment. Darker red colours represent higher quality research scores either as a proportion of submitted staff ('Scale by staff %') or by the volume of staff submitted ('Scale by staff counts'). Toggle the buttons below to animate changes.</p>
<div class="options">
<h5>Scale by</h5><button id="staff-pct">staff %</button><button id="staff-count">staff counts</button>
</div>
<div class="options">
<h5>Order by</h5><button id="order-inst">institution</button><button id="order-4">4*</button><button id="order-43">4* + 3*</button>
</div>
<div id="elm"></div>
</body>
//RedSift
//Showcasing the d3-rs-bars library
const FILLS = ['#C8091A', '#DF3C47', '#F07679', '#FEADAA', '#FFE4DC']; //colours fill in hex value
const LEGEND = ['4*', '3*', '2*', '1*', 'u/c']; //legend
const BAR_WIDTH = 800; //width of bar
const BAR_HEIGHT = 8; //height of bar
const BAR_PAD = 1; //
let pct = d3.format('.0%'); // % formatter for the tip
let chart = d3_rs_bars.html('refviewer')
.width(BAR_WIDTH) //customise width
.fill(FILLS) //bars colour fill
.legend(LEGEND) //add legend
.orientation('left') //change the orientation
.legendOrientation('top') //add legend
.displayHtml((d, i) => d.l + ', ' + LEGEND[i] + ', ' + (d.v[i] > 0 && d.v[i] < 1 ? pct(d.v[i]) : d.v[i]))
.inset({
left: 180,
right: 130,
top: 16,
bottom: 0
}); //add tip display
function draw(data, animate) { //update bar on called
chart.barSize(BAR_HEIGHT); //update bar height
chart.height(data.length * (BAR_HEIGHT + BAR_PAD) + chart.margin() + chart.inset().top + chart.inset().bottom + 40); //update chart height
let elm = d3.select('#elm').datum(data); //update to element and data
if (animate === true) { //on animate
elm = elm.transition(); //add transition
}
elm.call(chart); // updating is a case of re-calling the chart
}
// helper to make the buttons exclusive
function makeOptions(ops, cb) {
ops.forEach(function(n, i) { //toggle element & button class
let node = d3.select(n); //select element
if (i === 0) {
node.attr('class', 'danger'); //button property
}
node.on('click', function() { //on click
let isSet = node.classed('danger'); //change the button class
if (isSet) return; //if true
isSet = !isSet; //if false
node.attr('class', isSet ? 'danger' : ''); //toggle class if isSet return true or false
ops.forEach(function(p, j) { //toggle element & button class
if (j !== i) {
d3.select(ops[j]).attr('class', ''); //remove class if statement is met
}
});
d3.event.stopPropagation(); //stop event
cb(i); //stop update
});
});
}
// massage the data into presentable structures
d3.tsv("//static.redsift.io/blog/refviewer/institutions.txt", function(raw) {
let lookup = {}; //create an object
// create lookups table of display values
raw.forEach(d => lookup[d['Institution name']] = d['Display']); //assign Institution name to Display
// from: https://www.staff.city.ac.uk/~jwo/refviewer/data/RAE2008In2014Format.txt
d3.tsv("//static.redsift.io/blog/refviewer/RAE2008In2014Format.txt", function(raw) {
let all = raw.map(d => ({ //map raw data
l: lookup[d['Institution name']], //Set Index to Institution name
v: [parseInt(d['4*']), parseInt(d['3*']), parseInt(d['2*']), parseInt(d['1*']), parseInt(d['unclassified'])] //get each data and parse each as an Int
}));
// use the nest function to group
let grouped = d3.nest()
.key(d => d.l) //group by `l`
.rollup(v => v.reduce(function(p, e) { //reduce array `v`
for (let i = 0; i < 5; ++i) {
p[i] = p[i] + e.v[i] //add element to array p
};
return p;
}, [0, 0, 0, 0, 0]))
.entries(all) //dataset `all`
.map(d => ({ //map data
l: d.key, //index data
v: d.value, //value data
t: d3.sum(d.value) //sum of value
}));
let normal = grouped.map(e => ({
l: e.l, //set index (Institution)
v: e.v.map(d => d / e.t) //set value dividing for stars
}));
let staff = 0;
let sort = 0;
function update(animate) { //update function for animation
let data;
if (staff === 1) { //toggle staff data
data = grouped.slice(); //slice grouped dataset to display staff
} else {
data = normal.slice(); //set using normal dataset
}
if (sort === 1) { //toggle to sort = 1
data = data.sort((a, b) => a.v[0] < b.v[0] ? 1 : a.v[0] > b.v[0] ? -1 : 0); //draw animation on selected button
} else if (sort === 2) { //toggle sort = 2
data = data.sort((a, b) => a.v[0] + a.v[1] < b.v[0] + b.v[1] ? 1 : a.v[0] + a.v[1] > b.v[0] + b.v[1] ? -1 : 0); //draw animation on selected button
}
draw(data, animate); //generate chart
}
// draw initial state
update(false);
//function generating each chart
makeOptions(['#staff-pct', '#staff-count'], (i) => (staff = i, update(true)));
makeOptions(['#order-inst', '#order-4', '#order-43'], (i) => (sort = i, update(true)));
});
});
View Compiled