Skip to content

Instantly share code, notes, and snippets.

@Selbosh
Last active February 28, 2018 11:52
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 Selbosh/aee77c3fc6aa798ad4ad9fa0b1d08bab to your computer and use it in GitHub Desktop.
Save Selbosh/aee77c3fc6aa798ad4ad9fa0b1d08bab to your computer and use it in GitHub Desktop.
Queued animations for scatter plots with error bars
license: gpl-3.0
height: 700
scrolling: no
border: no
<!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>
'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))
}
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