{T}

选择集与数据绑定

D3 的核心机制是选择集(Selection)数据绑定(Data Join)。理解 Enter/Update/Exit 三阶段模型是掌握 D3 的关键。

1. 选择集基础

1.1 选择元素

javascript
import { select, selectAll } from 'd3'

// 选择单个元素(返回第一个匹配)
const svg = select('#chart')
const body = select('body')

// 选择多个元素(返回所有匹配)
const circles = selectAll('circle')
const items = selectAll('.bar-item')

// 通过节点选择
const node = document.getElementById('chart')
const sel = select(node)

1.2 链式操作

javascript
select('#chart')
  .append('svg')
  .attr('width', 600)
  .attr('height', 400)
  .style('background', '#f8f9fa')
  .append('g')
  .attr('transform', 'translate(50, 20)')

1.3 常用方法速查

方法用途示例
.attr(name, value)设置 HTML/SVG 属性.attr('cx', 100)
.style(name, value)设置 CSS 样式.style('fill', 'steelblue')
.text(value)设置文本内容.text('Hello')
.html(value)设置 innerHTML.html('<b>Bold</b>')
.append(tag)追加子元素.append('circle')
.insert(tag, before)在指定位置前插入.insert('rect', '.label')
.remove()删除元素.exit().remove()
.classed(name, bool)切换 CSS 类.classed('active', true)
.property(name, value)设置 DOM 属性.property('checked', true)
.datum(value)绑定单个数据.datum({name: 'A'})
.data(array)绑定数据数组.data([10, 20, 30])

2. 数据绑定

2.1 data() 方法

javascript
const dataset = [
  { name: 'Alice', score: 85 },
  { name: 'Bob', score: 92 },
  { name: 'Carol', score: 78 }
]

// 将数据绑定到选择集
const bars = selectAll('.bar')
  .data(dataset, d => d.name)  // 第二个参数:key 函数(推荐)

// 不带 key 函数(按索引绑定,不推荐用于动态数据)
const bars = selectAll('.bar').data(dataset)
Key 函数的重要性

不带 key 函数时,D3 按索引匹配数据与元素。当数据顺序变化或有增删时,会导致错误的元素复用。始终建议使用 key 函数。

2.2 Enter / Update / Exit 三阶段

图表渲染中…
阶段含义典型操作
Enter有数据但无对应元素创建新元素
Update数据与元素已匹配更新属性/样式
Exit有元素但无对应数据移除多余元素

2.3 经典模式(D3 v5 写法)

javascript
const circles = svg.selectAll('circle')
  .data(dataset, d => d.id)

// Enter:创建新元素
circles.enter()
  .append('circle')
  .attr('r', 0)
  .attr('cx', d => xScale(d.x))
  .attr('cy', d => yScale(d.y))
  .merge(circles)  // 合并 Enter + Update
  .transition()
  .attr('r', d => rScale(d.value))

// Exit:移除多余元素
circles.exit()
  .transition()
  .attr('r', 0)
  .remove()

2.4 现代模式:selection.join()(D3 v6+,推荐)

javascript
svg.selectAll('circle')
  .data(dataset, d => d.id)
  .join(
    // Enter
    enter => enter.append('circle')
      .attr('cx', d => xScale(d.x))
      .attr('cy', d => yScale(d.y))
      .attr('r', 0)
      .call(enter => enter.transition()
        .attr('r', d => rScale(d.value))
      ),
    // Update
    update => update
      .call(update => update.transition()
        .attr('cx', d => xScale(d.x))
        .attr('cy', d => yScale(d.y))
        .attr('r', d => rScale(d.value))
      ),
    // Exit
    exit => exit
      .call(exit => exit.transition()
        .attr('r', 0)
        .remove()
      )
  )

简化写法(Enter/Update 操作相同时):

javascript
svg.selectAll('circle')
  .data(dataset, d => d.id)
  .join('circle')
  .attr('cx', d => xScale(d.x))
  .attr('cy', d => yScale(d.y))
  .attr('r', d => rScale(d.value))
  .attr('fill', 'steelblue')

