| name | d3-interaction-animation |
| description | Use when creating interactive visualizations with transitions, animations, drag/zoom/brush behaviors, or DOM manipulation. Invoke for data binding with .join(), animated transitions, interactive behaviors, user input handling, or selection operations. |
| allowed-tools | Read, Grep, Glob |
D3 Interaction & Animation Expert
Purpose
Expert knowledge of D3's DOM manipulation, data binding, transitions, and interactive behaviors. Covers selections, data joins, transitions, easing, drag, zoom, brush, timers, and event handling.
When to Use
Invoke this skill when:
- Binding data to DOM elements with .data() or .join()
- Creating smooth transitions and animations
- Implementing drag-and-drop interactions
- Adding pan and zoom behaviors
- Creating brush selections for filtering
- Working with time-based animations
- Handling user input events
- Debugging data binding or transition issues
- Optimizing animation performance
- Creating interactive dashboards
Documentation Available
Location: /Users/zach/Documents/cc-skills/docs/d3/
Coverage (280 files):
Selections (55 files):
- Selecting elements (select, selectAll)
- Data binding (data, join, enter, exit, update)
- Modifying elements (attr, style, text, html)
- Event handling (on, dispatch)
- Namespaces and utilities
Transitions (34 files):
- Creating transitions
- Transition timing and duration
- Chaining and delays
- Attribute interpolation
- Custom tweens
Easing (34 files):
- Easing functions (linear, quad, cubic, elastic, bounce)
- Custom easing
- Polynomial easings
Interpolation (30 files):
- Number, color, string interpolation
- Array, object interpolation
- Custom interpolators
Drag (12 files):
- Drag behavior
- Drag events (start, drag, end)
- Drag subjects and containers
Zoom (34 files):
- Zoom behavior
- Pan and zoom transforms
- Zoom constraints
- Programmatic zoom
Brush (14 files):
- 1D and 2D brush selections
- Brush events and extent
- Programmatic brushing
Timer (8 files):
- Timer API
- Animation loops
- Timeout and interval
Time (45 files):
- Time intervals (second, minute, hour, day, week, month, year)
- Time ranges and arithmetic
- Time scales
Time Format (13 files):
- Date formatting and parsing
- Locale-aware formatting
Related Skills
- d3-shapes-paths: For animating shapes and paths
- d3-core-data: For data transformations before binding
- d3-layouts-hierarchies: For interactive force graphs
- d3-geo: For interactive maps
Best Practices
- Use .join() for data binding - Handles enter/update/exit in one call
- Chain transitions - Use .transition().transition() for sequences
- Set transition keys - Use .transition(name) for independent transitions
- Prefer attr() over direct DOM - Maintains D3's internal state
- Debounce expensive operations - Throttle zoom/pan handlers
- Use delegation for events - More efficient than per-element handlers
- Cancel transitions on interaction - Call .interrupt() before new transitions
- Use transform for positioning - Faster than changing x/y attributes
- Optimize selection queries - Cache selections when possible
Examples
Example 1: Data Join with .join()
Question: "How do I bind data to DOM elements with D3?"
Response:
import * as d3 from 'd3';
// Sample data
let data = [
{ id: 1, value: 30, color: 'red' },
{ id: 2, value: 80, color: 'blue' },
{ id: 3, value: 45, color: 'green' },
];
// Create SVG
const svg = d3.create('svg')
.attr('width', 800)
.attr('height', 400);
// Initial render
function update(data) {
const circles = svg.selectAll('circle')
.data(data, d => d.id) // Key function for object constancy
.join(
// Enter: new elements
enter => enter.append('circle')
.attr('cx', (d, i) => i * 100 + 50)
.attr('cy', 200)
.attr('r', 0) // Start small
.attr('fill', d => d.color)
.call(enter => enter.transition()
.duration(500)
.attr('r', d => d.value / 2)
),
// Update: existing elements
update => update
.call(update => update.transition()
.duration(500)
.attr('r', d => d.value / 2)
.attr('fill', d => d.color)
),
// Exit: removed elements
exit => exit
.call(exit => exit.transition()
.duration(500)
.attr('r', 0)
.remove()
)
);
}
// Initial render
update(data);
// Update after 2 seconds
setTimeout(() => {
data = [
{ id: 1, value: 50, color: 'red' }, // Updated
{ id: 3, value: 60, color: 'yellow' }, // Updated
{ id: 4, value: 70, color: 'purple' }, // New
];
update(data);
}, 2000);
Simple .join() (default behavior):
svg.selectAll('circle')
.data(data)
.join('circle') // Handles enter/update/exit automatically
.attr('cx', (d, i) => i * 100 + 50)
.attr('cy', 200)
.attr('r', d => d.value / 2)
.attr('fill', d => d.color);
Old-style enter/update/exit (verbose):
const circles = svg.selectAll('circle')
.data(data);
// Enter
circles.enter().append('circle')
.attr('r', 0)
.merge(circles) // Merge with update selection
.attr('cx', (d, i) => i * 100 + 50)
.attr('cy', 200)
.transition()
.attr('r', d => d.value / 2);
// Exit
circles.exit().remove();
References:
- See:
docs/d3/selection/join.md - See:
docs/d3/selection/data.md
Example 2: Smooth Transitions with Easing
Question: "How do I create smooth animated transitions?"
Response:
import * as d3 from 'd3';
const svg = d3.create('svg')
.attr('width', 800)
.attr('height', 400);
const circle = svg.append('circle')
.attr('cx', 50)
.attr('cy', 200)
.attr('r', 20)
.attr('fill', 'steelblue');
// Basic transition
circle.transition()
.duration(2000)
.attr('cx', 750);
// Transition with easing
circle.transition()
.duration(2000)
.ease(d3.easeCubicInOut) // Smooth acceleration/deceleration
.attr('cx', 750);
// Chained transitions
circle.transition()
.duration(1000)
.ease(d3.easeElasticOut) // Bounce effect
.attr('cy', 100)
.transition() // Chain next transition
.duration(1000)
.ease(d3.easeBounceOut)
.attr('cy', 300);
// Transition with delay
circle.transition()
.delay(500)
.duration(1000)
.attr('r', 40);
// Staggered transitions
svg.selectAll('circle')
.data([1, 2, 3, 4, 5])
.join('circle')
.attr('cx', (d, i) => i * 100 + 50)
.attr('cy', 200)
.attr('r', 0)
.transition()
.delay((d, i) => i * 100) // Stagger by 100ms
.duration(500)
.attr('r', 20);
// Named transitions (independent)
circle.transition('move')
.duration(2000)
.attr('cx', 750);
circle.transition('grow') // Runs simultaneously
.duration(1000)
.attr('r', 40);
// Custom tween
circle.transition()
.duration(2000)
.attrTween('r', function() {
const i = d3.interpolate(20, 60);
return t => i(Math.sin(t * Math.PI)); // Pulse in and out
});
// Transition events
circle.transition()
.duration(1000)
.attr('cx', 750)
.on('start', function() { console.log('Started'); })
.on('interrupt', function() { console.log('Interrupted'); })
.on('end', function() { console.log('Ended'); });
// Cancel transition
circle.interrupt(); // Stop current transition
Common Easing Functions:
easeLinear- Constant speedeaseCubicInOut- Smooth start and endeaseElasticOut- Bounce/spring effecteaseBounceOut- Bouncing balleaseExpOut- Exponential decayeaseBackOut- Overshoot and settle
References:
- See:
docs/d3/transition/ - See:
docs/d3/ease/
Example 3: Drag Behavior
Question: "How do I implement drag-and-drop?"
Response:
import * as d3 from 'd3';
const svg = d3.create('svg')
.attr('width', 800)
.attr('height', 600);
// Sample data
const nodes = [
{ x: 100, y: 100, r: 30, color: 'red' },
{ x: 200, y: 150, r: 40, color: 'blue' },
{ x: 300, y: 200, r: 25, color: 'green' },
];
// Create drag behavior
const drag = d3.drag()
.on('start', function(event, d) {
d3.select(this)
.raise() // Move to front
.attr('stroke', 'black')
.attr('stroke-width', 3);
})
.on('drag', function(event, d) {
// Update data
d.x = event.x;
d.y = event.y;
// Update position
d3.select(this)
.attr('cx', d.x)
.attr('cy', d.y);
})
.on('end', function(event, d) {
d3.select(this)
.attr('stroke', null);
});
// Draw circles
const circles = svg.selectAll('circle')
.data(nodes)
.join('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.r)
.attr('fill', d => d.color)
.call(drag); // Apply drag behavior
// Constrained drag (stay in bounds)
const constrainedDrag = d3.drag()
.on('drag', function(event, d) {
const r = d.r;
d.x = Math.max(r, Math.min(800 - r, event.x));
d.y = Math.max(r, Math.min(600 - r, event.y));
d3.select(this)
.attr('cx', d.x)
.attr('cy', d.y);
});
// Drag with container constraint
const containerDrag = d3.drag()
.container(function() {
return this.parentNode; // Use parent as coordinate system
})
.on('drag', function(event, d) {
d3.select(this)
.attr('cx', event.x)
.attr('cy', event.y);
});
// Drag subject (custom hit detection)
const customDrag = d3.drag()
.subject(function(event) {
// Find closest node within 50px
const [mx, my] = d3.pointer(event);
let closest = null;
let minDist = 50;
nodes.forEach(node => {
const dx = node.x - mx;
const dy = node.y - my;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist) {
closest = node;
minDist = dist;
}
});
return closest;
})
.on('drag', function(event) {
if (event.subject) {
event.subject.x = event.x;
event.subject.y = event.y;
updateCircles();
}
});
function updateCircles() {
svg.selectAll('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
References:
- See:
docs/d3/drag/
Example 4: Zoom and Pan
Question: "How do I add zoom and pan to a visualization?"
Response:
import * as d3 from 'd3';
const width = 800;
const height = 600;
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);
// Create group for zoomable content
const g = svg.append('g');
// Sample data
const circles = Array.from({ length: 50 }, (_, i) => ({
x: Math.random() * width,
y: Math.random() * height,
r: Math.random() * 20 + 5,
}));
// Draw circles
g.selectAll('circle')
.data(circles)
.join('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.r)
.attr('fill', 'steelblue')
.attr('opacity', 0.6);
// Create zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.5, 10]) // Min and max zoom
.translateExtent([[0, 0], [width, height]]) // Pan bounds
.on('zoom', (event) => {
// Apply transform to group
g.attr('transform', event.transform);
});
// Apply zoom to SVG
svg.call(zoom);
// Programmatic zoom
function zoomToArea(x, y, width, height) {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity
.translate(400, 300)
.scale(Math.min(8, 0.9 / Math.max(width / 800, height / 600)))
.translate(-x - width / 2, -y - height / 2)
);
}
// Reset zoom
function resetZoom() {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity);
}
// Zoom to fit all circles
function zoomToFit() {
const bounds = g.node().getBBox();
const fullWidth = bounds.width;
const fullHeight = bounds.height;
const midX = bounds.x + fullWidth / 2;
const midY = bounds.y + fullHeight / 2;
const scale = 0.9 / Math.max(fullWidth / width, fullHeight / height);
const translate = [width / 2 - scale * midX, height / 2 - scale * midY];
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity
.translate(translate[0], translate[1])
.scale(scale)
);
}
// Zoom on double-click
svg.on('dblclick.zoom', null); // Disable default behavior
svg.on('dblclick', function(event) {
const [x, y] = d3.pointer(event);
svg.transition()
.duration(500)
.call(zoom.scaleBy, 2, [x, y]); // Zoom in 2x at cursor
});
// Zoom with mouse wheel only (disable pan)
const wheelZoom = d3.zoom()
.filter(event => event.type === 'wheel')
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
// Constrain zoom to semantic zoom (update data, not transform)
const semanticZoom = d3.zoom()
.on('zoom', (event) => {
const newRadius = d => d.r * event.transform.k;
g.selectAll('circle')
.attr('r', newRadius);
});
Zoom Events:
zoom.transform- Current transformzoom.scaleBy(k)- Zoom by factor kzoom.scaleTo(k)- Zoom to scale kzoom.translateBy(x, y)- Pan by [x, y]zoom.translateTo(x, y)- Pan to [x, y]
References:
- See:
docs/d3/zoom/
Example 5: Brush Selection
Question: "How do I create a brush for filtering data?"
Response:
import * as d3 from 'd3';
const width = 800;
const height = 600;
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
// Sample data
const data = Array.from({ length: 100 }, () => ({
x: Math.random() * 100,
y: Math.random() * 100,
}));
// Create scales
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([margin.left, width - margin.right]);
const yScale = d3.scaleLinear()
.domain([0, 100])
.range([height - margin.bottom, margin.top]);
// Create SVG
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);
// Draw points
const circles = svg.append('g')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', 4)
.attr('fill', 'steelblue')
.attr('opacity', 0.6);
// Create 2D brush
const brush = d3.brush()
.extent([[margin.left, margin.top], [width - margin.right, height - margin.bottom]])
.on('start brush end', function(event) {
if (!event.selection) {
// No selection - reset
circles.attr('fill', 'steelblue').attr('opacity', 0.6);
return;
}
const [[x0, y0], [x1, y1]] = event.selection;
// Highlight selected points
circles.attr('fill', d => {
const cx = xScale(d.x);
const cy = yScale(d.y);
return cx >= x0 && cx <= x1 && cy >= y0 && cy <= y1
? 'red'
: 'steelblue';
})
.attr('opacity', d => {
const cx = xScale(d.x);
const cy = yScale(d.y);
return cx >= x0 && cx <= x1 && cy >= y0 && cy <= y1
? 1
: 0.2;
});
// Get selected data
const selected = data.filter(d => {
const cx = xScale(d.x);
const cy = yScale(d.y);
return cx >= x0 && cx <= x1 && cy >= y0 && cy <= y1;
});
console.log(`Selected ${selected.length} points`);
});
// Add brush to SVG
svg.append('g')
.attr('class', 'brush')
.call(brush);
// 1D brush (for timeline or axis)
const xBrush = d3.brushX()
.extent([[margin.left, 0], [width - margin.right, 50]])
.on('brush', (event) => {
if (event.selection) {
const [x0, x1] = event.selection.map(xScale.invert);
console.log('Selected range:', x0, x1);
}
});
// Programmatic brush
function selectArea(x0, y0, x1, y1) {
svg.select('.brush')
.call(brush.move, [[x0, y0], [x1, y1]]);
}
// Clear brush
function clearBrush() {
svg.select('.brush')
.call(brush.move, null);
}
// Brush with snap-to-grid
const gridBrush = d3.brush()
.on('end', function(event) {
if (!event.selection) return;
const [[x0, y0], [x1, y1]] = event.selection;
const snapped = [
[Math.round(x0 / 10) * 10, Math.round(y0 / 10) * 10],
[Math.round(x1 / 10) * 10, Math.round(y1 / 10) * 10],
];
d3.select(this).call(brush.move, snapped);
});
Brush Types:
brush()- 2D brush (x and y)brushX()- 1D horizontal brushbrushY()- 1D vertical brush
References:
- See:
docs/d3/brush/
Common Patterns
Responsive Data Join
function render(data) {
const bars = svg.selectAll('rect')
.data(data, d => d.id)
.join('rect')
.attr('x', (d, i) => i * 50)
.attr('y', d => height - d.value)
.attr('width', 40)
.attr('height', d => d.value)
.attr('fill', 'steelblue');
}
Coordinated Transitions
const t = svg.transition().duration(750);
circles.transition(t).attr('r', 20);
lines.transition(t).attr('stroke-width', 3);
Event Delegation
svg.on('click', function(event) {
const element = event.target;
if (element.tagName === 'circle') {
console.log('Circle clicked:', d3.select(element).datum());
}
});
Search Helpers
# Find selection docs
grep -r "select\|selectAll\|join\|data" /Users/zach/Documents/cc-skills/docs/d3/selection/
# Find transition docs
grep -r "transition\|duration\|ease" /Users/zach/Documents/cc-skills/docs/d3/transition/
# Find interaction docs
grep -r "drag\|zoom\|brush" /Users/zach/Documents/cc-skills/docs/d3/drag/ /Users/zach/Documents/cc-skills/docs/d3/zoom/ /Users/zach/Documents/cc-skills/docs/d3/brush/
# Find timer docs
grep -r "timer\|timeout\|interval" /Users/zach/Documents/cc-skills/docs/d3/timer/
# List interaction modules
ls /Users/zach/Documents/cc-skills/docs/d3/selection/
ls /Users/zach/Documents/cc-skills/docs/d3/transition/
Common Errors
Data not updating: Forgot to use key function
- Solution:
.data(data, d => d.id)
- Solution:
Transition not smooth: Wrong easing function
- Solution: Use
easeCubicInOutfor smooth start/end
- Solution: Use
Drag not working: Event propagation stopped
- Solution: Check
.call(drag)is applied correctly
- Solution: Check
Zoom jumps: Transform state out of sync
- Solution: Use
zoom.transformto get/set state
- Solution: Use
Brush not visible: Missing CSS or wrong extent
- Solution: Add default brush styles or check extent bounds
Performance Tips
- Use transform for positioning - Faster than x/y attributes
- Batch updates - Update multiple attributes in one call
- Cache selections - Don't re-select repeatedly
- Use CSS for static styles - Faster than .style()
- Interrupt transitions - Cancel before starting new ones
- Throttle event handlers - Debounce zoom/pan/brush
- Use Canvas for large datasets - 10,000+ elements
- Avoid layout thrashing - Read then write DOM properties
Notes
- Documentation covers D3 v7 (latest version)
- .join() is recommended over enter/update/exit pattern
- Transitions are asynchronous and can be chained
- Drag/zoom/brush behaviors are reusable functions
- Key functions enable object constancy in transitions
- Named transitions run independently
- Transform is CSS transform string (translate, scale, rotate)
- File paths reference local documentation cache
- For latest updates, check https://d3js.org/d3-selection