Based from THE article Top universities with the best student-to-staff ratio 2017, wanted to see the student-to-staff ratio compared to student numbers.
Last active
June 8, 2017 16:54
-
-
Save eesur/2ac63b3d0ece6682a42c0f9d3a6bfabc to your computer and use it in GitHub Desktop.
d3 | concentric circles
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var render = (function () { | |
// keys for concentric circles | |
var dataKeys = ['studentNumber', 'femalePercentRatio', 'malePercentRatio', 'intlStudentsPercent'] | |
// helpers | |
var width = 300 | |
var height = 250 | |
var t = d3.transition() | |
.duration(400) | |
.ease(d3.easeLinear) | |
// pass in a full value (student number) | |
// and pass in the percentage to calculate number | |
// these are relative to the total students of 'a' uni | |
function students(total, part) { | |
var percentage = d3.scaleLinear() | |
.domain([0, 100]) // pass in a percent | |
.range([0, total]) | |
return percentage(part) | |
} | |
// colour each circle | |
var sequentialScale = d3.scaleSequential() | |
.domain([0, 4]) | |
.interpolator(d3.interpolateRainbow) | |
// colour each circle | |
var col = d3.scaleOrdinal() | |
.domain(dataKeys) | |
.range(['#ab6dc5','#9ec94d','#76b021', '#44a4f6']) | |
var labels = d3.scaleOrdinal() | |
.domain(dataKeys) | |
.range(['No. Students: ','Female %: ','Male %: ', 'International %:']) | |
function update(data, bindTo) { | |
var maxStudents = d3.max(data, function (d) { return d.studentNumber; }) | |
// area of each circle taking the highest number of students | |
var sqrtScale = d3.scaleSqrt() | |
.domain([0, maxStudents]) | |
.range([0, 110]) | |
// render grid | |
var update = d3.select(bindTo) | |
.selectAll('.js-circle') | |
.data(data) | |
update.exit().remove() | |
var enter = update.enter() | |
.append('div') | |
.attr('class', 'block js-circle') | |
// create a title which is a link | |
enter.merge(update) | |
.append('a') | |
.attr('href', function (d) { return d.link; }) | |
.attr('class', 'block') | |
.append('h1') | |
.attr('class', 'js-uni-title') | |
.append('a') | |
.text(function (d) { return d.institution; }) | |
enter.merge(update).append('h2') | |
.attr('class', 'js-rank') | |
.text(function (d) { return d.rank; }) | |
// svg in each grid | |
var svg = enter.merge(update).append('svg') | |
.attr('class', function (d, i) { return 'js-svg svg-' + i; }) | |
.attr('width', width) | |
.attr('height', height) | |
// label for selected circle | |
var circleInfo = enter.merge(update) | |
.append('h3') | |
.attr('class', function (d, i) { return 'block js-circle-info js-circle-info-' + i; }) | |
.html(function (d) { return ("<span>Staff per student:</span> " + (d.studentStaffRatio)); }) | |
// append set of circles for each of the datakeys | |
// to each grid item | |
data.forEach(function(o, n) { | |
// extract the data and order it | |
// ensuring circles render largest to smallest | |
var list = [] | |
// create a list using the keys for the circles and current data object | |
dataKeys.forEach(function(_k, _n) { | |
return list.push( | |
{ | |
value: o[_k], // reference the value using the key | |
name: _k // reference the name | |
} | |
) | |
}) | |
// sort it in descending order | |
list.sort(function(x, y) { | |
return d3.ascending(y.value, x.value) | |
}) | |
console.log('list', list) | |
// render the set of circles | |
d3.select('.svg-' + n).selectAll('circle') | |
.data(list) | |
.enter().append('circle') | |
.attr('class', function (d, i) { return ("cc c-" + i + " " + (d.name)); }) | |
.attr('r', function (d) { | |
if (d.name == 'studentNumber') { | |
return sqrtScale(o[d.name]) | |
} else { | |
var v = students(o.studentNumber, d.value) | |
console.log('v', v) | |
return sqrtScale(v) | |
} | |
}) | |
.attr('cx', width/2) | |
.attr('cy', height/2) | |
.style('fill', 'transparent') | |
.style('stroke-width', 4) | |
.style('stroke', function (d) { return col(d.name); }) | |
.on('mouseover', function(d, i) { | |
mouseoverValues(d.name) | |
mouseOverHighlight(i) | |
}) | |
.on('mouseout', function(d) { | |
mouseOutReset(d) | |
}) | |
}) | |
function mouseoverValues(key) { | |
circleInfo | |
.html(function (d) { return ("<span>" + (labels(key)) + "</span> " + (d[key])); }) | |
} | |
function mouseOverHighlight(index) { | |
d3.selectAll('.cc') | |
.interrupt() | |
.transition(t) | |
.style('opacity', 0.1) | |
d3.selectAll('.c-' + index) | |
.interrupt() | |
.transition(t) | |
.style('opacity', 1) | |
} | |
function mouseOutReset(d) { | |
d3.selectAll('.cc') | |
.interrupt() | |
.transition(t) | |
.style('opacity', 1) | |
circleInfo | |
.html(function (d) { return ("<span>Staff per student:</span> " + (d.studentStaffRatio)); }) | |
} | |
function legend() { | |
var legend = d3.select('#legend').append('svg') | |
.attr('class', 'js-legend') | |
.attr('width', 700) | |
.attr('height', 40) | |
legend.selectAll('rect.legend-items') | |
.data(dataKeys) | |
.enter().append('rect') | |
.attr('class', 'legend-items') | |
.attr('width', 163) | |
.attr('height', 30) | |
.attr('fill', function (d) { return col(d); }) | |
.attr('y', 0) | |
.attr('x', function (d, i) { return i * 166; }) | |
.on('mouseover', function(d, i) { | |
mouseoverValues(d) | |
mouseOverHighlight(i) | |
}) | |
.on('mouseout', function(d) { | |
mouseOutReset(d) | |
}) | |
legend.selectAll('text.legend-lables') | |
.data(dataKeys) | |
.enter().append('text') | |
.attr('class', 'legend-labels') | |
.attr('y', 20) | |
.attr('x', function (d, i) { return i * 166 + 10; }) | |
.text(function (d) { return labels(d); }) | |
} | |
legend(); | |
} | |
return update | |
})() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ | |
{ | |
"institution": "Oregon Health and Science University", | |
"location": "Portland, Oregon, US", | |
"rank": "1", | |
"studentNumber": 2861, | |
"studentStaffRatio": 1.1, | |
"intlStudentsPercent": 2, | |
"femalePercentRatio": 65, | |
"malePercentRatio": 35, | |
"link": "https://www.timeshighereducation.com/world-university-rankings/oregon-health-and-science-university#ranking-dataset/134377" | |
}, | |
{ | |
"institution": "Saitama Medical University", | |
"location": "Tokyo, Japan", | |
"rank": "2", | |
"studentNumber": 1889, | |
"studentStaffRatio": 1.5, | |
"intlStudentsPercent": 0, | |
"femalePercentRatio": 53, | |
"malePercentRatio": 47, | |
"link": "https://www.timeshighereducation.com/world-university-rankings/saitama-medical-university#ranking-dataset/608682" | |
}, | |
{ | |
"institution": "Rush University", | |
"location": "Chicago, US", | |
"rank": "=3", | |
"studentNumber": 1987, | |
"studentStaffRatio": 2.2, | |
"intlStudentsPercent": 4, | |
"femalePercentRatio": 71, | |
"malePercentRatio": 29, | |
"link": "https://www.timeshighereducation.com/world-university-rankings/rush-university#ranking-dataset/612573" | |
}, | |
{ | |
"institution": "Tata Institute of Fundamental Research", | |
"location": "Mumbai, India", | |
"rank": "=3", | |
"studentNumber": 579, | |
"studentStaffRatio": 2.2, | |
"intlStudentsPercent": 1, | |
"femalePercentRatio": 33, | |
"malePercentRatio": 67, | |
"link": "https://www.timeshighereducation.com/world-university-rankings/tata-institute-fundamental-research#ranking-dataset/600172" | |
}, | |
{ | |
"institution": "Jikei University School of Medicine", | |
"location": "Tokyo, Japan", | |
"rank": "5", | |
"studentNumber": 1034, | |
"studentStaffRatio": 0.8, | |
"intlStudentsPercent": 0, | |
"femalePercentRatio": 44, | |
"malePercentRatio": 56, | |
"link": "https://www.timeshighereducation.com/world-university-rankings/jikei-university-school-medicine#ranking-dataset/608682" | |
} | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<!-- http://www.basscss.com/ --> | |
<link href="https://unpkg.com/[email protected]/css/basscss.min.css" rel="stylesheet"> | |
<!-- http://clrs.cc/ --> | |
<!-- <link href="//s3-us-west-2.amazonaws.com/colors-css/2.2.0/colors.min.css" rel="stylesheet"> --> | |
<style> | |
body { font-family: Consolas, monaco, monospace; padding-left: 20px;} | |
.js-circle { | |
background: #eee; | |
position: relative; | |
margin: 3px; | |
width: 320px; | |
height: 360px; | |
padding-left: 8px; | |
} | |
a { | |
text-decoration: none; | |
color: #454545; | |
} | |
a:hover { | |
color: tomato; | |
} | |
text.legend-labels { | |
font-size: 11px; | |
fill: #fff; | |
letter-spacing: 2px; | |
pointer-events: none; | |
} | |
rect.legend-items:hover { | |
opacity: 0.8; | |
} | |
h1.js-uni-title { | |
font-size: 14px; | |
letter-spacing: 2px; | |
max-width: 250px; | |
font-weight: normal; | |
} | |
h2.js-rank { | |
position: absolute; | |
top: -6px; | |
right: 20px; | |
color: #fff; | |
} | |
h3.js-circle-info { | |
position: absolute; | |
bottom: 5px; | |
left: 8px; | |
} | |
span { | |
color: #888; | |
font-size: 11px; | |
text-transform: uppercase; | |
letter-spacing: 3px; | |
} | |
</style> | |
</head> | |
<body> | |
<header> | |
<p>Each concentric circle represents number of students:</p> | |
<nav id="legend"></nav> | |
<span>Note: no circle means the value is zero</span> | |
</header> | |
<div id="vis" class="flex flex-wrap max-width-1600 mx-auto my2 js-circles"></div> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<!-- d3 code --> | |
<script src=".script-compiled.js" charset="utf-8"></script> | |
<!-- render code --> | |
<script> | |
d3.json('data.json', function(error, data) { | |
render(data, '#vis') | |
}) | |
// change frame height | |
d3.select(self.frameElement).style('height', '1250px'); | |
</script> | |
</body> | |
</html> | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const render = (function () { | |
// keys for concentric circles | |
const dataKeys = ['studentNumber', 'femalePercentRatio', 'malePercentRatio', 'intlStudentsPercent'] | |
// helpers | |
const width = 300 | |
const height = 250 | |
const t = d3.transition() | |
.duration(400) | |
.ease(d3.easeLinear) | |
// pass in a full value (student number) | |
// and pass in the percentage to calculate number | |
// these are relative to the total students of 'a' uni | |
function students(total, part) { | |
const percentage = d3.scaleLinear() | |
.domain([0, 100]) // pass in a percent | |
.range([0, total]) | |
return percentage(part) | |
} | |
// colour each circle | |
const sequentialScale = d3.scaleSequential() | |
.domain([0, 4]) | |
.interpolator(d3.interpolateRainbow) | |
// colour each circle | |
const col = d3.scaleOrdinal() | |
.domain(dataKeys) | |
.range(['#ab6dc5','#9ec94d','#76b021', '#44a4f6']) | |
const labels = d3.scaleOrdinal() | |
.domain(dataKeys) | |
.range(['No. Students: ','Female %: ','Male %: ', 'International %:']) | |
function update(data, bindTo) { | |
const maxStudents = d3.max(data, d => d.studentNumber) | |
// area of each circle taking the highest number of students | |
const sqrtScale = d3.scaleSqrt() | |
.domain([0, maxStudents]) | |
.range([0, 110]) | |
// render grid | |
const update = d3.select(bindTo) | |
.selectAll('.js-circle') | |
.data(data) | |
update.exit().remove() | |
const enter = update.enter() | |
.append('div') | |
.attr('class', 'block js-circle') | |
// create a title which is a link | |
enter.merge(update) | |
.append('a') | |
.attr('href', d => d.link) | |
.attr('class', 'block') | |
.append('h1') | |
.attr('class', 'js-uni-title') | |
.append('a') | |
.text(d => d.institution) | |
enter.merge(update).append('h2') | |
.attr('class', 'js-rank') | |
.text(d => d.rank) | |
// svg in each grid | |
const svg = enter.merge(update).append('svg') | |
.attr('class', (d, i) => 'js-svg svg-' + i) | |
.attr('width', width) | |
.attr('height', height) | |
// label for selected circle | |
const circleInfo = enter.merge(update) | |
.append('h3') | |
.attr('class', (d, i) => 'block js-circle-info js-circle-info-' + i) | |
.html(d => `<span>Staff per student:</span> ${d.studentStaffRatio}`) | |
// append set of circles for each of the datakeys | |
// to each grid item | |
data.forEach(function(o, n) { | |
// extract the data and order it | |
// ensuring circles render largest to smallest | |
let list = [] | |
// create a list using the keys for the circles and current data object | |
dataKeys.forEach(function(_k, _n) { | |
return list.push( | |
{ | |
value: o[_k], // reference the value using the key | |
name: _k // reference the name | |
} | |
) | |
}) | |
// sort it in descending order | |
list.sort(function(x, y) { | |
return d3.ascending(y.value, x.value) | |
}) | |
console.log('list', list) | |
// render the set of circles | |
d3.select('.svg-' + n).selectAll('circle') | |
.data(list) | |
.enter().append('circle') | |
.attr('class', (d, i) => `cc c-${i} ${d.name}`) | |
.attr('r', d => { | |
if (d.name == 'studentNumber') { | |
return sqrtScale(o[d.name]) | |
} else { | |
let v = students(o.studentNumber, d.value) | |
console.log('v', v) | |
return sqrtScale(v) | |
} | |
}) | |
.attr('cx', width/2) | |
.attr('cy', height/2) | |
.style('fill', 'transparent') | |
.style('stroke-width', 4) | |
.style('stroke', (d) => col(d.name)) | |
.on('mouseover', function(d, i) { | |
mouseoverValues(d.name) | |
mouseOverHighlight(i) | |
}) | |
.on('mouseout', function(d) { | |
mouseOutReset(d) | |
}) | |
}) | |
function mouseoverValues(key) { | |
circleInfo | |
.html(d => `<span>${labels(key)}</span> ${d[key]}`) | |
} | |
function mouseOverHighlight(index) { | |
d3.selectAll('.cc') | |
.interrupt() | |
.transition(t) | |
.style('opacity', 0.1) | |
d3.selectAll('.c-' + index) | |
.interrupt() | |
.transition(t) | |
.style('opacity', 1) | |
} | |
function mouseOutReset(d) { | |
d3.selectAll('.cc') | |
.interrupt() | |
.transition(t) | |
.style('opacity', 1) | |
circleInfo | |
.html(d => `<span>Staff per student:</span> ${d.studentStaffRatio}`) | |
} | |
function legend() { | |
const legend = d3.select('#legend').append('svg') | |
.attr('class', 'js-legend') | |
.attr('width', 700) | |
.attr('height', 40) | |
legend.selectAll('rect.legend-items') | |
.data(dataKeys) | |
.enter().append('rect') | |
.attr('class', 'legend-items') | |
.attr('width', 163) | |
.attr('height', 30) | |
.attr('fill', d => col(d)) | |
.attr('y', 0) | |
.attr('x', (d, i) => i * 166) | |
.on('mouseover', function(d, i) { | |
mouseoverValues(d) | |
mouseOverHighlight(i) | |
}) | |
.on('mouseout', function(d) { | |
mouseOutReset(d) | |
}) | |
legend.selectAll('text.legend-lables') | |
.data(dataKeys) | |
.enter().append('text') | |
.attr('class', 'legend-labels') | |
.attr('y', 20) | |
.attr('x', (d, i) => i * 166 + 10) | |
.text(d => labels(d)) | |
} | |
legend(); | |
} | |
return update | |
})() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment