D3数据可视化-饼图-pie

Demo

饼图/环形图可以清晰地展示数据之间的比例关系。下面两个环形图分别展现了电子游戏 GTAV 在各个游戏硬件平台上和在不同国家的销售量。

data-pie.png

我们的源数据是这样的:

data-table.png

数据来自 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

连线和标签的位置有根据不同的设计有不同的布局方法,这里采用的方式是在环形之外再计算一个不可见的环形,通过连接两个环形的图形画出一段延伸线,再根据这个环处在圆的左半边还是右半边,向外水平延伸一段距离。示意图如下:

outerArc.png

外部的环在最后的实现中是不可见的,这里画出来体现开发思路。

连线由三个部分组成,起点、转折点和终点。起点是可见环的图心,转折点是外部不可见环的图心,终点是从转折点向外水平延伸一段距离得到的。

判断环在左半圆还是右半圆可以通过中间角来判断

var midAngle = (startAngle + endAngle) / 2;

因为一个整圆的弧度是 2π,所以如果 midAngle 在 [0, π],那么我们可以判断环在左半边圆,如果在 (π, 2π] 这个范围那么在右半边圆。

Label

标签的位置根据转折点而定,其中一个细节要注意的是文字的对齐方式 (text-anchor),左半圆环形的标签应该右对齐,右半边圆环形应该左对齐。

在某些数据情况下,可能会存在相互遮盖的标签,这时候我们需要做设计防重合的设计,一个比较简单的思路是:如果连续两个标签在同一个半圆(都在左或者都在右),并且高度距离只差小于字体高度,那么把后一个标签向外延伸上一个标签的长度。这个设计并没有在下面的实现中体现,有时间的话读者可以自行尝试。

实现

下面是完整源码实现,数据源存储在另外的 csv 文件里面,这里通过 d3.csv 读取之后,再在 callback 函数里面渲染。github 地址在这里查看。

<!DOCTYPE html>
<html>
  <body>
    <script src="http://d3js.org/d3.v5.min.js"></script>
    <script type="text/javascript">

      // Sales number of video game GTAV
      // Data visualization in two pie charts

      const PLATFORM = {
        PS4: 'PS4',
        PS3: 'PS3',
        PC: 'PC',
        XBox360: 'XBOX360',
        XBoxOne: 'XBoxOne',
      };

      const REGION = {
        NA: 'NA',
        PAL: 'PAL',
        JP: 'JP',
        OTHER: 'OTHER',
      }

      function getPie(data = []) {
        // console.log('data', data);

        const width = 600;
        const height = 400;
        const outerRadius = 120;
        const innerRadius = 80;
        const pivotRadius = 160;

        // const colorArray = ['red', 'green', 'blue', 'yellow'];
        const colorArray = [
          '#204A87',
          '#EF2928',
          '#9ADE00',
          '#0084C8'
        ]

        function getMidAngle(d) {
          return (d.endAngle + d.startAngle) / 2;
        }

        const svg = d3.select('body')
          .append('svg')
          .attr('width', width)
          .attr('height', height)

        svg.append('g').attr('class', 'slices')
        svg.append('g').attr('class', 'lines')
        svg.append('g').attr('class', 'labels')

        const overallTotal = data.reduce((accu, curr) => accu + curr.total || 0, 0);
        const formattedOverallTotal = Math.floor(100 * overallTotal) / 100;
        svg.append('text').text(`${formattedOverallTotal} m`).attr('transform', `translate(${width / 2}, ${height / 2})`).attr('text-anchor', 'middle')

        const getValue = (d) => {
          return d.total;
        };

        const pie = d3.pie().value(getValue);
        const slices = pie(data);
        const innerArc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius);

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

        slice.exit().remove();

        const endPoints = [];
        const pivotArc = d3.arc().innerRadius(outerRadius).outerRadius(pivotRadius);
        const line = svg.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();

        const label = svg.select('.labels').selectAll('text').data(slices);
        label.enter()
          .append('text')
          .text((d) => {
            const value = d.value;
            const label = d.data.label;
            return `${label}: ${value} m`;
          })
          .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();
      }

      function visualize(data) {
        const regionData = Object.keys(REGION).map((region) => {
          const total = data.map((datum) => datum.sales[region]).reduce((accu, curr) => accu + curr, 0);

          return {
            region,
            total: Math.floor(total * 100) / 100,
            label: region,
          };
        });

        const platformData = data.map((d) => {
          let total;
          if (d.total) {
            total = d.total;
          } else {
            total = Object.values(d.sales).reduce((accu, curr) => accu + curr, 0);
          }
          return {
            ...d,
            total: Math.floor(100 * total) / 100,
            label: d.platform,
          };
        });

        getPie(platformData, 'pie1');
        getPie(regionData, 'pie2');
      }

      // Need to start a local file server for serving this file due to the security policy of web browser
      d3.csv('./GTAV.csv', (row) => {
        const dataObject = {
          total: parseFloat(row['TOTAL']) || 0,
        };

        return {
          platform: row.platform,
          sales: Object.keys(REGION).reduce((accu, region) => {
            return {
              ...accu,
              [region]: parseFloat(row[region]),
            };
          }, dataObject),
        };
      })
      .then(visualize);

    </script>
  </body>
</html
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • d3 (核心部分)选择集d3.select - 从当前文档中选择一系列元素。d3.selectAll - 从当前文...
    谢大见阅读 8,792评论 1 4
  • 上一节中, 我们对于svg 的坐标系统 和 常用的比例尺进行了学习, 了解了坐标轴的建立,懂得了数据绑定和事件回调...
    金字笙调阅读 5,672评论 0 4
  • 一.D3.js 概述 1.D3 是什么D3 的全称是(Data-Driven Documents),翻译过来就是一...
    nightZing阅读 25,218评论 0 8
  • 2016-04-14 本篇文章谈谈 d3 的动画、svg 图形生成器以及 d3 提供的 layout 一、动画 1...
    HeyDelilah阅读 5,637评论 0 2
  • We have higher goals but lesser actions. This is not new ...
    Jenna_King阅读 3,511评论 0 0