D3.js 交互事件 #

交互是数据可视化的灵魂,它让用户能够探索数据、发现洞察。D3.js 提供了强大的事件处理系统,支持各种交互方式。

事件基础 #

核心概念 #

text
┌─────────────────────────────────────────────────────────────┐
│                    交互事件流程                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   用户操作 ──► 事件触发 ──► 事件处理 ──► 视图更新            │
│                                                             │
│   点击/移动    event        handler     transition          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

事件监听 #

selection.on() #

javascript
d3.select('circle')
  .on('click', function(event, d) {
    console.log('Clicked:', d);
    d3.select(this).attr('fill', 'red');
  });

d3.selectAll('circle')
  .on('mouseover', function() {
    d3.select(this).attr('r', 10);
  })
  .on('mouseout', function() {
    d3.select(this).attr('r', 5);
  });

常用事件类型 #

事件类型 描述 触发时机
click 点击 鼠标单击
dblclick 双击 鼠标双击
mouseover 鼠标悬停 鼠标移入
mouseout 鼠标离开 鼠标移出
mouseenter 鼠标进入 鼠标进入(不冒泡)
mouseleave 鼠标离开 鼠标离开(不冒泡)
mousedown 鼠标按下 鼠标按键按下
mouseup 鼠标释放 鼠标按键释放
mousemove 鼠标移动 鼠标移动
wheel 滚轮 滚轮滚动
keydown 键盘按下 键盘按键按下
keyup 键盘释放 键盘按键释放
touchstart 触摸开始 触摸屏触摸
touchmove 触摸移动 触摸移动
touchend 触摸结束 触摸结束

事件对象 #

javascript
d3.select('circle')
  .on('click', function(event, d) {
    console.log(event.target);
    console.log(event.currentTarget);
    console.log(event.type);
    console.log(event.timeStamp);
    console.log(event.clientX, event.clientY);
    console.log(event.pageX, event.pageY);
    console.log(event.screenX, event.screenY);
    console.log(d);
    console.log(this);
  });

移除事件监听 #

javascript
d3.select('circle')
  .on('click', null);

d3.select('circle')
  .on('click.myNamespace', null);

命名空间 #

javascript
d3.select('circle')
  .on('click.main', function() { })
  .on('click.tooltip', function() { });

d3.select('circle')
  .on('click.main', null);

拖拽交互 d3.drag #

基本用法 #

javascript
const drag = d3.drag()
  .on('start', dragstarted)
  .on('drag', dragged)
  .on('end', dragended);

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

function dragstarted(event, d) {
  d3.select(this).raise().attr('stroke', 'black');
}

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

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

拖拽事件对象 #

javascript
function dragged(event, d) {
  console.log(event.x);
  console.log(event.y);
  console.log(event.dx);
  console.log(event.dy);
  console.log(event.subject);
  console.log(event.identifier);
  console.log(event.active);
  console.log(event.sourceEvent);
}

拖拽约束 #

javascript
const drag = d3.drag()
  .subject(function(event, d) {
    return { x: d.x, y: d.y };
  })
  .on('drag', function(event, d) {
    d.x = Math.max(0, Math.min(width, event.x));
    d.y = Math.max(0, Math.min(height, event.y));
    d3.select(this)
      .attr('cx', d.x)
      .attr('cy', d.y);
  });

拖拽容器 #

javascript
const drag = d3.drag()
  .container(function() {
    return this.parentNode;
  });

拖拽过滤器 #

javascript
const drag = d3.drag()
  .filter(function(event) {
    return event.button === 0;
  });

缩放交互 d3.zoom #

基本用法 #

javascript
const zoom = d3.zoom()
  .scaleExtent([1, 8])
  .on('zoom', zoomed);

svg.call(zoom);

function zoomed(event) {
  g.attr('transform', event.transform);
}

缩放事件 #

javascript
const zoom = d3.zoom()
  .on('start', zoomstarted)
  .on('zoom', zoomed)
  .on('end', zoomended);

