Skip to main content

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);
}