7.9 KiB
7.9 KiB
Transitions & Interactions
Transitions
Why Transitions Matter
The Problem: Instant changes are jarring. Users miss updates or lose track of which element changed.
D3's Solution: Smooth interpolation between old and new states over time. Movement helps users track element identity (object constancy).
When to Use: Data updates, not initial render. Transitions show change, so there must be a previous state.
Basic Pattern
// Without transition (instant)
selection.attr('r', 10);
// With transition (animated)
selection.transition().duration(500).attr('r', 10);
Key: Insert .transition() before .attr() or .style() calls you want animated.
Duration and Delay
selection
.transition()
.duration(750) // Milliseconds
.delay(100) // Wait before starting
.attr('r', 20);
// Staggered (delay per element)
selection
.transition()
.duration(500)
.delay((d, i) => i * 50) // 0ms, 50ms, 100ms...
.attr('opacity', 1);
Easing Functions
selection
.transition()
.duration(500)
.ease(d3.easeCubicOut) // Fast start, slow finish
.attr('r', 20);
Common Easings:
d3.easeLinear: Constant speedd3.easeCubicIn: Slow startd3.easeCubicOut: Slow finishd3.easeCubic: Slow start and finishd3.easeBounceOut: Bouncing effectd3.easeElasticOut: Elastic spring
Chained Transitions
selection
.transition()
.duration(500)
.attr('r', 20)
.transition() // Second transition starts after first
.duration(500)
.attr('fill', 'red');
Named Transitions
// Interrupt previous transition with same name
selection
.transition('resize')
.duration(500)
.attr('r', 20);
// Later (cancels previous 'resize' transition)
selection
.transition('resize')
.attr('r', 30);
Update Pattern with Transitions
function update(newData) {
// Update scale
yScale.domain([0, d3.max(newData, d => d.value)]);
// Update bars
svg.selectAll('rect')
.data(newData, d => d.id) // Key function
.join('rect')
.transition()
.duration(750)
.attr('y', d => yScale(d.value))
.attr('height', d => height - yScale(d.value));
// Update axis
svg.select('.y-axis')
.transition()
.duration(750)
.call(d3.axisLeft(yScale));
}
Enter/Exit Transitions
svg.selectAll('circle')
.data(data, d => d.id)
.join(
enter => enter.append('circle')
.attr('r', 0)
.call(enter => enter.transition().attr('r', 5)),
update => update
.call(update => update.transition().attr('fill', 'blue')),
exit => exit
.call(exit => exit.transition().attr('r', 0).remove())
);
Interactions
Event Handling
selection.on('click', function(event, d) {
console.log('Clicked', d);
d3.select(this).attr('fill', 'red');
});
Common Events: click, mouseover, mouseout, mousemove, dblclick
Tooltips
const tooltip = d3.select('body').append('div')
.attr('class', 'tooltip')
.style('position', 'absolute')
.style('visibility', 'hidden')
.style('background', '#fff')
.style('padding', '5px')
.style('border', '1px solid #ccc');
svg.selectAll('circle')
.data(data)
.join('circle')
.attr('r', 5)
.on('mouseover', (event, d) => {
tooltip.style('visibility', 'visible')
.html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);
})
.on('mousemove', (event) => {
tooltip.style('top', (event.pageY - 10) + 'px')
.style('left', (event.pageX + 10) + 'px');
})
.on('mouseout', () => {
tooltip.style('visibility', 'hidden');
});
Hover Effects
svg.selectAll('rect')
.data(data)
.join('rect')
.attr('fill', 'steelblue')
.on('mouseover', function() {
d3.select(this)
.transition()
.duration(200)
.attr('fill', 'orange');
})
.on('mouseout', function() {
d3.select(this)
.transition()
.duration(200)
.attr('fill', 'steelblue');
});
Drag Behavior
Basic Dragging
const drag = d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
function dragstarted(event, d) {
d3.select(this).raise().attr('stroke', 'black');
}
function dragged(event, d) {
d3.select(this)
.attr('cx', event.x)
.attr('cy', event.y);
}
function dragended(event, d) {
d3.select(this).attr('stroke', null);
}
svg.selectAll('circle').call(drag);
Event Properties:
event.x,event.y: New positionevent.dx,event.dy: Change from last positionevent.subject: Bound datum
Drag with Constraints
function dragged(event, d) {
d3.select(this)
.attr('cx', Math.max(0, Math.min(width, event.x)))
.attr('cy', Math.max(0, Math.min(height, event.y)));
}
Zoom and Pan
Basic Zoom
const zoom = d3.zoom()
.scaleExtent([0.5, 5]) // Min/max zoom
.on('zoom', zoomed);
function zoomed(event) {
g.attr('transform', event.transform);
}
svg.call(zoom);
// Container for zoomable content
const g = svg.append('g');
g.selectAll('circle').data(data).join('circle')...
Key: Apply transform to container group, not individual elements.
Programmatic Zoom
// Zoom to specific level
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity.scale(2));
// Zoom to fit
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity.translate(100, 100).scale(1.5));
Zoom with Constraints
const zoom = d3.zoom()
.scaleExtent([0.5, 5])
.translateExtent([[0, 0], [width, height]]) // Pan limits
.on('zoom', zoomed);
Brush Selection
2D Brush
const brush = d3.brush()
.extent([[0, 0], [width, height]])
.on('end', brushended);
function brushended(event) {
if (!event.selection) return; // No selection
const [[x0, y0], [x1, y1]] = event.selection;
// Filter data
const selected = data.filter(d => {
const x = xScale(d.x), y = yScale(d.y);
return x >= x0 && x <= x1 && y >= y0 && y <= y1;
});
console.log('Selected', selected);
}
svg.append('g').attr('class', 'brush').call(brush);
1D Brush
// X-axis only
const brushX = d3.brushX()
.extent([[0, 0], [width, height]])
.on('end', brushended);
// Y-axis only
const brushY = d3.brushY()
.extent([[0, 0], [width, height]])
.on('end', brushended);
Clear Brush
svg.select('.brush').call(brush.move, null); // Clear selection
Programmatic Brush
// Set selection programmatically
svg.select('.brush').call(brush.move, [[100, 100], [200, 200]]);
Common Pitfalls
Pitfall 1: Transition on Initial Render
// WRONG - transition when no previous state
svg.selectAll('circle').data(data).join('circle').transition().attr('r', 5);
// CORRECT - transition only on updates
const circles = svg.selectAll('circle').data(data).join('circle').attr('r', 5);
circles.transition().attr('r', 10); // Later, on update
Pitfall 2: Not Using Key Function
// WRONG - elements don't track data items
.data(newData).join('circle').transition().attr('cx', d => xScale(d.x));
// CORRECT - use key function
.data(newData, d => d.id).join('circle').transition().attr('cx', d => xScale(d.x));
Pitfall 3: Applying Zoom to Wrong Element
// WRONG - zoom affects individual elements
svg.selectAll('circle').call(zoom);
// CORRECT - zoom affects container
const g = svg.append('g');
svg.call(zoom);
g.selectAll('circle')... // Elements in zoomable container
Next Steps
- Apply to workflows: Workflows
- See complete examples: Common Patterns
- Combine with layouts: Advanced Layouts