D3.js, 数据可视化入门笔记

D3入门

D3(Data Driven Docuemtns)是一套非常优秀的数据可视化库,它可以帮助开发者在浏览器中直观地展现各种数据。

虽然这个工具本身非常强大,但是其学习门槛并不低。其中一个原因在于教程的不友好,新手学习起来很容易没有头绪。官方给出的 tutorials 一方面更新不及时,导致和API文档对不上号,用v3的教程对照v5的API文档看的一头雾水。二来很多例子也相对复杂,缺少一些循序渐进的讲解。

这里将会分享如何实现一些简单的数据可视化图形。

Fundamentals 入门教学

首先推荐一篇优秀的 d3 基础教程

如何制作一个饼图/环形图(pie/donut)

我们可以把一个数组 [110, 12, 18] ,用环形图的方式展示。

源代码在这里查看。

像这样的饼图,可以分成三个部门:环形(Arc)、文字(Label)和连接环形文字之间的连线(Line)。

Arc

在这里每一条数据对应的环形,可以使用 SVG 里的 Path 元素来表示,通过给 Path 传递描述图形形状的参数,便可以在浏览器中渲染出我们需要的图形。为了画出环形,我们需要几个基本信息:

  • 环内径 (innerRadius)
  • 环外经 (outerRadius)
  • 起始弧度 (startAngle)
  • 终止弧度 (endAngle)

这里使用弧度来表示环的起始和终止位置,而不是角度。长度等于半径的弧长对应的弧度定义为1,所以对于一个完整的圆,弧度是 2 * PI。后面我们会用到中间弧度 (midAngle) 来判断环在左半边圆还是右半边圆,这可以帮助我们定位数据标签 (label)。

d3-shape 可以帮我们计算需要的图形参数。调用 d3.pie() 会返回一个 pie 生成器 (generator)。这个生成器可以把传入的数据转化成扇形/环形相应的弧度。而为了得到环形的形状描述(path),我们还要用到 d3.arc 生成器,并传入内外径参数,有了弧度和半径参数之后,才能生成实际的图形。

// 指定了两个相关函数:value accessor 和 sort
// value 函数表示如何从单个 data 中取取到值
// sort 函数如果没有专门指定的话,会默认降序排列
const pie = d3.pie().value((d) => d).sort(null);
const slices = pie(data);
// slices 的结构如下,注意 slices 的排序和给定的 data 排序一致,index 对应 sort 之后的顺序。

[
  {
    data: number,
    value: number,
    index: number,
    startAngle: number,
    endAngle: number,
    padAngle: number,
  },
  ...
]

// 带有内外径的 arc 生成器
const innerArc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius);

// D3 的选择 (selection) 部分
const slice = d3.select('.slices').selectAll('path').data(slices);

slice.enter()
  .append('path')
  .attr('d', (d, i) => innerArc(slices[i]))
  .attr('transform', `translate(${width / 2}, ${height / 2})`)
  .attr('fill', (d, i) => colorArray[i % (colorArray.length)]);

slice.exit().remove();

Line

Line 是连接环形图心和文字标签之间的连线,可以用 Polyline 元素渲染。为了绘制连线,我们需要知道起点、转折点和终点。

连线的起点是环的圆心,转折点是外层一个不可见的同弧度环的圆心,终点根据环的位置判断,如果环在左半边圆,那么从转折点往左平移一段距离,在右边则向右平移一段距离。

 const endPoints = []; // 记录下终点,为文字标签定位
 
      const pivotArc = d3.arc().innerRadius(outerRadius).outerRadius(pivotRadius);
      const line = d3.select('.lines').selectAll('polyline').data(slices);
      line.enter()
        .append('polyline')
        .attr('points', (d, i) => {
          const slice = slices[i];
          const innerCentroid = innerArc.centroid(slice);
          const x1 = innerCentroid[0] + width / 2;
          const y1 = innerCentroid[1] + height / 2;
          const pivotPoint = pivotArc.centroid(slice);
          const x2 = pivotPoint[0] + width / 2;
          const y2 = pivotPoint[1] + height / 2;
          const midAngle = getMidAngle(slice);
          const x3 = x2 + (midAngle > Math.PI ? -20 : 20);
          const y3 = y2;
          endPoints[i] = [x3, y3];
          return `${x1},${y1} ${x2},${y2} ${x3},${y3}`;
        })
        .attr('fill', 'none')
        .attr('stroke', (d, i) => colorArray[i % (colorArray.length)]);
      line.exit().remove();

Label

定位到终点之后,我们可以添加文字标签。

const label = svg.select('.labels').selectAll('text').data(slices);      

label.enter()
  .append('text')
  .text((d) => d.value)
  .attr('transform', (d, i) => {
    const x = endPoints[i][0] + (getMidAngle(d) > Math.PI ? -10 : 10);
     const y = endPoints[i][1] + 5;
     return `translate(${x}, ${y})`;
   })
   .attr('text-anchor', (d) => {
     const midAngle = getMidAngle(d);
     return midAngle > Math.PI ? 'end' : 'start';
   });
   
label.exit().remove();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章