交互与事件
D3 提供完整的交互系统:拖拽(Drag)、缩放(Zoom)、刷选(Brush)以及原生 DOM 事件绑定,是构建交互式数据可视化的基础。
1. 事件绑定
1.1 基础事件
javascript
import { select, pointer } from 'd3'
svg.selectAll('circle')
.on('click', function(event, d) {
// event: 原生 DOM 事件
// d: 绑定的数据
// this: 当前 DOM 元素
console.log('点击了:', d.name)
select(this).attr('stroke', 'red')
})
.on('mouseenter', function(event, d) {
select(this).attr('r', 10)
})
.on('mouseleave', function(event, d) {
select(this).attr('r', 6)
})1.2 获取鼠标坐标
javascript
import { pointer } from 'd3'
svg.on('mousemove', function(event) {
// 相对于 SVG 的坐标
const [x, y] = pointer(event)
// 相对于指定元素的坐标
const [gx, gy] = pointer(event, gNode)
// 反向映射到数据域
const dataX = xScale.invert(x)
const dataY = yScale.invert(y)
})1.3 Tooltip 实现
javascript
// 创建 tooltip 容器
const tooltip = select('body')
.append('div')
.attr('class', 'tooltip')
.style('position', 'absolute')
.style('opacity', 0)
.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')
// 绑定到数据元素
svg.selectAll('circle')
.on('mouseenter', function(event, d) {
tooltip
.style('opacity', 1)
.html(`<strong>${d.name}</strong><br/>值: ${d.value}`)
})
.on('mousemove', function(event) {
tooltip
.style('left', `${event.pageX + 12}px`)
.style('top', `${event.pageY - 28}px`)
})
.on('mouseleave', function() {
tooltip.style('opacity', 0)
})2. 拖拽 d3.drag()
2.1 基础拖拽
javascript
import { drag } from 'd3'
const dragBehavior = drag()
.on('start', function(event, d) {
select(this).raise().attr('stroke', '#000')
})
.on('drag', function(event, d) {
// event.x, event.y 是拖拽坐标
select(this)
.attr('cx', event.x)
.attr('cy', event.y)
// 同步更新数据
d.x = event.x
d.y = event.y
})
.on('end', function(event, d) {
select(this).attr('stroke', 'none')
})
svg.selectAll('circle').call(dragBehavior)2.2 约束拖拽范围
javascript
const dragBehavior = drag()
.on('drag', function(event, d) {
const x = Math.max(0, Math.min(width, event.x))
const y = Math.max(0, Math.min(height, event.y))
select(this).attr('cx', x).attr('cy', y)
})2.3 拖拽排序
javascript
const dragBehavior = drag()
.on('start', function() {
select(this).raise().classed('dragging', true)
})
.on('drag', function(event, d) {
select(this).attr('transform', `translate(0,${event.y})`)
})
.on('end', function(event, d) {
select(this).classed('dragging', false)
// 计算新位置并重新排列
const newIndex = Math.round(event.y / itemHeight)
reorder(d, newIndex)
})3. 缩放与平移 d3.zoom()
3.1 基础缩放
javascript
import { zoom, zoomIdentity } from 'd3'
const zoomBehavior = zoom()
.scaleExtent([0.5, 10]) // 缩放范围
.translateExtent([[0, 0], [width, height]]) // 平移边界
.on('zoom', function(event) {
// event.transform: { x, y, k }
g.attr('transform', event.transform)
})
svg.call(zoomBehavior)3.2 语义缩放 vs 几何缩放
javascript
// 几何缩放:整个 g 元素缩放(文字也会变大)
.on('zoom', (event) => {
g.attr('transform', event.transform)
})
// 语义缩放:只缩放坐标,元素大小不变
.on('zoom', (event) => {
const newX = event.transform.rescaleX(xScale)
const newY = event.transform.rescaleY(yScale)
// 更新坐标轴
gx.call(axisBottom(newX))
gy.call(axisLeft(newY))
// 更新元素位置(大小不变)
svg.selectAll('circle')
.attr('cx', d => newX(d.x))
.attr('cy', d => newY(d.y))
})3.3 程序化缩放
javascript
// 缩放到指定区域
svg.transition().duration(750).call(
zoomBehavior.transform,
zoomIdentity.translate(width/2, height/2).scale(3).translate(-cx, -cy)
)
// 重置缩放
svg.transition().duration(500).call(
zoomBehavior.transform,
zoomIdentity
)
// 双击缩放(自定义)
svg.on('dblclick.zoom', null) // 禁用默认双击缩放4. 刷选 d3.brush()
4.1 一维刷选(范围选择)
javascript
import { brushX } from 'd3'
const brush = brushX()
.extent([[0, 0], [width, height]])
.on('brush end', function(event) {
if (!event.selection) return
const [x0, x1] = event.selection
// 反向映射到数据域
const [t0, t1] = [xScale.invert(x0), xScale.invert(x1)]
// 过滤数据
highlightData(d => d.date >= t0 && d.date <= t1)
})
svg.append('g')
.attr('class', 'brush')
.call(brush)4.2 二维刷选
javascript
import { brush } from 'd3'
const brush2d = brush()
.extent([[0, 0], [width, height]])
.on('end', function(event) {
if (!event.selection) {
// 点击空白处清除选择
resetHighlight()
return
}
const [[x0, y0], [x1, y1]] = event.selection
svg.selectAll('circle')
.classed('selected', d =>
xScale(d.x) >= x0 && xScale(d.x) <= x1 &&
yScale(d.y) >= y0 && yScale(d.y) <= y1
)
})4.3 Focus + Context 模式
图表渲染中…
javascript
// 缩略图刷选 → 更新主图 x 域
contextBrush.on('brush end', function(event) {
if (!event.selection) return
const [x0, x1] = event.selection.map(xContext.invert)
xFocus.domain([x0, x1])
updateFocusChart()
})5. 交互组合
5.1 缩放 + 刷选共存
javascript
// 刷选在前景,缩放在背景
svg.call(zoomBehavior)
svg.append('g').attr('class', 'brush').call(brushBehavior)
// 按住 Shift 时切换为刷选模式
svg.on('keydown', (event) => {
if (event.key === 'Shift') {
svg.on('.zoom', null) // 禁用缩放
}
})
svg.on('keyup', (event) => {
if (event.key === 'Shift') {
svg.call(zoomBehavior) // 恢复缩放
}
})6. 最佳实践
使用 .raise() 提升拖拽元素
拖拽开始时调用 select(this).raise() 将元素移到最前面,避免被其他元素遮挡。
缩放时节流重绘
高频 zoom 事件中避免复杂计算。使用 requestAnimationFrame 节流:
javascript
let ticking = false
.on('zoom', (event) => {
if (!ticking) {
ticking = true
requestAnimationFrame(() => {
redraw(event.transform)
ticking = false
})
}
})移动端触摸事件
D3 的 drag/zoom 已内置触摸支持(touchstart/touchmove/touchend)。但需确保 SVG 容器有 touch-action: none CSS 防止浏览器默认手势。