选择集与数据绑定
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)