function zoomstarted(event) {
  console.log('Zoom started');
}

function zoomed(event) {
  g.attr('transform', event.transform);
}

function zoomended(event) {
  console.log('Zoom ended');
}

缩放变换对象 #

javascript
function zoomed(event) {
  console.log(event.transform.x);
  console.log(event.transform.y);
  console.log(event.transform.k);
  console.log(event.transform.toString());
  
  g.attr('transform', event.transform);
  
  const point = event.transform.invert([100, 100]);
}

缩放控制 #

javascript
const zoom = d3.zoom()
  .scaleExtent([0.5, 10])
  .translateExtent([[0, 0], [width, height]]);

svg.transition().call(zoom.scaleBy, 2);

svg.transition().call(zoom.scaleTo, 2);

svg.transition().call(zoom.translateTo, 100, 100);

svg.call(zoom.transform, d3.zoomIdentity);

svg.call(zoom.transform, d3.zoomIdentity.translate(100, 50).scale(2));

禁用缩放行为 #

javascript
const zoom = d3.zoom()
  .filter(function(event) {
    return event.type !== 'wheel';
  });

const zoom = d3.zoom()
  .on('zoom', null);

刷选交互 d3.brush #

基本用法 #

javascript
const brush = d3.brush()
  .extent([[0, 0], [width, height]])
  .on('brush', brushed)
  .on('end', brushended);

svg.append('g')
  .attr('class', 'brush')
  .call(brush);

function brushed(event) {
  const selection = event.selection;
  if (selection) {
    const [[x0, y0], [x1, y1]] = selection;
    console.log(x0, y0, x1, y1);
  }
}

function brushended(event) {
  if (!event.selection) {
    console.log('Brush cleared');
  }
}

刷选方向 #

javascript
const brushX = d3.brushX()
  .extent([[0, 0], [width, height]])
  .on('end', brushed);

const brushY = d3.brushY()
  .extent([[0, 0], [width, height]])
  .on('end', brushed);

刷选控制 #

javascript
brush.move(svg.select('.brush'), [[100, 100], [200, 200]]);

brush.move(svg.select('.brush'), null);

brush.clear(svg.select('.brush'));

刷选过滤 #

javascript
function brushed(event) {
  const selection = event.selection;
  if (!selection) return;
  
  const [[x0, y0], [x1, y1]] = selection;
  
  circles.classed('selected', d => {
    const cx = xScale(d.x);
    const cy = yScale(d.y);
    return x0 <= cx && cx <= x1 && y0 <= cy && cy <= y1;
  });
}

提示框 Tooltip #

基本提示框 #

javascript
const tooltip = d3.select('body')
  .append('div')
  .attr('class', 'tooltip')
  .style('opacity', 0);

d3.selectAll('circle')
  .on('mouseover', function(event, d) {
    tooltip.transition()
      .duration(200)
      .style('opacity', 0.9);
    tooltip.html(`Value: ${d.value}`)
      .style('left', (event.pageX + 10) + 'px')
      .style('top', (event.pageY - 28) + 'px');
  })
  .on('mouseout', function() {
    tooltip.transition()
      .duration(500)
      .style('opacity', 0);
  });

CSS 样式 #

css
.tooltip {
  position: absolute;
  background-color: white;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 8px;
  font-size: 12px;
  pointer-events: none;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

鼠标跟踪 #

跟踪鼠标位置 #

javascript
svg.on('mousemove', function(event) {
  const [x, y] = d3.pointer(event);
  console.log(x, y);
  
  const [px, py] = d3.pointer(event, svg.node());
});

svg.on('mousemove', function(event) {
  const [x, y] = d3.mouse(this);
});

十字准线 #

javascript
const focus = svg.append('g')
  .style('display', 'none');

focus.append('circle')
  .attr('r', 4.5);

focus.append('line')
  .attr('class', 'x-hair')
  .attr('y1', 0)
  .attr('y2', height);

focus.append('line')
  .attr('class', 'y-hair')
  .attr('x1', 0)
  .attr('x2', width);

svg.on('mouseover', () => focus.style('display', null))
  .on('mouseout', () => focus.style('display', 'none'))
  .on('mousemove', function(event) {
    const [x, y] = d3.pointer(event);
    focus.select('circle')
      .attr('cx', x)
      .attr('cy', y);
    focus.select('.x-hair')
      .attr('x1', x)
      .attr('x2', x);
    focus.select('.y-hair')
      .attr('y1', y)
      .attr('y2', y);
  });

键盘交互 #

键盘事件 #

javascript
d3.select('body')
  .on('keydown', function(event) {
    console.log(event.key);
    console.log(event.code);
    console.log(event.keyCode);
    
    switch(event.key) {
      case 'ArrowUp':
        break;
      case 'ArrowDown':
        break;
      case 'Enter':
        break;
      case 'Escape':
        break;
    }
  });

快捷键 #

javascript
d3.select('body')
  .on('keydown', function(event) {
    if (event.ctrlKey && event.key === 's') {
      event.preventDefault();
      console.log('Save triggered');
    }
    
    if (event.shiftKey && event.key === 'A') {
      console.log('Select all');
    }
  });

触摸交互 #

触摸事件 #

javascript
d3.select('svg')
  .on('touchstart', function(event) {
    const touches = event.touches;
    console.log('Touches:', touches.length);
  })
  .on('touchmove', function(event) {
    event.preventDefault();
    const touch = event.touches[0];
    console.log(touch.clientX, touch.clientY);
  })
  .on('touchend', function(event) {
    console.log('Touch ended');
  });

交互实战示例 #

可拖拽散点图 #

javascript
const data = [
  { x: 100, y: 100, value: 10 },
  { x: 200, y: 150, value: 20 },
  { x: 150, y: 200, value: 15 }
];

const drag = d3.drag()
  .on('drag', function(event, d) {
    d.x = event.x;
    d.y = event.y;
    d3.select(this)
      .attr('cx', d.x)
      .attr('cy', d.y);
  });

svg.selectAll('circle')
  .data(data)
  .enter()
  .append('circle')
  .attr('cx', d => d.x)
  .attr('cy', d => d.y)
  .attr('r', d => d.value)
  .attr('fill', 'steelblue')
  .call(drag);

可缩放图表 #

javascript
const zoom = d3.zoom()
  .scaleExtent([0.5, 4])
  .on('zoom', function(event) {
    g.attr('transform', event.transform);
  });

svg.call(zoom);

svg.append('button')
  .text('Reset')
  .on('click', function() {
    svg.transition().call(zoom.transform, d3.zoomIdentity);
  });

可刷选散点图 #

javascript
const brush = d3.brush()
  .extent([[0, 0], [width, height]])
  .on('brush', function(event) {
    const selection = event.selection;
    if (!selection) return;
    
    const [[x0, y0], [x1, y1]] = selection;
    
    circles.classed('selected', d => {
      const cx = xScale(d.x);
      const cy = yScale(d.y);
      return x0 <= cx && cx <= x1 && y0 <= cy && cy <= y1;
    });
  });

svg.append('g')
  .attr('class', 'brush')
  .call(brush);

交互方法速查表 #

方法 描述 示例
on 添加事件监听 .on('click', handler)
drag 创建拖拽行为 d3.drag()
zoom 创建缩放行为 d3.zoom()
brush 创建刷选行为 d3.brush()
pointer 获取指针位置 d3.pointer(event)
preventDefault 阻止默认行为 event.preventDefault()
stopPropagation 阻止冒泡 event.stopPropagation()

下一步 #

现在你已经掌握了交互事件,接下来学习 布局算法,了解如何创建复杂的可视化布局!

最后更新:2026-03-28