---
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**:
```javascript
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):
```javascript
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):
```javascript
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**:
```javascript
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 speed
- `easeCubicInOut` - Smooth start and end
- `easeElasticOut` - Bounce/spring effect
- `easeBounceOut` - Bouncing ball
- `easeExpOut` - Exponential decay
- `easeBackOut` - 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**:
```javascript
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**:
```javascript
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 transform
- `zoom.scaleBy(k)` - Zoom by factor k
- `zoom.scaleTo(k)` - Zoom to scale k
- `zoom.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**:
```javascript
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 brush
- `brushY()` - 1D vertical brush

**References**:
- See: `docs/d3/brush/`

## Common Patterns

### Responsive Data Join
```javascript
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
```javascript
const t = svg.transition().duration(750);

circles.transition(t).attr('r', 20);
lines.transition(t).attr('stroke-width', 3);
```

### Event Delegation
```javascript
svg.on('click', function(event) {
  const element = event.target;
  if (element.tagName === 'circle') {
    console.log('Circle clicked:', d3.select(element).datum());
  }
});
```

## Search Helpers

```bash
# 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)`

- **Transition not smooth**: Wrong easing function
  - Solution: Use `easeCubicInOut` for smooth start/end

- **Drag not working**: Event propagation stopped
  - Solution: Check `.call(drag)` is applied correctly

- **Zoom jumps**: Transform state out of sync
  - Solution: Use `zoom.transform` to get/set state

- **Brush not visible**: Missing CSS or wrong extent
  - Solution: Add default brush styles or check extent bounds

## Performance Tips

1. **Use transform for positioning** - Faster than x/y attributes
2. **Batch updates** - Update multiple attributes in one call
3. **Cache selections** - Don't re-select repeatedly
4. **Use CSS for static styles** - Faster than .style()
5. **Interrupt transitions** - Cancel before starting new ones
6. **Throttle event handlers** - Debounce zoom/pan/brush
7. **Use Canvas for large datasets** - 10,000+ elements
8. **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
