{T}

交互与事件

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 防止浏览器默认手势。