<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>
body {
  margin: 1em;
}

button {
  margin-left: 1em;
}

div.options {
  display: flex;
  align-items: baseline;
  margin-top: 1em;
}
//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

External CSS

  1. //static.redsift.io/reusable/ui-rs-core/latest/css/ui-rs-core.min.css

External JavaScript

  1. //d3js.org/d3.v4.min.js
  2. //static.redsift.io/reusable/d3-rs-bars/latest/d3-rs-bars.umd-es2015.min.js