Animations & Transitions
Comprehensive guide to creating smooth animations and transitions in D3.js.
Basic Transitions
Simple Transition
// Animate a circle's radius
d3.select('circle')
.transition()
.duration(1000) // 1 second
.attr('r', 50);
// Multiple properties
d3.select('circle')
.transition()
.duration(1000)
.attr('r', 50)
.attr('fill', 'red')
.style('opacity', 0.5);
Transition with Delay
// Single delay
d3.select('circle')
.transition()
.delay(500) // Wait 500ms before starting
.duration(1000)
.attr('r', 50);
// Staggered delays for multiple elements
d3.selectAll('circle')
.transition()
.delay((d, i) => i * 100) // 0ms, 100ms, 200ms, etc.
.duration(500)
.attr('r', 10);
Easing Functions
Built-in Easing
// Linear (default)
.ease(d3.easeLinear)
// Cubic easing
.ease(d3.easeCubic)
.ease(d3.easeCubicIn)
.ease(d3.easeCubicOut)
.ease(d3.easeCubicInOut)
// Bounce easing
.ease(d3.easeBounce)
.ease(d3.easeBounceIn)
.ease(d3.easeBounceOut)
.ease(d3.easeBounceInOut)
// Elastic easing
.ease(d3.easeElastic)
.ease(d3.easeElasticIn)
.ease(d3.easeElasticOut)
.ease(d3.easeElasticInOut)
// Back easing
.ease(d3.easeBack)
.ease(d3.easeBackIn)
.ease(d3.easeBackOut)
.ease(d3.easeBackInOut)
Example with Easing
d3.select('circle')
.transition()
.duration(1000)
.ease(d3.easeBounce)
.attr('cy', 200);
Chained Transitions
Sequential Transitions
d3.select('circle')
.transition() // First transition
.duration(500)
.attr('cx', 100)
.transition() // Second transition
.duration(500)
.attr('cy', 100)
.transition() // Third transition
.duration(500)
.attr('r', 25);
Named Transitions
// Start multiple named transitions
d3.select('circle').transition('move').duration(1000).attr('cx', 100);
d3.select('circle').transition('resize').duration(2000).attr('r', 50);
// Cancel a specific transition
d3.select('circle').transition('move').attr('cx', 200); // Cancels previous "move" transition
Data-Driven Animations
Enter Animations
const circles = svg
.selectAll('circle')
.data(data)
.join(
enter =>
enter
.append('circle')
.attr('r', 0) // Start with radius 0
.attr('fill', 'blue')
.call(enter => enter.transition().duration(500).attr('r', 5)), // Animate to radius 5
update =>
update.call(update =>
update.transition().duration(300).attr('fill', 'green')
),
exit =>
exit.call(exit => exit.transition().duration(300).attr('r', 0).remove()) // Remove after animation
);
Update Pattern Animation
function update(data) {
const bars = svg.selectAll('.bar').data(data);
// Enter
bars
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', (d, i) => x(i))
.attr('y', height) // Start from bottom
.attr('width', x.bandwidth())
.attr('height', 0) // Start with height 0
.attr('fill', 'steelblue')
.merge(bars) // Merge enter and update
.transition()
.duration(750)
.attr('y', d => y(d))
.attr('height', d => height - y(d));
// Exit
bars
.exit()
.transition()
.duration(750)
.attr('y', height)
.attr('height', 0)
.remove();
}
Advanced Animations
Custom Interpolation
// Color interpolation
const interpolateColor = d3.interpolateRgb('red', 'blue');
d3.select('circle')
.transition()
.duration(1000)
.tween('color', function () {
return function (t) {
d3.select(this).attr('fill', interpolateColor(t));
};
});
// Number interpolation
const interpolateNumber = d3.interpolateNumber(10, 100);
d3.select('circle')
.transition()
.duration(1000)
.tween('radius', function () {
return function (t) {
d3.select(this).attr('r', interpolateNumber(t));
};
});
Path Animations
// Animate line drawing
const path = svg
.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 2)
.attr('d', line);
const totalLength = path.node().getTotalLength();
path
.attr('stroke-dasharray', totalLength + ' ' + totalLength)
.attr('stroke-dashoffset', totalLength)
.transition()
.duration(2000)
.ease(d3.easeLinear)
.attr('stroke-dashoffset', 0);
Morphing Shapes
// Animate between two path shapes
const path1 = 'M10,10 L50,50 L100,10 Z';
const path2 = 'M10,50 L50,10 L100,50 Z';
svg
.select('path')
.transition()
.duration(1000)
.attrTween('d', function () {
return d3.interpolatePath(path1, path2);
});
Interactive Animations
Hover Animations
svg
.selectAll('circle')
.on('mouseover', function (event, d) {
d3.select(this)
.transition()
.duration(200)
.attr('r', 15)
.attr('fill', 'orange');
})
.on('mouseout', function (event, d) {
d3.select(this)
.transition()
.duration(200)
.attr('r', 5)
.attr('fill', 'steelblue');
});
Click Animations
svg.selectAll('circle').on('click', function (event, d) {
d3.select(this)
.transition()
.duration(300)
.ease(d3.easeBounce)
.attr('r', 20)
.transition()
.duration(300)
.attr('r', 5);
});
Loading Animations
Skeleton Loading
function showSkeleton() {
const skeleton = svg
.selectAll('.skeleton')
.data(d3.range(10))
.join('rect')
.attr('class', 'skeleton')
.attr('x', (d, i) => i * 50)
.attr('y', 50)
.attr('width', 40)
.attr('height', 100)
.attr('fill', '#f0f0f0')
.attr('opacity', 0.3);
// Pulsing animation
function pulse() {
skeleton
.transition()
.duration(1000)
.attr('opacity', 0.8)
.transition()
.duration(1000)
.attr('opacity', 0.3)
.on('end', pulse);
}
pulse();
}
function hideSkeleton() {
svg
.selectAll('.skeleton')
.transition()
.duration(300)
.attr('opacity', 0)
.remove();
}
Progressive Reveal
function progressiveReveal(data) {
data.forEach((d, i) => {
setTimeout(() => {
svg
.append('circle')
.attr('cx', d.x)
.attr('cy', d.y)
.attr('r', 0)
.attr('fill', 'steelblue')
.transition()
.duration(500)
.ease(d3.easeBounce)
.attr('r', 5);
}, i * 100);
});
}
Performance Optimization
Efficient Transitions
// Use CSS transforms when possible (GPU accelerated)
.style("transform", d => `translate(${x(d.x)}px, ${y(d.y)}px)`)
// Batch DOM updates
const selection = svg.selectAll("circle")
.data(data);
selection.transition()
.duration(500)
.attr("cx", d => x(d.x))
.attr("cy", d => y(d.y))
.attr("r", d => r(d.value));
Interrupt Transitions
// Interrupt all transitions on selection
d3.select('circle').interrupt().transition().duration(500).attr('r', 10);
// Interrupt specific named transition
d3.select('circle')
.interrupt('move')
.transition('resize')
.duration(500)
.attr('r', 15);
Transition Events
Transition Callbacks
d3.select('circle')
.transition()
.duration(1000)
.attr('r', 50)
.on('start', function () {
console.log('Transition started');
})
.on('end', function () {
console.log('Transition completed');
});
Coordinated Animations
let transitionsCompleted = 0;
const totalTransitions = data.length;
svg
.selectAll('circle')
.data(data)
.join('circle')
.transition()
.duration(500)
.delay((d, i) => i * 50)
.attr('r', 5)
.on('end', function () {
transitionsCompleted++;
if (transitionsCompleted === totalTransitions) {
console.log('All animations complete!');
startNextPhase();
}
});
Common Animation Patterns
Staggered Entrance
svg
.selectAll('.bar')
.data(data)
.join('rect')
.attr('class', 'bar')
.attr('x', (d, i) => x(i))
.attr('y', height)
.attr('width', x.bandwidth())
.attr('height', 0)
.transition()
.delay((d, i) => i * 50)
.duration(500)
.ease(d3.easeBackOut)
.attr('y', d => y(d))
.attr('height', d => height - y(d));
Smooth Data Updates
function smoothUpdate(newData) {
const t = d3.transition().duration(750).ease(d3.easeQuadInOut);
const circles = svg.selectAll('circle').data(newData, d => d.id);
circles.exit().transition(t).attr('r', 0).remove();
circles
.transition(t)
.attr('cx', d => x(d.x))
.attr('cy', d => y(d.y));
circles
.enter()
.append('circle')
.attr('cx', d => x(d.x))
.attr('cy', d => y(d.y))
.attr('r', 0)
.transition(t)
.attr('r', 5);
}