Last active
February 28, 2018 11:52
-
-
Save Selbosh/aee77c3fc6aa798ad4ad9fa0b1d08bab to your computer and use it in GitHub Desktop.
Queued animations for scatter plots with error bars
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
license: gpl-3.0 | |
height: 700 | |
scrolling: no | |
border: no |
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> | |
<head> | |
<title>Animated caterpillar plot</title> | |
<meta charset="utf-8"> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script src="mwe.js" async></script> | |
<link rel="stylesheet" href="style.css"> | |
</head> | |
<body> | |
<h1>Caterpillar plot</h1> | |
<p class="dataset">Showing dataset <span class="dataset-no">0</span>.</p> | |
<p class="next" style="font-weight: bold;">Click to animate</p> | |
<p>Legend: | |
<span style="color:green;">enter</span> / | |
<span style="color:blue;">update</span> / | |
<span style="color:red;">exit</span> | |
</p> | |
</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
'use strict' | |
const margin = { | |
top: 40, | |
right: 10, | |
bottom: 10, | |
left: 10 | |
}, | |
width = 900 - margin.left - margin.right, | |
height = 600 - margin.top - margin.bottom | |
const svg = d3.select('body').append('svg') | |
.attr('width', width + margin.left + margin.right) | |
.attr('height', height + margin.top + margin.bottom) | |
const chart = svg.append('g') | |
.attr('transform', `translate(${margin.left}, ${margin.top})`) | |
const xScale = d3.scaleLinear().range([0, width]), | |
yScale = d3.scaleLinear().range([0, height]), | |
xAxisGen = d3.axisTop(xScale) | |
const xAxis = chart.append('g') | |
.attr('class', 'x axis') | |
.attr('transform', `translate(0, ${-margin.top / 2})`) | |
const dataset = [20, 10, 30, 30, 50, 100, 30, 30].map(generateData) | |
let k = 0 | |
updateScales(dataset[k]) | |
xAxis.call(xAxisGen) | |
plot(dataset[k]) | |
d3.select('.next').on('click', () => { | |
k = ++k % dataset.length | |
d3.select('.dataset-no').text(k) | |
plot(dataset[k]) | |
}) | |
function plot(data) { | |
/* Set up order and timing of transitions */ | |
const collapseErrorBars = d3.transition().duration(250), | |
exitTransition = collapseErrorBars.transition().duration(250), | |
updateTransition = exitTransition.transition().duration(1000), | |
growErrorBars = updateTransition.transition().duration(250) | |
/* Select points but don't bind data yet */ | |
let points = chart.selectAll('.point') | |
points.select('.error-bar').transition(collapseErrorBars) | |
.attr('x1', d => xScale(d.value)) | |
.attr('x2', d => xScale(d.value)) | |
/* Bind data to points */ | |
points = points.data(data, d => d.key) | |
/* Shrink exiting points */ | |
points.exit() | |
.call(selection => { | |
selection.select('circle').style('fill', 'red') | |
selection.select('.error-bar').style('stroke', 'red') | |
}) | |
.transition(exitTransition) | |
.call(selection => { | |
selection.select('circle') | |
.style('fill', 'red') | |
.attr('r', 0) | |
}) | |
.remove() | |
updateScales(data) | |
/* Move points to new positions */ | |
points | |
.call(selection => { | |
selection.select('circle').style('fill', 'blue') | |
selection.select('.error-bar').style('stroke', 'blue') | |
}) | |
.transition(updateTransition) | |
.call(selection => { | |
selection.select('circle') | |
.attr('cx', d => xScale(d.value)) | |
.attr('cy', d => yScale(d.key)) | |
}) | |
.select('.error-bar') | |
.attr('x1', d => xScale(d.value)) | |
.attr('x2', d => xScale(d.value)) | |
.attr('y1', d => yScale(d.key)) | |
.attr('y2', d => yScale(d.key)) | |
/* Animate x axis */ | |
xAxis.transition(updateTransition).call(xAxisGen) | |
/* Add new points, initially invisible */ | |
points.enter().append('g').attr('class', 'point') | |
.call(selection => { | |
selection.append('circle') | |
.style('fill', 'green') | |
.attr('r', 0) | |
.attr('cx', d => xScale(d.value)) | |
.attr('cy', d => yScale(d.key)) | |
selection.append('line').attr('class', 'error-bar') | |
.style('stroke', 'green') | |
.attr('x1', d => xScale(d.value)) | |
.attr('x2', d => xScale(d.value)) | |
.attr('y1', d => yScale(d.key)) | |
.attr('y2', d => yScale(d.key)) | |
}) | |
/* Grow points and expand error bars */ | |
.merge(points) | |
.call(selection => { | |
selection.transition(updateTransition) | |
.select('circle') | |
.attr('r', 4) // Grow fully, even if entering transition interrupted | |
selection.transition(growErrorBars) | |
.select('.error-bar') | |
.attr('x1', d => xScale(d.value - 2 * d.error)) | |
.attr('x2', d => xScale(d.value + 2 * d.error)) | |
}) | |
} | |
function generateData(n = 20) { | |
let dataset = [] | |
for (let i = 0; i < n; ++i) { | |
let obj = { | |
key: i, | |
value: 10 * Math.random(), | |
error: Math.random() | |
} | |
/* Ensure that enter() and exit() can happen in same transition */ | |
if (Math.random() < .5) | |
dataset.push(obj) | |
} | |
return dataset | |
} | |
function updateScales(data) { | |
xScale.domain([ | |
d3.min(data, d => d.value - 2 * d.error), | |
d3.max(data, d => d.value + 2 * d.error) | |
]) | |
yScale.domain(d3.extent(data, d => d.key)) | |
} |
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
svg { | |
border: 1px dotted black; | |
} | |
.point circle { | |
fill: steelblue; | |
} | |
.error-bar { | |
stroke: steelblue; | |
shape-rendering: crispEdges | |
} | |
h1 { | |
display: inline-block; | |
} | |
p { | |
margin-left: 1em; | |
display: inline-block; | |
} | |
svg { | |
display: block; | |
} | |
.next { | |
cursor: pointer; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment