跳到主要内容

过渡、动画与交互

过渡(Transitions)和交互(Interactions)是让可视化从静态走向动态的关键技术。过渡提供了平滑的动画效果,让数据变化变得可视化;交互则让用户能够探索和操作数据,获得更深入的信息。D3 的过渡系统建立在选择集的基础上,使用简单直观的 API 就能创建复杂的动画效果。

过渡基础

过渡的核心思想是将 DOM 从当前状态平滑地插值到目标状态。创建过渡非常简单:选择元素后调用 transition() 方法,然后设置目标属性值即可。

创建过渡

// 基础过渡:改变背景色
d3.select('body')
.transition()
.style('background-color', 'red');

// 过渡选择集的多个属性
d3.selectAll('circle')
.transition()
.attr('r', 20)
.attr('fill', 'blue')
.style('opacity', 0.8);

过渡会自动计算当前值和目标值之间的中间状态,并在指定时间内完成动画。对于数字、颜色、带有数字的字符串(如 "10px"),D3 都能自动识别并进行插值。

过渡配置

duration 方法:设置过渡持续时间,单位是毫秒:

d3.selectAll('circle')
.transition()
.duration(1000) // 持续 1 秒
.attr('r', 50);

delay 方法:设置过渡开始的延迟时间:

// 所有元素延迟 500ms 后开始
d3.selectAll('circle')
.transition()
.delay(500)
.duration(1000)
.attr('r', 50);

// 每个元素延迟不同时间(基于索引)
d3.selectAll('circle')
.transition()
.delay((d, i) => i * 100) // 每个元素延迟递增 100ms
.duration(500)
.attr('r', 50);

ease 方法:设置缓动函数,控制动画的速度曲线:

d3.selectAll('circle')
.transition()
.ease(d3.easeLinear) // 匀速
.duration(1000)
.attr('r', 50);

// 常用缓动函数
d3.easeLinear // 匀速
d3.easeQuad // 二次缓动(默认)
d3.easeCubic // 三次缓动
d3.easeSin // 正弦缓动
d3.easeExp // 指数缓动
d3.easeCircle // 圆形缓动
d3.easeElastic // 弹性效果
d3.easeBack // 回退效果
d3.easeBounce // 弹跳效果

过渡的链式调用

过渡可以链式调用,创建连续的动画序列:

// 先变红,再变大,最后变蓝
d3.select('circle')
.transition()
.duration(500)
.attr('fill', 'red')
.transition() // 新的过渡
.duration(500)
.attr('r', 50)
.transition() // 第三个过渡
.duration(500)
.attr('fill', 'blue');

每个 transition() 调用都会创建一个新的过渡,在上一个过渡结束后开始。这种方式适合创建有明确先后顺序的动画序列。

选择性过渡

可以为过渡命名,从而在同一元素上运行多个独立的过渡:

// 创建名为 'color' 的过渡
d3.select('circle')
.transition('color')
.duration(1000)
.attr('fill', 'blue');

// 同时创建名为 'size' 的过渡
d3.select('circle')
.transition('size')
.duration(2000)
.attr('r', 50);

命名过渡允许同一元素的不同属性以不同的节奏和时机变化,非常适合复杂的动画场景。

缓动函数详解

缓动函数控制动画过程中值变化的速率。选择合适的缓动函数可以让动画更加自然和有表现力。

缓动函数类型

线性缓动(Linear):匀速变化,没有任何加速或减速:

.ease(d3.easeLinear)

二次缓动(Quad):加速或减速效果适中:

d3.easeQuadIn      // 缓入(开始慢,结束快)
d3.easeQuadOut // 缓出(开始快,结束慢)
d3.easeQuadInOut // 缓入缓出(两端慢,中间快)

三次缓动(Cubic):比二次更明显的加速或减速:

d3.easeCubicIn
d3.easeCubicOut
d3.easeCubicInOut // D3 默认缓动函数

指数缓动(Exp):强烈的加速或减速效果:

d3.easeExpIn       // 开始非常慢,结束时快速
d3.easeExpOut // 开始快速,结束时非常慢
d3.easeExpInOut

弹性缓动(Elastic):带有弹性振荡效果,常用于吸引用户注意:

// 默认振幅和周期
d3.easeElastic

// 自定义振幅(1.0 为参考值)和周期(弧度)
d3.easeElastic.amplitude(2) // 更大的振幅
d3.easeElastic.period(0.5) // 更快的振荡

弹跳缓动(Bounce):模拟弹跳效果,结束时产生多次小幅弹跳:

d3.easeBounceOut   // 常用的弹跳效果
d3.easeBounceIn // 开始时弹跳(较少使用)
d3.easeBounceInOut // 两端弹跳

回退缓动(Back):动画开始前或结束后稍微回退一点:

d3.easeBackIn      // 开始前先向后退一点
d3.easeBackOut // 结束后向前超出一点再回来
d3.easeBackInOut

// 自定义回退程度
d3.easeBack.overshoot(2) // 更大的回退幅度

缓动函数选择指南

效果推荐缓动场景
自然过渡easeCubicInOut大多数情况
元素出现easeQuadOut淡入、缩放
元素消失easeQuadIn淡出、缩小
吸引注意easeElastic强调、高亮
活泼效果easeBounce游戏化界面
优雅出场easeBackOut弹出提示框

补间动画

当 D3 的默认插值无法满足需求时,可以使用 attrTween 和 styleTween 自定义插值行为。

attrTween 方法

attrTween 允许你自定义属性值的插值过程:

// 自定义圆形半径的插值
d3.select('circle')
.transition()
.duration(1000)
.attrTween('r', function() {
// 获取当前值
const current = parseFloat(d3.select(this).attr('r')) || 0;
const target = 50;
// 创建插值器
const interpolate = d3.interpolate(current, target);
// 返回一个函数,接收时间 t(0-1),返回属性值
return function(t) {
return interpolate(t);
};
});

实际应用:饼图展开动画

// 饼图从 0 角度展开到目标角度
arcs.append('path')
.attr('d', d => {
// 初始状态:角度为 0
const interpolateStart = d3.interpolate({startAngle: 0, endAngle: 0}, d);
return arc(interpolateStart(0));
})
.transition()
.duration(1000)
.attrTween('d', function(d) {
const interpolate = d3.interpolate({startAngle: 0, endAngle: 0}, d);
return function(t) {
return arc(interpolate(t));
};
});

styleTween 方法

styleTween 用于自定义样式属性的插值:

// 自定义颜色的插值(使用 HCL 色彩空间)
d3.select('rect')
.transition()
.duration(1000)
.styleTween('fill', function() {
return d3.interpolateHcl('red', 'blue');
});

// 使用 RGB 插值
d3.select('rect')
.transition()
.styleTween('background-color', function() {
return d3.interpolateRgb('#ff0000', '#0000ff');
});

tween 方法

tween 方法用于自定义任意值的插值,不限于属性或样式:

d3.select('text')
.transition()
.duration(1000)
.tween('number', function() {
const node = d3.select(this);
const interpolate = d3.interpolate(0, 1000);
return function(t) {
node.text(Math.floor(interpolate(t)));
};
});

这个例子展示了如何创建一个数字递增动画,文本从 0 增长到 1000。

过渡事件

可以监听过渡的开始、进行中和结束事件:

d3.select('circle')
.transition()
.duration(1000)
.attr('r', 50)
.on('start', function() {
console.log('过渡开始');
})
.on('end', function() {
console.log('过渡结束');
// 可以在这里启动下一个动画
})
.on('interrupt', function() {
console.log('过渡被中断');
});

事件类型

  • start:过渡开始时触发
  • end:过渡完成时触发
  • interrupt:过渡被中断时触发

事件监听的高级用法

// 每个元素单独监听
d3.selectAll('circle')
.transition()
.duration(1000)
.attr('r', 50)
.on('end', function(d, i) {
console.log(`${i} 个元素的过渡结束`);
});

// 链式过渡中使用事件
d3.select('circle')
.transition()
.duration(500)
.attr('fill', 'red')
.on('end', function() {
d3.select(this)
.transition()
.duration(500)
.attr('r', 50);
});

中断过渡

在某些情况下,需要中断正在进行的过渡:

// 中断元素上的所有过渡
d3.select('circle').interrupt();

// 中断特定名称的过渡
d3.select('circle').interrupt('color');

// 中断选择集上所有元素的所有过渡
d3.selectAll('circle').interrupt();

中断过渡时,会触发 interrupt 事件,元素的属性会保持在被中断时的状态。

交互事件

D3 的交互系统建立在原生 DOM 事件的基础上,提供了统一的事件处理接口。

基本事件绑定

使用 on 方法绑定事件处理函数:

// 单个事件
d3.selectAll('circle')
.on('click', function(event, d) {
console.log('点击了', d);
});

// 多个事件(使用命名空间)
d3.selectAll('circle')
.on('click.main', handleClick)
.on('click.secondary', handleClick2)
.on('mouseover', handleMouseOver)
.on('mouseout', handleMouseOut);

事件处理函数的参数

function handler(event, d) {
// event: 原生 DOM 事件对象
// d: 绑定到当前元素的数据
// this: 当前 DOM 元素(需要使用普通函数而非箭头函数)

console.log('事件类型:', event.type);
console.log('鼠标位置:', event.pageX, event.pageY);
console.log('绑定数据:', d);
console.log('当前元素:', this);
}

