Bootstrap

【D3.js in Action 3 精译_040】4.4 D3 弧形图的绘制方法

当前内容所在位置:

  • 第四章 直线、曲线与弧线的绘制 ✔️
    • 4.1 坐标轴的创建(上篇)
      • 4.1.1 D3 中的边距约定(中篇)
      • 4.1.2 坐标轴的生成(中篇)
        • 4.1.2.1 比例尺的声明(中篇)
        • 4.1.2.2 坐标轴的添加(下篇)
        • 4.1.2.3 轴标签的添加(下篇)
    • 4.2 D3 折线图的绘制
      • 4.2.1 直线生成工具的使用
      • 4.2.2 对数据点作曲线插值处理
    • 4.3 D3 面积图的绘制
      • 4.3.1 面积图生成工具的用法
      • 4.3.2 用标签提高图表的可读性
    • 4.4 D3 弧形图的绘制 ✔️
      • 4.4.1 D3 中的极坐标系 ✔️
      • 4.4.2 圆弧生成器的使用 ✔️
      • 4.4.3 圆弧形心的计算 ✔️
      • 4.4.4 人物专访:Francis Gagnon、Patricia Angkiriwang 和 Olivia Gélinas
    • 4.5 本章小结

《D3.js in Action》全新第三版封面

《D3.js in Action》全新第三版封面

译者按
终于来到了第四章的最后一节——弧形图的绘制。有了前面几节的知识储备,看似云遮雾绕的 D3 弧形图绘制也可以立马手到擒来。这也是 D3 可视化的另一个强大之处——掌握相关基础知识后可以快速触类旁通其他图表类型!

4.4 D3 弧形图的绘制 Drawing arcs

在本章最后一节,一起再来看看 D3 弧形图的绘制方法。弧形是数据可视化中的常见形状,经常用于绘制饼图、旭日图(sunburst diagrams)和南丁格尔玫瑰图(Nightingale rose charts)等,用于呈现目标数量与总量之间的关系。此外还可以在自定义的径向可视化作品(custom radial visualizations)中见到它们的身影。

