Files
gh-lyndonkl-claude/skills/d3-visualization/resources/transitions-interactions.md
2025-11-30 08:38:26 +08:00

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 speed
  • d3.easeCubicIn: Slow start
  • d3.easeCubicOut: Slow finish
  • d3.easeCubic: Slow start and finish
  • d3.easeBounceOut: Bouncing effect
  • d3.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 position
  • event.dx, event.dy: Change from last position
  • event.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