形状生成器
D3 的形状生成器(Shape Generators)将数据转换为 SVG 路径字符串(d 属性),是绘制折线、面积、弧线、饼图等复杂图形的核心工具。
1. 生成器体系
图表渲染中…
2. 折线生成器 d3.line()
2.1 基础用法
javascript
import { line, scaleLinear, scaleTime } from 'd3'
const data = [
{ date: new Date(2024, 0, 1), value: 30 },
{ date: new Date(2024, 1, 1), value: 45 },
{ date: new Date(2024, 2, 1), value: 38 },
{ date: new Date(2024, 3, 1), value: 62 }
]
const xScale = scaleTime()
.domain([data[0].date, data[data.length - 1].date])
.range([0, 600])
const yScale = scaleLinear()
.domain([0, 70])
.range([300, 0])
// 创建折线生成器
const lineGenerator = line()
.x(d => xScale(d.date)) // x 访问器
.y(d => yScale(d.value)) // y 访问器
// 生成路径字符串
const pathD = lineGenerator(data)
// → "M0,171.4L200,107.1L400,137.1L600,34.3"
// 渲染
svg.append('path')
.datum(data)
.attr('d', lineGenerator)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 2)2.2 曲线插值
javascript
import { line, curveBasis, curveCardinal, curveMonotoneX, curveStep } from 'd3'
const lineGen = line()
.x(d => x(d.date))
.y(d => y(d.value))
.curve(curveMonotoneX) // 单调插值(推荐,不会过冲)| 曲线类型 | 特点 | 适用场景 |
|---|---|---|
curveLinear | 直线连接(默认) | 精确数据展示 |
curveMonotoneX | 单调平滑,不过冲 | 时间序列(推荐) |
curveBasis | B 样条,极度平滑 | 趋势展示 |
curveCardinal | 可调张力平滑 | 通用平滑 |
curveStep | 阶梯状 | 离散状态变化 |
curveNatural | 自然样条 | 数学可视化 |
2.3 处理缺失数据
javascript
const lineGen = line()
.x(d => x(d.date))
.y(d => y(d.value))
.defined(d => d.value !== null) // null 值处断开线条
// 数据中有 null 时,线条会在该处断开
const data = [
{ date: d1, value: 30 },
{ date: d2, value: null }, // 断开
{ date: d3, value: 45 }
]3. 面积生成器 d3.area()
javascript
import { area, curveMonotoneX } from 'd3'
const areaGenerator = area()
.x(d => xScale(d.date))
.y0(height) // 基线(底部)
.y1(d => yScale(d.value)) // 顶线
.curve(curveMonotoneX)
svg.append('path')
.datum(data)
.attr('d', areaGenerator)
.attr('fill', 'steelblue')
.attr('fill-opacity', 0.3)
.attr('stroke', 'steelblue')3.1 堆叠面积图
javascript
import { stack, stackOrderNone, stackOffsetNone, area } from 'd3'
const stackGenerator = stack()
.keys(['desktop', 'mobile', 'tablet'])
.order(stackOrderNone)
.offset(stackOffsetNone)
const stackedData = stackGenerator(data)
// → [[{0,1,data}, ...], [{0,1,data}, ...], ...]
const areaGen = area()
.x(d => xScale(d.data.date))
.y0(d => yScale(d[0])) // 下边界
.y1(d => yScale(d[1])) // 上边界
svg.selectAll('.layer')
.data(stackedData)
.join('path')
.attr('class', 'layer')
.attr('d', areaGen)
.attr('fill', (d, i) => colorScale(i))4. 弧与饼图
4.1 d3.arc() 弧生成器
javascript
import { arc } from 'd3'
// 环形图的一段弧
const arcGenerator = arc()
.innerRadius(60) // 内半径(0 = 实心扇形)
.outerRadius(100) // 外半径
.startAngle(0) // 起始角度(弧度)
.endAngle(Math.PI / 2) // 结束角度
const pathD = arcGenerator()
// → "M0,-100A100,100,0,0,1,100,0L60,0A60,60,0,0,0,0,-60Z"4.2 d3.pie() 饼图布局
javascript
import { pie, arc } from 'd3'
const data = [
{ name: 'React', value: 40 },
{ name: 'Vue', value: 30 },
{ name: 'Angular', value: 20 },
{ name: 'Others', value: 10 }
]
// pie() 计算每个扇区的角度
const pieGenerator = pie()
.value(d => d.value)
.sort(null) // 不排序,保持原始顺序
.padAngle(0.02) // 扇区间隙
const arcs = pieGenerator(data)
// → [{ startAngle, endAngle, value, data }, ...]
// arc() 将角度转为路径
const arcGenerator = arc()
.innerRadius(0)
.outerRadius(120)
// 渲染
const g = svg.append('g')
.attr('transform', 'translate(200, 200)')
g.selectAll('path')
.data(arcs)
.join('path')
.attr('d', arcGenerator)
.attr('fill', (d, i) => colorScale(i))
.attr('stroke', '#fff')
.attr('stroke-width', 2)
// 标签
const labelArc = arc().innerRadius(80).outerRadius(80)
g.selectAll('text')
.data(arcs)
.join('text')
.attr('transform', d => `translate(${labelArc.centroid(d)})`)
.attr('text-anchor', 'middle')
.text(d => d.data.name)4.3 环形图(Donut)
javascript
const arcGenerator = arc()
.innerRadius(70) // 内半径 > 0 = 环形
.outerRadius(120)
.cornerRadius(4) // 圆角弧
// 悬停放大效果
const arcHover = arc()
.innerRadius(70)
.outerRadius(130) // 悬停时外半径增大
.cornerRadius(4)5. 符号生成器 d3.symbol()
javascript
import { symbol, symbolCircle, symbolTriangle, symbolStar, symbolCross } from 'd3'
const symbolGenerator = symbol()
.type(symbolTriangle)
.size(64) // 面积(平方像素)
svg.selectAll('path')
.data(data)
.join('path')
.attr('d', symbolGenerator)
.attr('transform', d => `translate(${x(d.x)},${y(d.y)})`)
.attr('fill', d => colorScale(d.category))| 符号 | 常量 | 形状 |
|---|---|---|
| 圆形 | symbolCircle | ● |
| 三角形 | symbolTriangle | ▲ |
| 星形 | symbolStar | ★ |
| 十字 | symbolCross | ✚ |
| 菱形 | symbolDiamond | ◆ |
| 方形 | symbolSquare | ■ |
6. 最佳实践
生成器是纯函数
D3 的形状生成器不操作 DOM,只返回路径字符串。你负责将字符串设置到 <path d="..."> 上。这种分离使生成器可测试、可复用。
时间序列优先使用 curveMonotoneX
curveBasis 虽然平滑但会"吃掉"极值点,curveMonotoneX 保证曲线经过每个数据点且不过冲。
饼图排序
pie().sort(null) 保持数据原始顺序。默认排序会按值从大到小排列,可能不符合业务预期。