D3 简介

图形生成器

掌握 line、area、arc 三种图形生成器,画出折线图、面积图和饼图。

🎯 引言

柱状图用 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);
折线图两个经典坑:
  1. 整条折线只有一个 path,所以用 datum(data) 绑整个数组,不是 data(data)——后者会按数据个数创建一堆 path。
  2. 一定要设置 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 也支持传函数,比如堆叠面积图的下边界就是下一层数据的顶部。初学阶段先掌握固定底部即可。


🧪 小练习

基于一周销量数据完成一个“折线 + 面积”组合图:

  1. scalePoint 创建 x 轴比例尺,scaleLinear 创建 y 轴比例尺。
  2. d3.area() 画一个半透明的面积图。
  3. d3.line()curveMonotoneX 在上面叠一条平滑折线。
const data = [
    { day: '周一', value: 120 },
    { day: '周二', value: 200 },
    { day: '周三', value: 150 },
    { day: '周四', value: 80 },
    { day: '周五', value: 70 },
];

onMounted(() => {
    // 请在这里编写代码
});

🎉 恭喜你已经掌握三大图形生成器!下一篇我们给图表加上过渡动画,让数据“动”起来。