Claude Code Plugins

Community-maintained marketplace

Feedback

d3-interaction-animation

@zacharyr0th/next-starter
1
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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 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:

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 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:

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

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)
  • 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