Block-a-Day #10. In this Gapminder recreation, continents can be exploded into their constituent countries with a paintball-splatter-like effect.
Data Sources: Gapminder.
What I Learned: I've wanted to build something with this technique ever since I saw Nadieh Bremer's demonstrations of it. My addition is to create a separate filter definition for each continent, enabling them to be individually transitioned. For more details, check out her original blog post and follow-up.
What I'd Do With More Time: The explosions have to wait until the next year tick. I'd like to be able to explode them immediately, but I'm not aware of a way to do this with d3.transition alone, so it would probably require a custom animation loop.
Just what it sounds like. For fifteen days, I will make a D3.js v4 block every single day. Rules:
forked from cmgiven's block: Gooey Exploding Scatterplot
forked from maritrinez's block: Gooey Exploding Scatterplot
xxxxxxxxxx
<meta charset="utf-8">
<style>
text { fill: #333; }
text.year {
font-family: monospace;
pointer-events: none;
}
#controls {
position: absolute;
top: 15px; right: 15px;
}
</style>
<body>
<div id="controls">
<button id="explode">Explode All</button>
<button id="collapse">Collapse All</button>
</div>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var years = d3.range(1950, 2015 + 1)
var interval = 500
var blurStable = '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 5 -7'
var blurIn = '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 35 -11'
var blurOut = '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 15 -7'
var margin = { top: 15, right: 15, bottom: 45, left: 55 }
var width = 960 - margin.left - margin.right
var height = 500 - margin.top - margin.bottom
var svg = d3.select('body')
.append('svg')
.attr('width', width + margin.left + margin.top)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
var x = d3.scaleLinear().range([0, width])
var y = d3.scaleLinear().range([height, 0])
var r = d3.scaleSqrt().range([0, 50])
var color = d3.scaleOrdinal()
.range(['#1b9e77', '#d95f02', '#7570b3', '#e7298a', '#e6ab02', '#666666'])
var xAxis = d3.axisBottom().scale(x)
var yAxis = d3.axisLeft().scale(y)
var yearLabel = svg.append('text')
.attr('class', 'year')
.attr('x', width / 2)
.attr('y', height / 2)
.attr('dy', '.28em')
.style('font-size', width / 3)
.style('text-anchor', 'middle')
.style('font-weight', 'bold')
.style('opacity', 0.2)
.text('1950')
d3.csv('data.csv', function (d) {
return {
country: d.country,
year: d.year,
life_expectancy: +d.life_expectancy,
total_fertility: +d.total_fertility,
population: +d.population,
continent: d.continent
}
}, initialize)
function initialize(error, data) {
if (error) { throw error }
x.domain([0, d3.max(data, function (d) { return d.total_fertility })]).nice()
y.domain([0, d3.max(data, function (d) { return d.life_expectancy })]).nice()
r.domain([0, d3.max(data, function (d) { return d.population })])
data = d3.nest()
.key(function (d) { return d.year })
.key(function (d) { return d.continent })
.entries(data)
data.forEach(function (d) {
d.values.forEach(function (e) {
e.population = d3.sum(e.values, function (f) { return f.population })
e.total_fertility = d3.sum(e.values, function (f) {
return f.population * f.total_fertility
}) / e.population
e.life_expectancy = d3.sum(e.values, function (f) {
return f.population * f.life_expectancy
}) / e.population
e.values.forEach(function (f) { f.parent = e })
})
})
var uniqueContinents = data[0].values.map(function (d) { return d.key })
// from https://www.visualcinnamon.com/2016/06/fun-data-visualizations-svg-gooey-effect.html
// modified to create a filter for each continent group, which can be individually transitioned
var filters = svg.append('defs')
.selectAll('filter')
.data(uniqueContinents)
.enter().append('filter')
.attr('id', function (d) { return 'gooeyCodeFilter-' + d.replace(' ', '-') })
filters.append('feGaussianBlur')
.attr('in', 'SourceGraphic')
.attr('stdDeviation', '10')
.attr('color-interpolation-filters', 'sRGB')
.attr('result', 'blur')
var blurValues = filters.append('feColorMatrix')
.attr('class', 'blurValues')
.attr('in', 'blur')
.attr('mode', 'matrix')
.attr('values', blurStable)
.attr('result', 'gooey')
filters.append('feBlend')
.attr('in', 'SourceGraphic')
.attr('in2', 'gooey')
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis)
.append('text')
.attr('x', width)
.attr('y', 30)
.style('text-anchor', 'end')
.style('font-weight', 'bold')
.text('Fertility (births per woman)')
svg.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('x', 0)
.attr('y', -30)
.style('text-anchor', 'end')
.style('font-weight', 'bold')
.text('Life expectancy (years)')
var yearIndex = 0
var year = '' + years[yearIndex]
var exploded = d3.set()
var blurTransition = d3.set()
d3.select('#explode').on('click', function () {
uniqueContinents.forEach(function (d) {
if (!exploded.has(d)) {
exploded.add(d)
blurTransition.add(d)
}
})
})
d3.select('#collapse').on('click', function () {
uniqueContinents.forEach(function (d) {
if (exploded.has(d)) {
exploded.remove(d)
blurTransition.add(d)
}
})
})
var continents = svg.selectAll('.continent')
.data(data[0].values)
.enter().append('g')
.attr('class', 'continent')
.style('filter', function (d) { return 'url(#gooeyCodeFilter-' + d.key.replace(' ', '-') + ')' })
continents.append('circle')
.attr('class', 'aggregate')
.attr('cx', width / 2)
.attr('cy', height / 2)
.style('fill', function (d) { return color(d.key) })
.on('click', function (d) { exploded.add(d.key); blurTransition.add(d.continent) })
.append('title').text(function (d) { return d.key })
update()
d3.interval(incrementYear, interval)
function incrementYear() {
year = '' + years[++yearIndex >= years.length ? yearIndex = 0 : yearIndex]
update()
}
function update() {
yearLabel.transition().duration(0).delay(interval / 2).text(year)
continents = continents.data(
data.find(function (d) { return d.key === year }).values,
function (d) { return d.key }
)
var countries = continents.selectAll('.country')
.data(function (d) { return d.values }, function (d) { return d.country })
countries.exit().remove()
var enterCountries = countries.enter().insert('circle', '.aggregate')
.attr('class', 'country')
.attr('cx', width / 2)
.attr('cy', height / 2)
.style('fill', function (d) { return color(d.continent) })
.on('click', function (d) { exploded.remove(d.continent); blurTransition.add(d.continent) })
enterCountries.append('title').text(function (d) { return d.country })
countries = countries.merge(enterCountries)
var t = d3.transition()
.ease(d3.easeLinear)
.duration(interval)
continents.select('.aggregate')
.transition(t)
.attr('r', function (d) { return exploded.has(d.key) ? 0 : r(d.population) })
.attr('cx', function (d) { return x(d.total_fertility) })
.attr('cy', function (d) { return y(d.life_expectancy) })
countries
.transition(t)
.attr('r', function (d) { return r(d.population) })
.attr('cx', function (d) { return x((exploded.has(d.continent) ? d : d.parent).total_fertility) })
.attr('cy', function (d) { return y((exploded.has(d.continent) ? d : d.parent).life_expectancy) })
blurValues
.transition(t)
.attrTween('values', function (d) {
if (blurTransition.has(d)) {
blurTransition.remove(d)
if (exploded.has(d)) {
return d3.interpolateString(blurIn, blurOut)
} else {
return d3.interpolateString(blurOut, blurIn)
}
}
return function () { return blurStable }
})
}
}
</script>
</body>
https://d3js.org/d3.v4.min.js