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

416 lines
7.9 KiB
Markdown

# 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
```javascript
// 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
```javascript
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
```javascript
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
```javascript
selection
.transition()
.duration(500)
.attr('r', 20)
.transition() // Second transition starts after first
.duration(500)
.attr('fill', 'red');
```
---
### Named Transitions
```javascript
// 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
```javascript
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
```javascript
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
```javascript
selection.on('click', function(event, d) {
console.log('Clicked', d);
d3.select(this).attr('fill', 'red');
});
```
**Common Events**: `click`, `mouseover`, `mouseout`, `mousemove`, `dblclick`
---
### Tooltips
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
// 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
```javascript
const zoom = d3.zoom()
.scaleExtent([0.5, 5])
.translateExtent([[0, 0], [width, height]]) // Pan limits
.on('zoom', zoomed);
```
---
## Brush Selection
### 2D Brush
```javascript
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
```javascript
// 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
```javascript
svg.select('.brush').call(brush.move, null); // Clear selection
```
---
### Programmatic Brush
```javascript
// Set selection programmatically
svg.select('.brush').call(brush.move, [[100, 100], [200, 200]]);
```
---
## Common Pitfalls
### Pitfall 1: Transition on Initial Render
```javascript
// 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
```javascript
// 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
```javascript
// 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](workflows.md)
- See complete examples: [Common Patterns](common-patterns.md)
- Combine with layouts: [Advanced Layouts](advanced-layouts.md)