常用事件类型

鼠标事件

d3.selectAll('.bar')
.on('click', function(event, d) {
// 单击
})
.on('dblclick', function(event, d) {
// 双击
})
.on('mouseover', function(event, d) {
// 鼠标移入
})
.on('mouseout', function(event, d) {
// 鼠标移出
})
.on('mousemove', function(event, d) {
// 鼠标移动
})
.on('mousedown', function(event, d) {
// 鼠标按下
})
.on('mouseup', function(event, d) {
// 鼠标释放
})
.on('contextmenu', function(event, d) {
// 右键菜单
event.preventDefault(); // 阻止默认右键菜单
});

键盘事件

d3.select('body')
.on('keydown', function(event) {
console.log('按下键:', event.key);
console.log('键码:', event.keyCode);
})
.on('keyup', function(event) {
console.log('释放键:', event.key);
});

触摸事件(移动端)

d3.selectAll('.bar')
.on('touchstart', function(event, d) {
// 触摸开始
})
.on('touchmove', function(event, d) {
// 触摸移动
})
.on('touchend', function(event, d) {
// 触摸结束
});

获取鼠标位置

D3 提供了 d3.pointer 和 d3.pointers 方法来获取鼠标或触摸点位置:

// 获取相对于当前元素的位置
d3.selectAll('rect')
.on('click', function(event) {
const [x, y] = d3.pointer(event);
console.log('相对于元素:', x, y);
});

// 获取相对于指定容器的位置
d3.selectAll('rect')
.on('click', function(event) {
const [x, y] = d3.pointer(event, svg.node());
console.log('相对于 SVG:', x, y);
});

// 获取所有触摸点位置(多点触控)
d3.selectAll('svg')
.on('touchstart', function(event) {
const points = d3.pointers(event, this);
points.forEach(([x, y], i) => {
console.log(`触摸点 ${i}:`, x, y);
});
});

事件传播控制

DOM 事件有三个阶段:捕获、目标、冒泡。D3 默认在冒泡阶段处理事件:

// 阻止事件冒泡
d3.selectAll('.bar')
.on('click', function(event, d) {
event.stopPropagation();
console.log('点击了 bar');
});

// 阻止默认行为
d3.selectAll('a')
.on('click', function(event) {
event.preventDefault();
// 自定义行为
});

提示框(Tooltip)

提示框是最常用的交互组件之一,用于在鼠标悬停时显示详细信息。

创建提示框

// 创建提示框元素
const tooltip = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('position', 'absolute')
.style('display', 'none')
.style('background', 'rgba(0, 0, 0, 0.8)')
.style('color', '#fff')
.style('padding', '10px')
.style('border-radius', '4px')
.style('font-size', '12px')
.style('pointer-events', 'none') // 不阻止鼠标事件
.style('z-index', '1000');

// 绑定事件
d3.selectAll('.bar')
.on('mouseover', function(event, d) {
// 显示提示框
tooltip
.style('display', 'block')
.html(`
<strong>${d.name}</strong><br/>
数值: ${d.value}<br/>
占比: ${(d.value / total * 100).toFixed(1)}%
`);

// 高亮当前元素
d3.select(this)
.attr('opacity', 0.7);
})
.on('mousemove', function(event) {
// 更新提示框位置
tooltip
.style('left', (event.pageX + 15) + 'px')
.style('top', (event.pageY - 10) + 'px');
})
.on('mouseout', function() {
// 隐藏提示框
tooltip.style('display', 'none');

// 恢复元素样式
d3.select(this)
.attr('opacity', 1);
});

提示框位置优化

function positionTooltip(event, tooltip, width, height) {
let x = event.pageX + 15;
let y = event.pageY - 10;

const tooltipWidth = tooltip.node().offsetWidth;
const tooltipHeight = tooltip.node().offsetHeight;

// 防止超出右边界
if (x + tooltipWidth > window.innerWidth) {
x = event.pageX - tooltipWidth - 15;
}

// 防止超出下边界
if (y + tooltipHeight > window.innerHeight) {
y = event.pageY - tooltipHeight - 10;
}

return {x, y};
}

d3.selectAll('.bar')
.on('mousemove', function(event) {
const pos = positionTooltip(event, tooltip);
tooltip
.style('left', pos.x + 'px')
.style('top', pos.y + 'px');
});

拖拽交互

D3 提供了 d3-drag 模块来处理拖拽交互:

// 创建拖拽行为
const drag = d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);

// 应用到元素
d3.selectAll('circle')
.call(drag);

function dragstarted(event) {
d3.select(this)
.raise() // 提升到最上层
.attr('stroke', '#333')
.attr('stroke-width', 2);
}