3. 数据绑定进阶

3.1 嵌套数据绑定

javascript
// 分组数据
const groups = [
  { category: 'A', values: [10, 20, 30] },
  { category: 'B', values: [15, 25, 35] }
]

const groupSel = svg.selectAll('.group')
  .data(groups)
  .join('g')
  .attr('class', 'group')

// 子元素绑定父数据的 values
groupSel.selectAll('circle')
  .data(d => d.values)  // 子数据来自父数据
  .join('circle')
  .attr('cx', (d, i) => i * 30)
  .attr('r', d => d / 2)

3.2 访问父级数据

javascript
groupSel.selectAll('circle')
  .data(d => d.values.map(v => ({ value: v, category: d.category })))
  .join('circle')
  .attr('fill', d => colorScale(d.category))
  .attr('r', d => d.value / 2)

3.3 datum() vs data()

方法绑定方式适用场景
.data(array)数组元素一一绑定到多个 DOM多个同类元素(柱状图的柱子)
.datum(value)整个值绑定到单个 DOM单个元素需要复杂数据(路径的 line data)
javascript
// datum:整条线绑定一个数据数组
svg.append('path')
  .datum(lineData)
  .attr('d', lineGenerator)
  .attr('fill', 'none')
  .attr('stroke', 'steelblue')

4. 选择集遍历与过滤

javascript
// 过滤
selectAll('circle')
  .filter(d => d.value > 50)
  .style('fill', 'red')

// 排序(重新排列 DOM 顺序)
selectAll('.bar')
  .sort((a, b) => b.value - a.value)

// 遍历
selectAll('rect').each(function(d, i) {
  // this 指向当前 DOM 元素
  console.log(i, d, this)
})

// 获取/设置单个元素
const firstNode = select('circle').node()
const allNodes = selectAll('circle').nodes()

5. 最佳实践

始终使用 key 函数
javascript
// ✅ 推荐
.data(items, d => d.id)

// ❌ 避免(动态数据时会导致错误复用)
.data(items)
优先使用 join() 而非 enter/exit

join() 是 D3 v6+ 的推荐 API,代码更简洁、意图更清晰。

避免在回调中使用箭头函数访问 this

D3 的 .attr('x', function(d) { ... })this 指向当前 DOM。箭头函数会丢失 this 绑定。需要 this 时使用 function 关键字。

6. 完整示例:动态柱状图

javascript
import { select, scaleBand, scaleLinear, axisBottom, axisLeft } from 'd3'

function renderBarChart(container, data) {
  const margin = { top: 20, right: 20, bottom: 40, left: 50 }
  const width = 600 - margin.left - margin.right
  const height = 400 - margin.top - margin.bottom

  const svg = select(container)
    .append('svg')
    .attr('width', width + margin.left + margin.right)
    .attr('height', height + margin.top + margin.bottom)
    .append('g')
    .attr('transform', `translate(${margin.left},${margin.top})`)

  const x = scaleBand()
    .domain(data.map(d => d.name))
    .range([0, width])
    .padding(0.2)

  const y = scaleLinear()
    .domain([0, Math.max(...data.map(d => d.value))])
    .nice()
    .range([height, 0])

  // 坐标轴
  svg.append('g')
    .attr('transform', `translate(0,${height})`)
    .call(axisBottom(x))

  svg.append('g')
    .call(axisLeft(y))

  // 柱子(使用 join)
  svg.selectAll('.bar')
    .data(data, d => d.name)
    .join('rect')
    .attr('class', 'bar')
    .attr('x', d => x(d.name))
    .attr('y', d => y(d.value))
    .attr('width', x.bandwidth())
    .attr('height', d => height - y(d.value))
    .attr('fill', 'steelblue')

  return svg
}

// 使用
const data = [
  { name: 'React', value: 85 },
  { name: 'Vue', value: 72 },
  { name: 'Angular', value: 45 },
  { name: 'Svelte', value: 30 }
]
renderBarChart('#chart', data)