图形生成器
🎯 引言
柱状图用 rect 就能画,但折线、扇形这类图形要靠 SVG 的 path 元素,它的路径字符串手写起来是灾难。学会这篇文章,你能掌握 D3 的图形生成器——把数据自动变成路径,轻松画出折线图、面积图和饼图。
🧱 为什么需要生成器
先看一眼 SVG 折线的“真面目”。path 元素靠 d 属性描述形状:
<svg width="400" height="300">
<path d="M0,180 L100,60 L200,120 L300,240" fill="none" stroke="#5470c6" />
</svg>
M0,180 表示“移动到点 (0,180)”,L100,60 表示“画一条线到点 (100,60)”……4 个点还能忍,500 个点的字符串能让人崩溃。
上面这段手写代码的真实渲染效果——就是一条 4 个点的折线:
图形生成器就是帮我们生成 d 字符串的函数:数据喂进去,路径字符串吐出来。
🧱 d3.line 折线图
d3.line() 创建折线生成器,通过 .x() 和 .y() 告诉它:每个数据点的横纵坐标怎么取:
const line = d3
.line()
.x((d) => xScale(d.day)) // 每个点的 x 坐标
.y((d) => yScale(d.value)); // 每个点的 y 坐标
line(data); // 返回 "M0,180 L100,60 L200,120..." 这样的字符串
用法是把整个数据数组绑给一个 path:
svg.append('path')
.datum(data) // 一条线是一个整体,用 datum 绑整个数组
.attr('d', line) // line 自动把数据转成路径
.attr('fill', 'none')
.attr('stroke', '#5470c6')
.attr('stroke-width', 2);
- 整条折线只有一个 path,所以用
datum(data)绑整个数组,不是data(data)——后者会按数据个数创建一堆 path。 - 一定要设置
fill: 'none',否则 path 默认填充黑色,折线会变成一坨实心色块。
这段代码的真实渲染效果:
另外,折线图 x 轴上的点是离散位置,推荐用 scalePoint:用法和 scaleBand 一样,但它直接返回每个点的坐标,不需要再算柱子宽度:
const xScale = d3
.scalePoint()
.domain(['周一', '周二', '周三', '周四', '周五'])
.range([0, width]);
xScale('周三'); // 正好落在中间位置
🧱 curve 平滑曲线
默认折线是生硬的直线段,加一句 .curve() 就变平滑:
const line = d3
.line()
.x((d) => xScale(d.day))
.y((d) => yScale(d.value))
.curve(d3.curveMonotoneX); // 平滑,但保证经过每个数据点
d3.curveMonotoneX 是最常用的平滑方式,曲线会穿过每个真实数据点,不会为了好看而“画过头”。
真实渲染效果:平滑曲线严格穿过每个红色数据点:
平滑曲线严格穿过每个红色数据点,不会“画过头”
🧱 d3.area 面积图
面积图可以想象成“折线图 + 下方填色”。它比折线多一个下边界:y0 是下边界、y1 是上边界:
const area = d3
.area()
.x((d) => xScale(d.day))
.y0(height) // 下边界:固定在底部
.y1((d) => yScale(d.value)); // 上边界:跟随数据
svg.append('path').datum(data).attr('d', area).attr('fill', 'rgba(84, 112, 198, 0.3)');
实际项目里常把两者叠着画:下面铺一层半透明面积,上面描一条折线,图表立刻有了层次感。下面就是“面积 + 平滑折线”组合的真实渲染效果:
.curve(),面积图也必须加同样的 .curve()。否则折线是平滑曲线、面积还是直线拼接,填充区域的边缘会和折线对不上,露出难看的缝隙。🧱 d3.arc + d3.pie 饼图
饼图要两个生成器配合,分工明确:pie 算角度,arc 画扇形。
第一步,pie 把数值换算成每个扇形的起止角度:
const data = [
{ name: '手机', value: 45 },
{ name: '电脑', value: 30 },
{ name: '平板', value: 25 },
];
const pie = d3.pie().value((d) => d.value);
pie(data); // 返回带角度的新数组
pie(data) 的返回值大概长这样(角度单位是弧度,一圈是 6.28):
[
{ data: { name: '手机', value: 45 }, startAngle: 0, endAngle: 2.83 },
{ data: { name: '电脑', value: 30 }, startAngle: 2.83, endAngle: 4.71 },
{ data: { name: '平板', value: 25 }, startAngle: 4.71, endAngle: 6.28 },
]
手机占 45/100,就分到一圈的 45%(约 2.83 弧度)——pie 干的就是这个按占比分角度的活。
第二步,arc 拿到角度后生成扇形路径:
const arc = d3.arc().innerRadius(0).outerRadius(150); // 内半径 0、外半径 150
const colors = ['#5470c6', '#91cc75', '#fac858'];
svg.selectAll('path')
.data(pie(data)) // 注意:绑定的是 pie 处理过的角度数据
.join('path')
.attr('d', arc) // arc 根据每个数据的角度生成扇形
.attr('fill', (d, i) => colors[i]);
innerRadius 从 0 改成 80,中间就镂空了。饼图和环形图只差这一个参数。饼图的真实渲染效果:
🧾 小节总结
- 图形生成器是“数据 → path 的 d 属性”的转换函数,复杂图形全靠它。
d3.line()画折线:datum绑整个数组,记得fill: 'none',x 轴离散点用scalePoint。.curve(d3.curveMonotoneX)让折线平滑且经过每个数据点。d3.area()画面积:y0下边界固定、y1上边界随数据。- 饼图 =
pie()按占比分角度 +arc()按角度画扇形,innerRadius > 0变环形图。
❓ 知识问答
Q1:折线图画出来是黑乎乎一坨,怎么回事?
忘了设置 fill: 'none'。path 默认会填充闭合区域,折线被填成了黑色多边形。
Q2:饼图的文字标签怎么放?
用 arc.centroid(d) 获取每个扇形的中心点坐标,在该位置添加 text 元素即可。
Q3:curve 有哪些可选值?
常用的有 curveLinear(默认直线)、curveMonotoneX(平滑)、curveStep(阶梯)。初学记住 curveMonotoneX 就够用了。
Q4:面积图的下边界可以不固定在底部吗?
可以,y0 也支持传函数,比如堆叠面积图的下边界就是下一层数据的顶部。初学阶段先掌握固定底部即可。
🧪 小练习
基于一周销量数据完成一个“折线 + 面积”组合图:
- 用
scalePoint创建 x 轴比例尺,scaleLinear创建 y 轴比例尺。 - 用
d3.area()画一个半透明的面积图。 - 用
d3.line()加curveMonotoneX在上面叠一条平滑折线。
const data = [
{ day: '周一', value: 120 },
{ day: '周二', value: 200 },
{ day: '周三', value: 150 },
{ day: '周四', value: 80 },
{ day: '周五', value: 70 },
];
onMounted(() => {
// 请在这里编写代码
});
🎉 恭喜你已经掌握三大图形生成器!下一篇我们给图表加上过渡动画,让数据“动”起来。
