D3.js —— 绘制饼图

1,575 阅读5分钟

一、前言

上一篇文章我们完整实现了一个柱状图,并且提到了一个比例尺的概念。这篇文章我们继续来学习 D3.js,但是呢是用它来绘制一个饼图。那么废话不多说,我们正式开始🏃

二、正文

2.1、API

函数用法
d3.pie利用给定的数据生成饼图,并且返回一个对象数组
d3.arc圆弧生成器

2.2.、绘制圆弧

前面我们提到了 d3.arc 是一个圆弧生成器。举个例子:

const arc = d3.arc()
    .innerRadius(0)
    .outerRadius(100)
    .startAngle(0)
    .endAngle(Math.PI / 2);

arc(); // "M0,-100A100,100,0,0,1,100,0L0,0Z"

我们可以看到最终 arc() 返回了这样串结果:"M0,-100A100,100,0,0,1,100,0L0,0Z"

🤔 那么这串结果是啥意思呢?我们把这串结果分解为如下命令:

image.png

结合起来就是:将当前位置挪到了 (0,100)(0, -100) ,基于当前位置绘制一条椭圆曲线,它的 rxrxryry 都为 100100,终点位置为 (100,0)(100, 0)。结果就是绘制了一个 1/41/4 圆。

image.png
可想而知,我们将利用这个属性来绘制我们的饼图。

2.3、绘制饼图

首先我们先来生成一个数组,数字范围随便设置

const data = Array.from({length: 10}).map((v, i) => i * 100 + 10)

接着再添加 svg & 设置容器

const svg = d3.select('#pie')
    .append('svg')
    .attr('width', 600)
    .attr('height', 600)
    .attr('class', 'svg')
		// 使得视图居中
    .attr('viewBox', '-300 -300 600 600')

然后我们需要做两件事情:

  1. 配置一个圆弧生成器 😐
  2. 根据 data 生成每个数据对应的 startAngleendAngle 😐
  3. 结合上面两步生成一个饼图👍

那么上面我们提到了用 d3.arcd3.pie 就可以完成这两件事👇

const arcs = d3.pie()(data)
const arc = d3.arc()
  .outerRadius(100)
  .innerRadius(0)

svg.selectAll('path')
    .data(arcs)
    .enter()
    .append('path')
    .attr('d', arc)

最终我们得到了一个黑漆漆的饼图 🐶

image.png

2.4、添加颜色

如上所示,我们最终的饼图是没有颜色的,那么怎么给它上色呢?有一个方法是我们自己来手动配色,但是这稍微显得有点蛋疼。另外一种方法就是使用 D3.js 中内置的方法 —— d3.schemePaired

代码如下:

svg.selectAll('path')
    .data(arcs)
    .enter()
    .append('path')
    .attr('d', arc)
    .attr('fill', (d, i) => d3.schemePaired[i])

这样我们就得到了一个颇有颜值的饼图 🐶

image.png

2.5、添加标注

2.5.1、数据结构

为了给饼图中的每一块增加标注,首先我们要把数据结构稍稍调整一下:

const datasets = [
    {name: 'cat', value: 100},
    {name: 'dog', value: 100},
    {name: 'pig', value: 100},
    {name: 'cow', value: 100},
    {name: 'bird', value: 100},
    {name: 'fish', value: 100},
    {name: 'snake', value: 100},
    {name: 'mouse', value: 100},
    {name: 'monkey', value: 100},
    {name: 'elephant', value: 100},
  ]

2.5.2、确定坐标

为了确保我们画出一条完美的折线,接下来我们需要确定几个坐标:

  1. 起始坐标
  2. 终点坐标
  3. 文本坐标

D3.js 提供了一个函数 —— arc.centroid,通过它我们可以得到一个当前扇形的中间坐标。以我们当前所画的饼图为例,我们把这些点标注为一个个白色的圆。如下图所示👇

image.png

我们观察图中的原点,结合控制台可以发现,饼图左边的点横坐标区间为 x<0x < 0,饼图右边的点横坐标区间为 x0x ≥ 0

因此,我们就可以得出一个绘制标注的逻辑:

  1. 起点坐标: arc.centroid
  2. 终点坐标:沿着圆心绘制一条线穿过当前扇形的 arc.centroid ,它的长度为 rarc.centroid距离圆心的直线距离+offsetYr - arc.centroid 距离圆心的直线距离 + offsetYoffsetYoffsetY 为自定义的偏移量)
  3. 文本坐标:以终点坐标为起点,横坐标增加 offsetXoffsetX(offsetXoffsetX 为自定义的偏移量)

当目标扇形的中点处于 xx 轴上方时,如下图所示:

image.png 我们把 A 点认为是扇形的「中点」,易知:

OD=cosAOBOCOD = cos∠AOB * OCCD=sinAOBOCCD = sin∠AOB * OC

