|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
body { font-family: sans-serif; } |
|
#controls { height: 20px; padding: 15px; } |
|
#chart { position: relative; height: 450px; } |
|
#chart canvas, #chart canvas { position: absolute; top: 0; left: 0;} |
|
input[type="range"]{ |
|
-webkit-appearance: none !important; |
|
width: 100%; |
|
height: 4px; |
|
background: #aaa; |
|
border-radius: 4px; |
|
} |
|
input[type="range"]::-webkit-slider-thumb{ |
|
-webkit-appearance: none !important; |
|
width: 28px; |
|
height: 28px; |
|
background: #17b; |
|
border-radius: 28px; |
|
} |
|
text { fill: #333; } |
|
text.year { |
|
font-family: monospace; |
|
pointer-events: none; |
|
} |
|
.axis line { stroke: #ccc; } |
|
.y.axis .domain { display: none; } |
|
</style> |
|
<body> |
|
<div id="controls"> |
|
<input id="year-range" type="range" step="0.1" /> |
|
</div> |
|
<div id="chart"></div> |
|
<script src="//d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
var margin = { top: 15, right: 15, bottom: 40, left: 52 } |
|
var width = 960 - margin.left - margin.right |
|
var height = 450 - margin.top - margin.bottom |
|
|
|
var speed = 800 |
|
var handleWidth = 28 |
|
var radius = 5 |
|
|
|
var svg = d3.select('#chart').append('svg') |
|
.attr('width', width + margin.left + margin.right) |
|
.attr('height', height + margin.top + margin.bottom) |
|
.append('g') |
|
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') |
|
|
|
var canvas = d3.select('#chart').append('canvas') |
|
var ctx = canvas.node().getContext('2d') |
|
|
|
if (window.devicePixelRatio) { |
|
canvas |
|
.attr('width', (width + margin.left + margin.right) * window.devicePixelRatio) |
|
.attr('height', (height + margin.top + margin.bottom) * window.devicePixelRatio) |
|
.style('width', (width + margin.left + margin.right) + 'px') |
|
.style('height', (height + margin.top + margin.bottom) + 'px') |
|
|
|
ctx.scale(window.devicePixelRatio, window.devicePixelRatio) |
|
} else { |
|
canvas |
|
.attr('width', width + margin.left + margin.right) |
|
.attr('height', height + margin.top + margin.bottom) |
|
} |
|
|
|
ctx.translate(margin.left, margin.top) |
|
|
|
var yearLabel = svg.append('text') |
|
.attr('class', 'year') |
|
.attr('x', width / 2) |
|
.attr('y', height / 2) |
|
.attr('dy', '.26em') |
|
.style('font-size', width / 3) |
|
.style('text-anchor', 'middle') |
|
.style('font-weight', 'bold') |
|
.style('fill', '#ddd') |
|
|
|
var xScale = d3.scaleLinear() |
|
.domain([0, 90]) |
|
.range([0, width]) |
|
|
|
var yScale = d3.scaleLinear() |
|
.domain([0, 24000000]) |
|
.range([height, 0]) |
|
|
|
var xAxis = d3.axisBottom() |
|
.scale(xScale) |
|
.ticks(20) |
|
|
|
var yAxis = d3.axisLeft() |
|
.scale(yScale) |
|
.tickFormat(function (n) { return n / 1000000 }) |
|
.tickSize(-width) |
|
.tickPadding(6) |
|
|
|
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('Age at Census') |
|
|
|
svg.append('g') |
|
.attr('class', 'y axis') |
|
.call(yAxis) |
|
.append('text') |
|
.attr('transform', 'rotate(-90)') |
|
.attr('x', 0) |
|
.attr('y', -26) |
|
.style('text-anchor', 'end') |
|
.style('font-weight', 'bold') |
|
.text('Population (in millions)') |
|
|
|
d3.csv('data.csv', function (d) { |
|
return { |
|
year: +d.year, |
|
age: +d.age, |
|
sex: +d.sex, |
|
people: +d.people |
|
} |
|
}, initialize) |
|
|
|
function initialize(data) { |
|
var yearRange = d3.extent(data, function (d) { return d.year }) |
|
var year = yearRange[0] |
|
var availableYears = d3.set(data.map(function (d) { return d.year })).values() |
|
var yearRound = d3.scaleThreshold() |
|
.domain(d3.pairs(availableYears).map(function (years) { |
|
return (parseInt(years[0], 10) + parseInt(years[1], 10)) / 2 |
|
})) |
|
.range(availableYears) |
|
|
|
var key = 'total' |
|
|
|
var animating = false |
|
var dragging = false |
|
var timer |
|
|
|
var cohorts = d3.nest() |
|
.key(function (d) { return d.year - d.age }) |
|
.sortKeys(d3.ascending) |
|
.key(function (d) { return d.year }) |
|
.sortKeys(d3.ascending) |
|
.rollup(function (d) { |
|
var male = d.find(function (e) { return e.sex === 1 }).people |
|
var female = d.find(function (e) { return e.sex === 2 }).people |
|
return { age: d[0].age, male: male, female: female, total: male + female } |
|
}) |
|
.entries(data) |
|
|
|
var bisect = d3.bisector(function (d) { return +d.key }).right |
|
|
|
window.focus() |
|
d3.select(window).on('keydown', function () { |
|
switch (d3.event.keyCode) { |
|
case 32: toggleAnimation(); d3.event.preventDefault(); break // space |
|
} |
|
}) |
|
|
|
var slider = d3.select('#year-range') |
|
.attr('min', yearRange[0]) |
|
.attr('max', yearRange[1]) |
|
.attr('value', year) |
|
.on('mousedown', function () { |
|
animating = false |
|
dragging = true |
|
|
|
// When the slider bar is clicked, we jump to that value. The handleWidth |
|
// adjustment is necessary as the offsetWidth includes extra space to the |
|
// left and right of the slider bar for when the handle is at either end. |
|
year = yearRange[0] + |
|
(yearRange[1] - yearRange[0]) * |
|
(d3.event.offsetX - handleWidth / 2) / |
|
(this.offsetWidth - handleWidth) |
|
|
|
if (timer) { timer.stop() } |
|
timer = d3.timer(update) |
|
}) |
|
.on('mousemove', function () { |
|
if (!dragging) { return } |
|
year = parseFloat(this.value) |
|
}) |
|
.on('mouseup', function () { |
|
dragging = false |
|
year = parseFloat(this.value) |
|
snapYear() |
|
}) |
|
|
|
update() |
|
toggleAnimation() |
|
|
|
function update() { |
|
if (!dragging) { slider.node().value = year } |
|
yearLabel.text(yearRound(year)) |
|
|
|
ctx.clearRect(-margin.left, -margin.top, |
|
width + margin.left + margin.right, |
|
height + margin.top + margin.bottom) |
|
|
|
ctx.strokeStyle = '#17b' |
|
ctx.fillStyle = '#17b' |
|
|
|
var i = -1 |
|
var len = bisect(cohorts, year + 10) |
|
while (++i < len) { drawCohort(cohorts[i]) } |
|
} |
|
|
|
function drawCohort(cohort) { |
|
var i = 0 |
|
var len = bisect(cohort.values, year) |
|
|
|
var valA = cohort.values[i] |
|
var yearA = +valA.key |
|
var x = xScale(valA.value.age) |
|
var y = yScale(valA.value[key]) |
|
var rem = 1 - Math.min((year - yearA) / -10, 1) |
|
|
|
var valB, yearB |
|
|
|
while (++i < len + 1) { |
|
valB = cohort.values[i] |
|
yearB = valB ? +valB.key : yearA + 10 |
|
rem = yearB < year ? 1 : (year - yearA) / (yearB - yearA) |
|
|
|
if (!valB || yearA === year) { |
|
rem = Math.max(1 - rem, 0) |
|
break |
|
} |
|
|
|
ctx.globalAlpha = 0.5 - (len - i) / 25 |
|
ctx.beginPath() |
|
ctx.moveTo(x, y) |
|
|
|
x += (xScale(valB.value.age) - x) * rem |
|
y += (yScale(valB.value[key]) - y) * rem |
|
|
|
ctx.lineTo(x, y) |
|
ctx.stroke() |
|
|
|
valA = valB |
|
yearA = yearB |
|
rem = 1 |
|
} |
|
|
|
if (rem === 0) { return } |
|
|
|
ctx.globalAlpha = rem |
|
ctx.beginPath() |
|
ctx.arc(x, y, radius, 0, 2 * Math.PI) |
|
ctx.fill() |
|
} |
|
|
|
function toggleAnimation() { |
|
if (dragging) { return } |
|
if (animating) { |
|
animating = false |
|
snapYear() |
|
} else { |
|
animating = true |
|
animateYear(year === yearRange[1] ? yearRange[0] : year, yearRange[1], speed) |
|
} |
|
} |
|
|
|
function snapYear() { |
|
animateYear(year, +yearRound(year), speed / 2) |
|
} |
|
|
|
function animateYear(start, end, speed) { |
|
var distance = end - start |
|
var duration = Math.abs(distance) / 10 * speed |
|
|
|
if (timer) { timer.stop() } |
|
|
|
timer = d3.timer(function (elapsed) { |
|
if (elapsed >= duration) { |
|
animating = false |
|
timer.stop() |
|
year = end |
|
} else { |
|
year = start + elapsed / duration * distance |
|
} |
|
|
|
update() |
|
}) |
|
} |
|
} |
|
|
|
</script> |
|
</body> |