Demo
饼图/环形图可以清晰地展示数据之间的比例关系。下面两个环形图分别展现了电子游戏 GTAV 在各个游戏硬件平台上和在不同国家的销售量。
我们的源数据是这样的:
数据来自 vgcharts
可以看到相对于表格数据,饼图可以更容易让人察觉到数据要传达的意义。
解析
这样的饼图,可以分成三个组成部分:环形(Arc)、文字标签(Label)和连接环形和标签之间的连线(Line)。
Arc
SVG 的 path 元素可以帮助我们在浏览器中渲染出环形,描述形状的 d 属性需要使用 d3.arc 来生成。为了生成一个环形,我们需要四个参数:开始角度(startAngle),结束角度(endAngle),内圆半径长(innerRadius),外圆半径长(outerRadius)。当内圆半径为0的时候生成的图形是饼,不为0的时候则是环形。内外圆的半径长度我们指定为自己想要的任意长度,但是开始和结束的角度是根据给出的数据计算出来的,d3.pie 可以根据输入的数据生成这些角度数据。
假设们有这样一个数组描述数据:
var data = [1, 1, 2, 3, 5, 8, 13, 21];
d3.pie 可以把这个数据转化为扇形的角度数据
var generator = d3.pie().value((d) => d);
var slices = generator(data);
其中 value 是一个取值函数,也就是 accessor,如果单个数据使用对象(object)来描述,那么这个函数可以指定取 object 里面的哪个值用于这个扇形的角度。
转化之后的数据为这样的结构:
[
{"data": 1, "value": 1, "index": 6, "startAngle": 6.050474740247008, "endAngle": 6.166830023713296, "padAngle": 0},
{"data": 1, "value": 1, "index": 7, "startAngle": 6.166830023713296, "endAngle": 6.283185307179584, "padAngle": 0},
{"data": 2, "value": 2, "index": 5, "startAngle": 5.817764173314431, "endAngle": 6.050474740247008, "padAngle": 0},
{"data": 3, "value": 3, "index": 4, "startAngle": 5.468698322915565, "endAngle": 5.817764173314431, "padAngle": 0},
{"data": 5, "value": 5, "index": 3, "startAngle": 4.886921905584122, "endAngle": 5.468698322915565, "padAngle": 0},
{"data": 8, "value": 8, "index": 2, "startAngle": 3.956079637853813, "endAngle": 4.886921905584122, "padAngle": 0},
{"data": 13, "value": 13, "index": 1, "startAngle": 2.443460952792061, "endAngle": 3.956079637853813, "padAngle": 0},
{"data": 21, "value": 21, "index": 0, "startAngle": 0.000000000000000, "endAngle": 2.443460952792061, "padAngle": 0}
]
其中 index 指的是该数据在原数组中的 index。
接下来再指定内外径就可以生成环形了。
var arc = d3.arc().innerRadius([radius]).outerRadius([outerRadius]);
对这个 arc generator 传入之前获取的角度属性即可得到 path 的 d 属性。
var d = arc(slices[0]);
以上这个例子来源于官方文档。
Line
连线和标签的位置有根据不同的设计有不同的布局方法,这里采用的方式是在环形之外再计算一个不可见的环形,通过连接两个环形的图形画出一段延伸线,再根据这个环处在圆的左半边还是右半边,向外水平延伸一段距离。示意图如下:
外部的环在最后的实现中是不可见的,这里画出来体现开发思路。
连线由三个部分组成,起点、转折点和终点。起点是可见环的图心,转折点是外部不可见环的图心,终点是从转折点向外水平延伸一段距离得到的。
判断环在左半圆还是右半圆可以通过中间角来判断
var midAngle = (startAngle + endAngle) / 2;
因为一个整圆的弧度是 2π,所以如果 midAngle 在 [0, π],那么我们可以判断环在左半边圆,如果在 (π, 2π] 这个范围那么在右半边圆。
Label
标签的位置根据转折点而定,其中一个细节要注意的是文字的对齐方式 (text-anchor),左半圆环形的标签应该右对齐,右半边圆环形应该左对齐。
在某些数据情况下,可能会存在相互遮盖的标签,这时候我们需要做设计防重合的设计,一个比较简单的思路是:如果连续两个标签在同一个半圆(都在左或者都在右),并且高度距离只差小于字体高度,那么把后一个标签向外延伸上一个标签的长度。这个设计并没有在下面的实现中体现,有时间的话读者可以自行尝试。
实现
下面是完整源码实现,数据源存储在另外的 csv 文件里面,这里通过 d3.csv 读取之后,再在 callback 函数里面渲染。github 地址在这里查看。