function dragged(event) {
d3.select(this)
.attr('cx', event.x)
.attr('cy', event.y);
}

function dragended(event) {
d3.select(this)
.attr('stroke', null);
}

拖拽结合力导向图

const simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2));

const drag = d3.drag()
.on('start', function(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
})
.on('drag', function(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
})
.on('end', function(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
});

d3.selectAll('circle')
.call(drag);

缩放与刷选交互

缩放和刷选是数据可视化中最重要的两种高级交互方式。缩放让用户能够放大查看细节或缩小查看全局;刷选则允许用户通过框选来筛选数据范围。

d3-zoom 和 d3-brush 模块提供了完整的缩放和刷选功能,支持鼠标滚轮、拖拽、触摸手势等多种输入方式。关于缩放和刷选的详细用法,包括缩放范围控制、程序化缩放、刷选事件处理、缩放与坐标轴联动等内容,请参阅专门的缩放与刷选交互章节。

下面是缩放和刷选的基本用法示例:

// 缩放基本用法
const zoom = d3.zoom()
.scaleExtent([0.5, 8]) // 缩放范围
.on('zoom', function(event) {
g.attr('transform', event.transform);
});
svg.call(zoom);

// 刷选基本用法
const brush = d3.brush()
.extent([[0, 0], [width, height]])
.on('brush', function(event) {
if (!event.selection) return;
const [[x0, y0], [x1, y1]] = event.selection;
// 处理选中的数据...
});
svg.append('g').attr('class', 'brush').call(brush);

完整交互示例

下面是一个结合多种交互技术的完整示例:

// 数据
const data = Array.from({length: 50}, (_, i) => ({
id: i,
x: Math.random() * 100,
y: Math.random() * 100,
category: ['A', 'B', 'C'][Math.floor(Math.random() * 3)]
}));

// 尺寸
const margin = {top: 30, right: 30, bottom: 50, left: 60};
const width = 700 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;

// 创建 SVG
const svg = d3.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);

const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);

// 比例尺
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, width]);

const yScale = d3.scaleLinear()
.domain([0, 100])
.range([height, 0]);

const colorScale = d3.scaleOrdinal()
.domain(['A', 'B', 'C'])
.range(['#3498db', '#e74c3c', '#2ecc71']);

// 创建提示框
const tooltip = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('position', 'absolute')
.style('display', 'none')
.style('background', 'rgba(0,0,0,0.8)')
.style('color', '#fff')
.style('padding', '8px 12px')
.style('border-radius', '4px')
.style('font-size', '12px')
.style('pointer-events', 'none');

// 绘制数据点
const circles = g.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', 8)
.attr('fill', d => colorScale(d.category))
.attr('opacity', 0.7)
.attr('stroke', '#fff')
.attr('stroke-width', 2)
.style('cursor', 'pointer');

// 添加坐标轴
g.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(xScale));

g.append('g')
.call(d3.axisLeft(yScale));

// 添加交互
circles
.on('mouseover', function(event, d) {
// 高亮
d3.select(this)
.transition()
.duration(200)
.attr('r', 12)
.attr('opacity', 1);

// 显示提示框
tooltip
.style('display', 'block')
.html(`
<strong>ID:</strong> ${d.id}<br/>
<strong>类别:</strong> ${d.category}<br/>
<strong>X:</strong> ${d.x.toFixed(1)}<br/>
<strong>Y:</strong> ${d.y.toFixed(1)}
`)
.style('left', (event.pageX + 15) + 'px')
.style('top', (event.pageY - 10) + 'px');
})
.on('mousemove', function(event) {
tooltip
.style('left', (event.pageX + 15) + 'px')
.style('top', (event.pageY - 10) + 'px');
})
.on('mouseout', function() {
d3.select(this)
.transition()
.duration(200)
.attr('r', 8)
.attr('opacity', 0.7);

tooltip.style('display', 'none');
});

// 添加缩放
const zoom = d3.zoom()
.scaleExtent([0.5, 4])
.on('zoom', function(event) {
g.attr('transform', event.transform);
});

svg.call(zoom);

// 入场动画
circles
.attr('r', 0)
.transition()
.delay((d, i) => i * 20)
.duration(500)
.ease(d3.easeBounce)
.attr('r', 8);

总结

过渡和交互是 D3 可视化的灵魂。过渡系统基于选择集,通过简单的 API 就能创建复杂的动画效果。缓动函数控制动画的速度曲线,让动画更加自然。补间动画允许自定义插值行为,处理复杂的动画场景。

交互系统建立在原生 DOM 事件基础上,提供了统一的事件处理接口。提示框、拖拽、缩放、刷选等交互功能让用户能够深入探索数据。在实际应用中,合理组合过渡和交互,可以创造出既美观又实用的可视化作品。