Skip to content

Instantly share code, notes, and snippets.

@eesur
Last active June 8, 2017 16:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eesur/2ac63b3d0ece6682a42c0f9d3a6bfabc to your computer and use it in GitHub Desktop.
Save eesur/2ac63b3d0ece6682a42c0f9d3a6bfabc to your computer and use it in GitHub Desktop.
d3 | concentric circles
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
})()
[
{
"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"
}
]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- http://www.basscss.com/ -->
<link href="https://unpkg.com/basscss@8.0.2/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>
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