D3.js v7+ 数据可视化指南
D3.js(Data-Driven Documents)是前端数据可视化领域最强大的底层库,通过数据驱动 DOM 操作,提供灵活的绑定、变换和过渡能力。D3 v7 基于 ES Module,全面拥抱现代 JavaScript。
核心理念
图表渲染中…
| 特性 | 说明 |
|---|---|
| 数据驱动 | 数据决定 DOM 结构,数据变化驱动视觉更新 |
| 底层控制 | 不提供现成图表,提供构建图表的原子能力 |
| 模块化 | v7 拆分为 30+ 独立模块,按需引入 |
| ES Module | 原生 ESM 支持,Tree Shaking 友好 |
安装与引入
完整引入
bash
npm install d3javascript
import * as d3 from 'd3'按需引入(推荐)
javascript
import { select, scaleLinear, axisBottom, line } from 'd3'CDN 引入
html
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>选择集与数据绑定
选择元素
javascript
const svg = d3.select('#chart')
.append('svg')
.attr('width', 600)
.attr('height', 400)
d3.selectAll('.bar')
.style('fill', 'steelblue')数据绑定与 join
javascript
const data = [10, 20, 30, 40, 50]
d3.select('svg')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', (d, i) => i * 80 + 40)
.attr('cy', (d) => 200 - d * 3)
.attr('r', (d) => d * 0.5)
.attr('fill', 'steelblue')Enter / Update / Exit 模式
javascript
function update(data) {
const circles = svg.selectAll('circle').data(data)
circles
.join(
(enter) =>
enter
.append('circle')
.attr('cx', (d, i) => i * 60 + 30)
.attr('cy', 200)
.attr('r', 0)
.call((e) => e.transition().duration(500).attr('r', (d) => d)),
(update) =>
update.call((u) =>
u.transition().duration(500).attr('r', (d) => d)
),
(exit) => exit.call((e) => e.transition().duration(500).attr('r', 0).remove())
)
}比例尺(Scales)
比例尺是 D3 的核心概念,将数据域映射到视觉域。
常用比例尺
| 比例尺 | 用途 | 输入 → 输出 |
|---|---|---|
scaleLinear | 连续数值 | 连续数值 |
scaleBand | 分类数据 | 离散带宽 |
scaleOrdinal | 分类数据 | 离散颜色/值 |
scaleTime | 时间数据 | 连续像素 |
scaleSequential | 连续数值 | 颜色插值 |
javascript
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500])
const yScale = d3.scaleBand()
.domain(['A', 'B', 'C', 'D'])
.range([0, 300])
.padding(0.2)
const colorScale = d3.scaleOrdinal()
.domain(['A', 'B', 'C'])
.range(['#4e79a7', '#f28e2b', '#e15759'])坐标轴(Axes)
javascript
const xAxis = d3.axisBottom(xScale)
.ticks(5)
.tickFormat(d3.format('.0f'))
const yAxis = d3.axisLeft(yScale)
svg.append('g')
.attr('transform', 'translate(0, 300)')
.call(xAxis)
svg.append('g')
.call(yAxis)常见图表实现
柱状图
javascript
const data = [
{ name: 'A', value: 30 },
{ name: 'B', value: 80 },
{ name: 'C', value: 45 },
{ name: 'D', value: 60 },
{ name: 'E', value: 20 },
]
const width = 600
const height = 400
const margin = { top: 20, right: 30, bottom: 40, left: 40 }
const x = d3.scaleBand()
.domain(data.map((d) => d.name))
.range([margin.left, width - margin.right])
.padding(0.2)
const y = d3.scaleLinear()
.domain([0, d3.max(data, (d) => d.value)])
.nice()
.range([height - margin.bottom, margin.top])
const svg = d3.select('#chart')
.append('svg')
.attr('viewBox', [0, 0, width, height])
svg.append('g')
.attr('transform', `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
svg.append('g')
.attr('transform', `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
svg.selectAll('.bar')
.data(data)
.join('rect')
.attr('class', 'bar')
.attr('x', (d) => x(d.name))
.attr('y', (d) => y(d.value))
.attr('width', x.bandwidth())
.attr('height', (d) => y(0) - y(d.value))
.attr('fill', 'steelblue')折线图
javascript
const line = d3.line()
.x((d) => x(d.date))
.y((d) => y(d.value))
.curve(d3.curveMonotoneX)
svg.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 2)
.attr('d', line)饼图
javascript
const pie = d3.pie()
.value((d) => d.value)
.sort(null)
const arc = d3.arc()
.innerRadius(0)
.outerRadius(150)
const arcs = svg.selectAll('path')
.data(pie(data))
.join('path')
.attr('d', arc)
.attr('fill', (d) => colorScale(d.data.name))过渡动画(Transitions)
javascript
svg.selectAll('.bar')
.data(newData)
.join('rect')
.transition()
.duration(750)
.ease(d3.easeCubicInOut)
.attr('y', (d) => y(d.value))
.attr('height', (d) => y(0) - y(d.value))缓动函数
| 函数 | 效果 |
|---|---|
easeLinear | 线性 |
easeCubicInOut | 平滑起止 |
easeBounceOut | 弹跳 |
easeElasticOut | 弹性 |
easeBackInOut | 回弹 |
交互
鼠标事件
javascript
svg.selectAll('.bar')
.on('mouseover', function (event, d) {
d3.select(this).attr('fill', 'orange')
tooltip.style('opacity', 1).html(`${d.name}: ${d.value}`)
})
.on('mousemove', function (event) {
tooltip
.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`)
})
.on('mouseout', function () {
d3.select(this).attr('fill', 'steelblue')
tooltip.style('opacity', 0)
})缩放与平移
javascript
const zoom = d3.zoom()
.scaleExtent([0.5, 5])
.on('zoom', (event) => {
chartGroup.attr('transform', event.transform)
})
svg.call(zoom)拖拽
javascript
const drag = d3.drag()
.on('start', function (event) {
d3.select(this).raise().attr('stroke', 'black')
})
.on('drag', function (event) {
d3.select(this)
.attr('cx', (d) => (d.x = event.x))
.attr('cy', (d) => (d.y = event.y))
})
.on('end', function () {
d3.select(this).attr('stroke', null)
})
svg.selectAll('circle').call(drag)数据处理
D3-array 工具
javascript
d3.mean([1, 2, 3, 4, 5])
d3.median([1, 2, 3, 4, 5])
d3.deviation([1, 2, 3, 4, 5])
d3.extent([1, 2, 3, 4, 5])
d3.bin().thresholds(10)(data)
d3.group(data, (d) => d.category)
d3.rollup(data, (v) => v.length, (d) => d.category)数据加载
javascript
const csv = await d3.csv('data.csv')
const json = await d3.json('data.json')
const tsv = await d3.tsv('data.tsv')地理可视化
javascript
const projection = d3.geoMercator()
.center([104, 35])
.scale(600)
.translate([width / 2, height / 2])
const path = d3.geoPath().projection(projection)
svg.selectAll('path')
.data(geoFeatures)
.join('path')
.attr('d', path)
.attr('fill', (d) => colorScale(d.properties.value))
.attr('stroke', '#fff')与框架集成
Vue 3 + D3
Vue SFC
<template>
<div ref="chartRef"></div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import * as d3 from 'd3'
const props = defineProps({ data: Array })
const chartRef = ref(null)
function render(data) {
d3.select(chartRef.value).selectAll('*').remove()
const svg = d3.select(chartRef.value).append('svg')
// ... 绑定数据绘制
}
onMounted(() => render(props.data))
watch(() => props.data, render)
</script>React + D3
jsx
function BarChart({ data }) {
const ref = useRef()
useEffect(() => {
d3.select(ref.current).selectAll('*').remove()
const svg = d3.select(ref.current).append('svg')
// ... 绑定数据绘制
}, [data])
return <div ref={ref} />
}D3 v7 新特性
| 特性 | 说明 |
|---|---|
| ES Module | 全面 ESM,支持 Tree Shaking |
| d3.group / d3.rollup | 替代 d3.nest,更简洁的分组 API |
| selection.join | 简化 Enter/Update/Exit 模式 |
| async 数据加载 | d3.csv/json 等返回 Promise |
| d3.scaleRadial | 径向比例尺 |
| d3.bisect | 改进的二分查找 |
最佳实践
- SVG viewBox:使用
viewBox实现响应式图表 - margin convention:统一使用 margin 对象管理边距
- 数据驱动:始终通过数据绑定操作 DOM,避免手动操作
- 过渡动画:使用
transition而非即时更新,提升用户体验 - 按需引入:只引入需要的 D3 子模块,减小打包体积
- 组件化:将图表封装为可复用函数或类
- 无障碍:为 SVG 元素添加
aria-label和title子元素