与折线图和面积图一样,弧形图也是通过 SVG 路径元素绘制的。可能您已经猜到了,D3 内置了一个便捷的弧形生成工具专门用于计算弧形路径的 d 属性。在详细介绍弧形生成器之前,先要做好示例项目的相应准备工作。本节要绘制的弧形图,即本章最开始介绍的图 4.1 中右边的“降水天数”环形图,也可以在该图形的线上版本进行查看(详见 http://mng.bz/5orB)。其中蓝色弧线表示 2021 年纽约市全年降水天数的百分比(35%),灰色弧线则表示无降水天数。

首先打开 arc.js 文件,本节后续实现逻辑都将写到这里。同往常一样,这里需要加载一个数据集,即 data 文件夹下的降水量数据文件 daily_precipitation.csv。查看该 CSV 文件,会看到当中只包含两列,其中 date 列为 2021 年的每一天,而 total_precip_in 列则为每一天的总降水量,单位为英寸。

在下面的代码段中,我们使用 d3.csv() 读取数据集,并利用格式转换函数 d3.autoType 得到正确的日期和数字;然后链式调用 Promisethen 方法,并将得到的数据集输出到控制台:

d3.csv("./data/daily_precipitation.csv", d3.autoType).then(data => {
  console.log("precipitation data", data);
});

注意

这里不会展开讨论 d3.csv() 方法的细节,相关内容详见第三章,或者参考本章 4.1 小节了解 d3.autoType 函数的用法。

如果查看控制台中的数据,就会看到日期和数字的格式都是正确的。接下来就可以将处理好的数据集传入 drawArc() 函数。该函数已经事先在 arcs.js 中定义好了(译注:只是不完整):

d3.csv("../data/daily_precipitation.csv", d3.autoType).then(data => {
  console.log("precipitation data", data);
  drawArc(data);
});

drawArc() 函数中,可以添加一个新的 SVG 容器,如以下代码所示。先指定 SVG 容器的宽高为 300px,并添加到一个 ID 为 arcdiv 元素中。该 ID 值已在 index.html 文件中声明。这里利用第一章介绍的知识将 SVG 元素设为响应式容器:忽略其 widthheight 属性,直接指定 viewBox 属性并传入指定的宽高。这样,SVG 容器就能自适应父容器的大小,并保持恒定纵横比不变。最后,再将 SVG 容器的选择集赋给常量 svg 备用:

const pieChartWidth = 300;
const pieChartHeight = 300;
const svg = d3.select("#arc")
  .append("svg")
  .attr("viewBox", [0, 0, pieChartWidth, pieChartHeight]);

4.4.1 D3 中的极坐标系 The polar coordinate system

回顾 4.1 小节介绍的内容,当时的图表是绘制在一个 SVG 分组元素中并通过平移到达容器指定位置;而本节的实现策略不太一样,由于无需为坐标轴和标签预留足够空间,因此边距约定环节可以省略。但与目前创建的所有可视化效果不同,弧形图是基于极坐标系绘制的,并非之前的直角坐标系。

如图 4.24 所示,SVG 容器的坐标系为直角坐标系(也称笛卡尔坐标系)。它通过两个相互垂直的维度 xy 来描述二维平面空间中的位置;而平面极坐标系也有两个维度:半径与角度。其中半径为坐标原点与空间中某个点之间的距离,而角度则基于 12 点方向沿顺时针算得。这种空间坐标的描述方式在处理圆弧时特别管用。

图 4.24 直角坐标系的两个维度相互垂直,而极坐标系通过半径和角度来描述空间中的方位

【图 4.24 直角坐标系的两个维度相互垂直,而极坐标系通过半径和角度来描述空间中的方位】

由于元素在极坐标系中相对于原点定位,因此我们可以说,待绘制的弧形可视化效果的原点位于 SVG 容器的中心,如图 4.25 所示。

图 4.25 通过在 SVG 容器的分组元素中绘制弧形图,然后平移至容器正中,可以很方便地实现弧形图的绘制。此时各圆弧相对于图表中心定位,也就是极坐标系的原点位置。

【图 4.25 通过在 SVG 容器的分组元素中绘制弧形图,然后平移至容器正中,可以很方便地实现弧形图的绘制。此时各圆弧相对于图表中心定位,也就是极坐标系的原点位置。】

以下代码片段将在 SVG 容器内添加一个分组元素,然后平移至容器正中,并将该选择集赋给常量 innerChart

const innerChart = svg
  .append("g")
    .attr("transform", `translate(${pieChartWidth/2}, ${pieChartHeight/2})`);

要绘制圆弧,还需要计算弧形图中降水天数对应的角度值。使用 D3 创建饼图或环形图时,通常使用饼图布局生成工具(即 pie layout generator)来处理相关计算,该生成器工具将在下一章中重点介绍。本节只涉及两条弧线,因此计算起来很简单。

首先,通过数据集的 length 属性,我们知道了 2021 年全年数据点的总天数为 365 天。接着筛选出降水量大于 0 的天数,即 126 天,从而得到当年有降水记录的天数。最后用降水天数除以总天数,得到 35% 的占比:

const numberOfDays = data.length;
const numberOfDaysWithPrecipitation = data.filter(d => 
  d.total_precip_in > 0).length;
const percentageDaysWithPrecipitation =   
  Math.round(numberOfDaysWithPrecipitation / numberOfDays * 100);

接下来需要计算降水天数对应的角度,用占比乘以 360 度(即一整圈的度数),得到 126 度。用角度进行计算更加直观,但使用时需转换为弧度值,即用角度值乘以 π/180,算得弧度约为 2.2。最后将该结果赋给常量 angleDaysWithPrecipitation_rad

之所以需要转换,是因为后续会用到的弧形生成器需要弧度制单位而非角度单位。根据经验,在处理角度时 JavaScript 通常使用弧度制,而 CSS 样式表中则采用角度制计量:

const angleDaysWithPrecipitation_deg = percentageDaysWithPrecipitation * 
  360 / 100;
const angleDaysWithPrecipitation_rad = angleDaysWithPrecipitation_deg * 
  Math.PI / 180;

4.4.2 圆弧生成器的使用 Using the arc generator

终于讲到最有意思的部分:圆弧的生成了!首先需要像折线图和面积图那样,声明一个圆弧生成器(arc generator)。圆弧生成器函数 d3.arc() 位于 d3-shape 模块;本例中需要两个关键的访问器函数(accessor functions):圆弧的内圆半径和外圆半径,分别通过 innerRadius() 函数和 outerRadius() 函数进行设置,传入参数分别为 80px120px。如果内圆半径为 0,则会得到一个饼图。

const arcGenerator = d3.arc()
  .innerRadius(80)
  .outerRadius(120);

如图 4.26 所示,可以调用访问器函数 padAngle() 设置圆弧的填充角来定制圆弧形状。该函数接受一个弧度单位的角度值。这里用的是 0.02 弧度,相当于 1° 多一点点。此外还可以调用 cornerRadius() 函数对圆弧的拐角作圆化处理,其参数为一个基于像素的数字。该访问器函数的效果于 CSS 中的 border-radius 属性类似:

const arcGenerator = d3.arc()
  .innerRadius(80)
  .outerRadius(120)
  .padAngle(0.02)
  .cornerRadius(6);

图 4.26 圆弧生成器利用多个访问器函数计算弧形的 d 属性。图中声明生成器时分别指定了内圆半径、外圆半径、填充角及拐角半径。添加路径元素时需要传入每个圆弧的起始角与终止角。

【图 4.26 圆弧生成器利用多个访问器函数计算弧形的 d 属性。图中声明生成器时分别指定了内圆半径、外圆半径、填充角及拐角半径。添加路径元素时需要传入每个圆弧的起始角与终止角。】

您也许会问,为什么不使用访问器函数来处理圆弧覆盖的角度呢?本例中的角度值是手动计算的,因此在添加路径时将其直接传给圆弧生成器会更简单。在下一章中,我们还将介绍另一些不同的情况。

下面来添加代表降水天数的弧线。在以下代码片段中,先往内部图表的选择集内添加一个 path 元素;接着通过调用前面声明的圆弧生成器来指定其 d 属性。

注意观察,绘制圆弧时需要向生成器中传入一个包含起始角与终止角的配置对象:起始角 startAngle 的值为 0,对应 12 点钟方向;终止角 endAngle 的值为前面算好的降水天数对应的弧度值。最后指定圆弧的填充色为青蓝色 #6EB7C2

innerChart
  .append("path")
    .attr("d", () => {
      return arcGenerator({
        startAngle: 0,
        endAngle: angleDaysWithPrecipitation_rad
      });
    })
    .attr("fill", "#6EB7C2");

同理可添加第二段圆弧。此时弧线从上一条圆弧的终止位置开始,并在整个圆的终止处结束(对应的弧度为 2*Pi)。接着再给第二段圆弧指定填充色 #DCE2E2,即更接近灰色的颜色,表明这些日子没有降水:

innerChart
  .append("path")
    .attr("d", () => {
      return arcGenerator({
        startAngle: angleDaysWithPrecipitation_rad,
        endAngle: 2 * Math.PI
      });
    })
    .attr("fill", "#DCE2E2");

图 4.27 用以展示降水天数与无降水天数对比情况的弧线图效果

【图 4.27 用以展示降水天数与无降水天数对比情况的弧线图效果】

保存项目后,本地页面的弧形图应该与图 4.27 中的版本类似。您可以自行尝试变换传入圆弧生成器各访问器函数中的参数值,例如半径和拐角尺寸,观察它们对圆弧外观的影响。

如图 4.28 所示,弧形图的绘制过程与折线图及面积图类似,主要区别在于,弧线在平面空间中的位置坐标是通过极坐标描述的,并非之前的直角坐标系。这一点在圆弧生成器的访问函数中也有所体现。

图 4.28 弧形图的绘制步骤

【图 4.28 弧形图的绘制步骤】

4.4.3 圆弧形心的计算 Calculating the centroid of an arc

饼图和环形图近期在数据可视化领域可谓负面新闻不断,主要原因时人们发现肉眼并不擅长估算角度所代表的比例大小。不过,在包含少量数据类别时,这些图表的表现还是可圈可点的;但我们绝对可以通过添加标签来提高它们的可读性,这就是本节要实现的目标。

在表示降水天数的圆弧上,我们希望添加一个 “35%” 的标签,即之前算出的降水天数占比。该标签的最佳位置应该在圆弧的中心处,即形心(centroid)位置(也叫质心,即 center of mass)。该位置坐标可通过圆弧生成器计算得到。

在下面的代码片段中,我们调用了此前声明的圆弧生成器上的方法。此时,起始角与终止角也可以通过链式调用 startAngle()endAngle() 这两个访问器函数进行设置,最后再链式调用形心计算方法 centroid(),得到该圆弧的中点坐标:

const centroid = arcGenerator
  .startAngle(0)
  .endAngle(angleDaysWithPrecipitation_rad)
  .centroid();

将中点坐标输出到控制台,可以看到形心为包含两个坐标值的数组组成,分别为形心的水平与垂直坐标值,本例即为 [89, -45]

在以下代码片段中,我们向内部图表选择集添加了一个 text 元素来创建文本标签。为了让标签值带有百分号 %,这里用到了 D3 的格式化方法 d3.format(".0%"),然后在后面的括号中传入指定的数值。该方法可以轻松实现数字的各种格式化表示,如货币、百分比及指数形式;也可以为数字添加指定的后缀,如表示百万的 M 以及表示微米数量级的 μ。具体的格式转换列表详见 d3-format 模块的官方文档。

接着,我们使用 centroid() 方法返回的坐标数组完成 xy 属性的设置。注意,这里还指定了 text-anchor 属性和 dominant-baseline 属性,以确保文本标签在水平与垂直方向均相对 xy 属性居中渲染。

最后,将标签的字体颜色改为白色,粗细设为 500,以提高标签的可读性。保存项目后,带文本标签的弧形图效果应该与图 4.29 一致:

图 4.29 带文本标签的弧形图最终效果

【图 4.29 带文本标签的弧形图最终效果】

innerChart
  .append("text")
    .text(d => d3.format(".0%")(percentageDaysWithPrecipitation/100))
    .attr("x", centroid[0])
    .attr("y", centroid[1])
    .attr("text-anchor", "middle")
    .attr("dominant-baseline", "middle")
    .attr("fill", "white")
    .style("font-weight", 500);

至此,我们已经掌握了 D3 折线图、面积图以及弧形图的绘制方法。下一章,我们将利用布局生成工具(layout generators)让这些图形的绘制再提高一个档次!



另附:专栏文章连载期间 完全免费,后续 不排除 调整为收费专栏。对 D3.js 感兴趣、或者想要从零开始彻底掌握 D3 的朋友们强烈建议及时关注本专栏,一起学习交流,共同进步!

目前译好的其他章节内容如下(可进入专栏查看详情):

  • 第一部分 D3.js 基础知识
    • 第一章 D3.js 简介(已完结)
      • 1.1 何为 D3.js?
      • 1.2 D3 生态系统——入门须知
      • 1.3 数据可视化最佳实践(上)
      • 1.3 数据可视化最佳实践(下)
      • 1.4 本章小结
    • 第二章 DOM 的操作方法(已完结)
      • 2.1 第一个 D3 可视化图表
      • 2.2 环境准备
      • 2.3 用 D3 选中页面元素
      • 2.4 向选择集添加元素
      • 2.5 用 D3 设置与修改元素属性
      • 2.6 用 D3 设置与修改元素样式
      • 2.7 本章小结
    • 第三章 数据的处理(已完结)
      • 3.1 理解数据
      • 3.2 准备数据
      • 3.3 将数据绑定到 DOM 元素
        • 3.3.1 利用数据给 DOM 属性动态赋值
      • 3.4 让数据适应屏幕
        • 3.4.1 比例尺简介(上篇)
        • 3.4.2 线性比例尺(中篇)
          • 3.4.2.1 基于 Mocha 测试 D3 线性比例尺(DIY 实战)
        • 3.4.3 分段比例尺(下篇)
          • 3.4.3.1 使用 Observable 在线绘制 D3 条形图(DIY 实战)
      • 3.5 加注图表标签(上篇)
        • 3.5.1 人物专访:Krisztina Szűcs(下篇)
      • 3.6 本章小结
;