其中: cosAOB=x/zcos∠AOB = x / zsinAOB=y/zsin∠AOB = y / zz=(x2+y2)z = √(x^2 + y^2)

当目标扇形的中点处于 xx 轴下方时,如下图所示:

image.png

此时:cosAOB=y/zcos∠AOB = y / zsinAOB=x/zsin∠AOB = x / zz=(x2+y2)z = √(x^2 + y^2)

2.5.3、代码实现

知道如何绘制之后我们就来动手写代码吧,首先是先绘制折线👇


const generatePolyline = selection => {
    const offsetY = 10;
    const distance = r + 50;
    const points = []
    const r1 = 20

    // 生成折线
    selection.append('polyline')
      .attr('class', 'polyline')
      .attr('points', (d, i) => {
        const centerX = arc.centroid(d)[0]
        const centerY = arc.centroid(d)[1]
        const centerZ = Math.sqrt(Math.pow(centerX, 2) + Math.pow(centerY, 2))
        const offsetX = getOffsetX(datasets[i].name)
        // 当前处于第一、四象限
        if (centerY <= 0) {
          const cos = Math.abs(centerX / centerZ)
          const sin = Math.abs(centerY / centerZ)
          const X = centerX >= 0 ? cos * distance : -cos * distance
          const Y = -sin * distance
          points.push({[datasets[i].name]: [X, Y]})
          if (centerX >= 0) {
            return `${centerX},${centerY} ${X},${Y}, ${X + offsetX},${Y}`
          }
          return `${centerX},${centerY} ${X},${Y}, ${X - offsetX},${Y}`
        }
        // 当前处于第二、三象限
        const cos = Math.abs(centerY / centerZ)
        const sin = Math.abs(centerX / centerZ)
        const X = centerX > 0 ? sin * distance : -sin * distance
        const Y = cos * distance
        points.push({[datasets[i].name]: [X, Y]})
        if (centerX > 0) {
          return `${centerX},${centerY} ${X},${Y}, ${X + offsetX},${Y}`
        }
        return `${centerX},${centerY} ${X},${Y}, ${X - offsetX},${Y}`
      })
      .attr('stroke', '#6F68A7')
      .attr('fill', 'none')
}

svg
	.selectAll('.path-group')
  .call(generatePolyline)

此时饼图发生了变化

image.png 紧接着我们把剩余的代码也加进来 generatePolyline 函数中👇

// 添加文字
selection.append('text')
  .attr('class', 'text')
  .attr('x', (d, i) => {
    const {name} = datasets[i]
    const [x] = points[i][name]
    if (x > 0) {
      return x + 5
    }
    return x - getWordWidth(name) - 5
  })
  .attr('y', (d, i) => {
    const {name} = datasets[i]
    const [, y] = points[i][name]
    return y - offsetY
  })
  .attr('fill', '#6F68A7')
  .text((d, i) => datasets[i].name)
  .style('font-size', '14px')

// 添加百分比
selection.append('text')
  .attr('class', 'text')
  .attr('x', (d, i) => {
    const {name} = datasets[i]
    const [x] = points[i][name]
    const offsetX = getOffsetX(name)
    if (x > 0) {
      return x + offsetX + r1 - 10
    }
    return x - offsetX - r1 - 10
  })
  .attr('y', (d, i) => {
    const {name} = datasets[i]
    const [, y] = points[i][name]
    return y + 4
  })
  .attr('fill', '#6F68A7')
  .style('font-size', '12px')
  .text((d, i) => {
    const sum = datasets.reduce((acc, cur) => acc + cur.value, 0)
    return `${datasets[i].value / sum * 100}%`
  })

// 添加圆点
selection.append('circle')
  .attr('class', 'circle')
  .attr('cx', (d, i) => {
    const {name} = datasets[i]
    const [x] = points[i][name]
    const offsetX = getOffsetX(name)
    if (x >0) {
      return x + offsetX + r1
    }
    return x - offsetX - r1
  })
  .attr('cy', (d, i) => {
    const {name} = datasets[i]
    const [, y] = points[i][name]
    return y
  })
  .attr('r', r1)
  .attr('fill', 'none')
  .attr('stroke', '#6F68A7')

刷新页面后我们就能看到一个带有完整标注的饼图了

image.png

2.6、添加动画

可以使用attrTween函数添加动画效果,控制动画速度、持续时间及属性变化,实现复杂变化,达到精确控制效果。

因为要加的地方还挺多的,这里就不一一展示了,所以我们直接来看最终效果👇

2023-02-12 161719.gif

完整代码参考:github.com/pigpigever/…

三、总结

本文介绍了如何使用 D3.js 绘制一个饼图,并详细解释了饼图的绘制原理,以及绘制过程中所需的基本步骤,从绘制底层圆环开始,再绘制扇形、添加标注,最后还添加了动画效果。

想要了解更多前端知识,欢迎关注我的公众号:tony老师的前端补习班

参考资料:developer.mozilla.org/zh-CN/docs/…