Day9-D3绘图:绘制形状的Helper Functions

本篇大纲:Generator、Component、Layout

截至目前,我们已经学会 D3 如何将资料与DOM 元素绑定来呈现资料视觉化,也知道要怎麽将资料整理成想要的内容,接下来我们就要看看 D3 如何建构图形罗!

看到这边大家心里可能会有个疑问:不是用 SVG 去建构图形就好吗?为何 D3 还要设计其它建构图形的API 呢?

那是因为我们之前提到 SVG 提供的内建几何图形(圆形、矩形、线条、路径等)其实只是小小的集合,一张完整图表则是由几百个这些小元件组成的复杂集合,如果只使用 SVG 提供的图形,就需要很辛苦地一个一个建立图形,才能完成一张图表。为了省去这个麻烦,D3 创建了很多不同的 API 来协助建构复杂图形/图表,这些 API 们也因此便被称为 helper functions。

这些用来协助绘制图型的 helper functions 可以依照使用的资料复杂度产生出来的结果来划分为三大类:

  • Generator:产生 < path > 的d标签路径
  • Component:产生 DOM 元素
  • Layout:产生整张图表

我们先看到下面这张图表
https://ithelp.ithome.com.tw/upload/images/20210921/20134930umP8TEOFl9.jpg

看完不太懂也没关系,下面我们会分别讲讲这三大类 helper functions 的特性~

Generators

这一大类的 API 们是 D3 里面最基本的方法~主要是透过使用基础的资料集(像是array、number等等),来产生绘制 svg <path>需要的命令列字串(d)

我们在 Day3-SVG 那篇有提到,如果我想在 svg 使用 < path > 去绘制一条线的话,需要透过 d 属性与属性值去设定这条线的位置

  <path
    d="M50 20 C80 90,40 200,250,100"  // <==就是这个家伙
    stroke="black"
    fill="none"
    stroke-width="2"
  />

但 d 属性值的这一大串英文+数字基本上很难用人力去自行换算,因此我们就要借助 d3 的API去计算。

d3.line( )

我们先以 d3.line( ) 来示范:一样先到官网看看 d3.line( ) 有哪些API可以使用
https://ithelp.ithome.com.tw/upload/images/20210921/20134930pilhKAzWc5.jpg

接着来看看 d3.line( )官方的解说,了解可以带入那些参数、哪些方法必须搭配使用
https://ithelp.ithome.com.tw/upload/images/20210921/20134930OpMpqDX0sB.jpg

从官网的解说得知,d3.line() 可以带入两个参数来进行运算:分别是 x 跟 y 值,而这两个参数可以是数字或是方法
https://ithelp.ithome.com.tw/upload/images/20210921/20134930CACJaaHJuW.jpg

https://ithelp.ithome.com.tw/upload/images/20210921/201349308jBB77rj2k.jpg

举例来说,假设我们手上有一笔资料,想把它换算成 需要的 d 属性值

// Line Generator
const data1 = [{x:10,y:10},{x:20,y:10},{x:30,y:10},{x:40,y:10},{x:50,y:10}]

一开始先用 line( ) 来设定方法

const line = d3.line()
               .x(d=> d.x) // 设定x值要抓哪些资料
               .y(d=> d.y) // 设定y值要抓哪些资料

设定好方法後,我们再将手上的这笔资料带进去,就能得到想要的值了

line(data1) // 带入要换算的资料,得到"M10,10L20,10L30,10L40,10L50,10"

取得可以用在d属性上的值後,最後就是把这些资料绑订到指定的 DOM 元素上面啦~

// html
<svg class="line"></svg>

// js
d3.select('.line')
  .append('path')
  .attr('d', line(data1))
  .attr('stroke', 'black')
  .attr('stroke-width', '2')
  .attr('fill', 'none')

成功产出线条!
https://ithelp.ithome.com.tw/upload/images/20210921/20134930nIoVJLaTHY.jpg

是不是很简单呢? Generator 这类的方法主要就是在做这些事: 将资料换算成绘制 svg 需要的 code。这一类常见的API 包含:line ( )、arc ( )、area( )、symbol ( ) 等等,都是将资料换算并产出 < path > 的d属性值,再将值套到 DOM 元素上去绘制图型。我们再来多看几个 Generator 的范例吧!

d3.area( )

一样上[官网](https://github.com/d3/d3-shape/blob/v3.0.1/README.md#area)去看解说,得知d3.area( ) 有三个必须要带的参数,分别是
API 解释
area.x( ) x的座标
area.y1( ) y轴做边
area.y0( ) 开始绘制区域的y轴范围

知道应该要带那些参数後,我们就开始绘制区域图案吧!我们手上有的资料是 data1,一样使用 d3.area( ) 先设定 area 这个方法

// html
<svg class="area"></svg>

// js
const data1 = [{x:10,y:100},{x:20,y:100},{x:30,y:100},{x:90,y:20},{x:220,y:10}]
const area = d3.area()
                   .x(d=>d.x)
                   .y1(d=>d.y)
                   .y0(10)

area(data1) // 呼叫方法并带入资料,得到 M10,100L20,100L30,100L90,20L220,10L220,10L90,10L30,10L20,10L10,10Z

得到 d 的属性值後,我们就可以将这个资料带进选定的 DOM 元素啦

d3.select('.area')
  .append('path')
  .attr('d', area(data1))
  .attr('stroke', 'blue')
  .attr('fill', 'blue')

登登登~成功得到一个填满区域的图型!
https://ithelp.ithome.com.tw/upload/images/20210921/20134930hRuPMFs5f0.jpg

这边只是基础的介绍跟使用,等之後与 scale( )、axis( ) 等其他方法结合後,就可以使用 area( ) 去绘制出类似下方的图表,是不是很好看呀~
https://ithelp.ithome.com.tw/upload/images/20210921/20134930Y26tPdcoJS.jpg

d3.arc( )

最後再来看到另一个 Generator 中很用到的API — d3.arc( ),这个 API 主要是用来画弧线,它通常会跟 pie( ) 这个 API 结合绘制圆饼图。但它还能搭配其他API 画另外一种很酷炫的图,猜得到是什麽图表吗?

就是「车子的仪表板」!
https://ithelp.ithome.com.tw/upload/images/20210921/201349300cRuctYx78.jpg

是不是很酷呀?很想知道要怎麽画吗?先别急,我们先来看看要怎麽使用 d3.arc( )。一样先看到官方文件,得知要使用 d3.arc( ) 需要搭配另外四个 API

参数 解释
arc.innerRadius( ) 内圈范围
arc.outerRadius( ) 外圈范围
arc.startAngle( ) 起始角度
arc.endAngle( ) 终点角度

官方文件上对於 arc( ) 跟它的定义也写得很清楚

  • arc ( ) 这个API 是用来绘制弧形
  • 弧形的中心点永远 [0 , 0],亦即 画面左上角,如果想要移动这个弧形就用 transform 去处理
  • 弧形角度由 .startAngle( ) 跟.endAngle( ) 去控制,当start 跟 end 的角度加起来大於或等於 τ (也就是2π) 的时候,就会形成一个完整的圆型
  • .startAngle( ) 起始角度从指针12点钟方向开始,顺时针画圆弧
  • pie ( ) 这个API 会将资料阵列换算成 arc( ) 需要的 start 跟 end 角度,所以 arc( ) 通常跟 pie( ) 搭配使用

https://ithelp.ithome.com.tw/upload/images/20210921/20134930P9wYEocqIc.jpg

了解这些後,我们就可以直接来使用 arc( ) 啦!

<svg class="arc"></svg>

// js
const arc = d3.arc()
              .innerRadius(40)  // 内圈范围40
              .outerRadius(50)  // 内圈范围50
              .startAngle(0)
              .endAngle(Math.PI*0.5) // 画一个 1/4 的圆形

d3.select('.arc')
      .append("g")
      .attr("transform", "translate(100,100)")  // 把整个圆弧移动到 100,100 的位置
      .append('path')
      .attr('d', arc())
      .attr('stroke', 'blue')
      .attr('fill', 'blue')

这样就能得到 1/4 个半圆弧啦~
https://ithelp.ithome.com.tw/upload/images/20210921/2013493092sr4JDij6.jpg

因此,如果想要画出方向盘的圆弧,只要调整一下 start angle 跟 end angle;想改变圆的大小则是调整 innerRadius 跟 outerRadius

// arc
    const arc = d3.arc()
                  .innerRadius(60)
                  .outerRadius(65)
                  .startAngle(Math.PI*1.2)
                  .endAngle(Math.PI*2.8)

    d3.select('.arc')
      .append("g")
      .attr("transform", "translate(150,80)")
      .append('path')
      .attr('d', arc())
      .attr('stroke', 'blue')
      .attr('fill', 'blue')

https://ithelp.ithome.com.tw/upload/images/20210921/20134930ZBvP6fCjZz.jpg


Components

接下来我们来讲讲第二类 helper functions — Components。上面提到 Generators 只建立给 < path > 用的d 的命令指令,而 Components 则完全相反。这一大类的 API 们会使用回传的方法去建立一整组图形物件,供给特定的图表使用。就拿这一大类中最被使用的 d3.axis( ) 来举例好了,.axis( ) 会接收 scale( ) 回传的方法,接着绘制出 < line >, < path >, < g > 与 < text > 等等的一堆元素,一起组成一组轴线。我们实际来看看例子会更清楚:

axis ( )

一样先开官方文件来看看有哪些 API 可以用!
https://ithelp.ithome.com.tw/upload/images/20210921/20134930hicktTgwHa.jpg

知道有这些API 可以使用之後,我们来看看 axis( ) 的解说~官方文件上其实就是简单短短的一行

axis( ) 用来将 scale( )的资料转换成人类能看得懂的文字,让这个最无趣的任务变得轻松简单

没错,就是这麽简单!因为当我们想使用 d3 建构图表时,要先用 scale( ) 把数据资料换算成符合比例、能够让d3读懂的数值,才能把这些数值传换成图表;而axis( ) 唯一的任务就是把 scale( ) 换算好的值,再转成人类看得懂的文字,最後绘制成轴线。

这边我们就来实际绘制一条X轴线看看吧:

// 我目前的资料集
const data1 = [{x:10,y:100},{x:20,y:100},{x:30,y:100},{x:90,y:20},{x:220,y:10}]

// 抓出 x 轴要使用的值
const xData = data1.map((i) => i.x);

// 设定X轴的比例尺与绘制范围
const xScale = d3.scaleLinear()
                 .domain([0, d3.max(xData)])
                 .range([10, 290]);

//使用xScale的设定,绘制刻度(ticks)朝下的轴线
const xAxis = d3
    .axisBottom(xScale)

// 呼叫轴线
d3.select('.axis').append("g").call(xAxis);

透过这几个步骤,我们就能画出一条写有刻度的轴线啦!
https://ithelp.ithome.com.tw/upload/images/20210921/201349303NzYaXfL5j.jpg

看到这边你可能会大喊:写错了!X轴应该在下面才对!赔钱、还我时间!!

先别激动,这是因为我们之前说过,svg 的原点都在左上角,依序由上至下、由左至右建构图形,所以这边的X轴会飘在上方也是合情合理、完全没错。那要怎麽让它导邪归正,做回一条正常的X轴线呢?一样就要派 transform 上场啦~

d3.select('.axis')
  .append("g")
  .call(xAxis)
  .attr("transform", "translate(0,130)")  // 调整X轴位置

透过修改 DOM 元素的 transform 值,我们就能将 X 轴移动到任何想要的位置上~
https://ithelp.ithome.com.tw/upload/images/20210921/20134930rheMbJ4peg.jpg

这边由於篇幅的关系,axis( ) 就先介绍到这边,等到後面专门讲轴线的章节时,会有更详细的解说。

看完 axis( ) 的例子後,有清楚 Components 这类的 API 在做什麽了吗? 除了 axis( ) 之外,brush( )、zoom( ) 也都归纳在 Components 这一类 Helper Functions 内。後面也会有专门的章节来讲这两个 API,有兴趣的人可以订阅按赞开启小铃铛


Layouts

看完前面两类 Helper Functions 之後,我们来看看最後一大类 Helper Functions 吧!Layouts 比起 Generator、Components 又更进阶,这类的 API 是直接拿 一整个完整的资料集去绘制完整的图表,这类的 API 可以很直觉简单,例如:pie( ) 绘制的圆饼图;也可以很复杂,例如:force( ) 绘制的原力关联图。

Layouts 需要的完整资料集有可能是多个阵列,也有可能是 Generators 类的 API 产生的资料,它会使用资料集去计算像素座标与角度,常见的API 有 stack( )、pie( )、force( ),下面我们就用 stack( ) 来实际演练看看吧!

stack( )

stack( ) 这个 API 主要用来画长条堆集图,但还得要搭配 scale( )、axis( ) 等API 才能画出下面的图表。等到後面章节讲完其他必要的API後,会再带大家实际绘制图表,今天就先稍微了解 stack( ) 在干嘛就好
https://ithelp.ithome.com.tw/upload/images/20210921/20134930mky479MMk3.jpg

我们先看到 官网 上列出 stacks 有哪些 API 可以使用,以及它们的功能是什麽
https://ithelp.ithome.com.tw/upload/images/20210921/20134930Ou8vS3cTk0.jpg

接着我们来看看 d3.stack( )的解释
https://ithelp.ithome.com.tw/upload/images/20210921/201349301KqgcXvM9U.jpg

文件上写着当我们使用 d3.stack( ) 带入一个资料阵列时,这个 API 会返还另一个代表每笔资料集的阵列,而这些资料集是用 keys 去决定的。看完後是不是觉得有看没有懂呢?没关系很正常(看得懂的是神人),我们来一一解说吧!

由於d3.stack( ) 大多被用来绘制长条堆积图,因此很重要的一点就是:要把哪些资料归为同一集合?假设我目前有一系列的资料,纪录2021年1~4月,中国、美国、台湾的每月肺炎确诊人数

const dataStack = [
      {month: new Date(2021, 0, 1), China: 32, America: 20, Taiwan: 30},
      {month: new Date(2021, 1, 1), China: 7, America: 27, Taiwan: 18},
      {month: new Date(2021, 2, 1), China: 13, America: 33, Taiwan: 18},
      {month: new Date(2021, 3, 1), China: 6, America: 18, Taiwan: 20}
    ];

目前的资料是:一个阵列内含四个物件,这四个物件内分别有 month、China、America、Taiwan 四个 key 值,而这些正是 d3.stack() 所需要的 keys。

当我们使用d3.stack() 时,它会根据资料的 key 值把资料分类,同样 key 值的数据会被视为同一个集合,因此这边就有四个集合

  1. month [new Date(2021, 0, 1)、new Date(2021, 1, 1)、new Date(2021, 2, 1)、new Date(2021, 3, 1)]
  2. China [32、7、13、6]
  3. America [20、27、33、18]
  4. Taiwan [30、10、18、20]

以这个例子来说明,我们先使用 d3.stack( ) 定义一个叫做 stack 的方法,设定要建立堆叠图的资料 keys 分别是"China"、 "America"、 "Taiwan",接着再设定一个变数stackedSeries,这个变数是把 stack 方法带 dataStack 的资料後得到的数值

const stack = d3.stack()
                .keys(["China", "America", "Taiwan"]) // 设定资料的keys

const stackedSeries = stack(dataStack); // 把资料带入stack方法

因为我们设定的keys有三项,因此 d3.stack() 会把同一个 key 的数值视为同一集合(series),就像这样
https://ithelp.ithome.com.tw/upload/images/20210921/20134930gJOCIXC945.jpg

接着使用这些集合去计算并各自返还一个阵列
https://ithelp.ithome.com.tw/upload/images/20210921/20134930UWlJM0mPoz.jpg

每个集合中有几笔资料,就会一一返还对应的阵列
https://ithelp.ithome.com.tw/upload/images/20210921/20134930teW4TFMyGo.jpg

我们把返还的阵列展开,看看里面到底是放什麽资料~展开後我们会看到每个阵列都包含三笔资料,分别代表

  • 起始值
  • 终点值
  • 隶属哪个物件
    https://ithelp.ithome.com.tw/upload/images/20210921/20134930CnaZZQMlMg.jpg

这样一来我们就得到需要的资料了,可以运用这些起始值终点值来绘制堆集图啦!

// stack
    const dataStack = [
      {month: new Date(2021, 0, 1), China: 132, America: 80, Taiwan: 30},
      {month: new Date(2021, 1, 1), China: 67, America: 27, Taiwan: 188},
      {month: new Date(2021, 2, 1), China: 123, America: 153, Taiwan: 18},
      {month: new Date(2021, 3, 1), China: 27, America: 112, Taiwan: 20}
    ];

    const stack = d3.stack()
                    .keys(['China', 'America', 'Taiwan'])

    const stackedSeries = stack(dataStack);
    console.log(stackedSeries)

    // 颜色
    const colorScale = d3.scaleOrdinal()
                         .domain(['China', 'America', 'Taiwan'])
                         .range(["red", "blue", "orange"])

    // 建立集合元素g、设定颜色
    const g = d3.select('.stack')
                .attr('width', 300)
                .selectAll('g')
                .data(stackedSeries)
                .enter()
                .append('g')
                .attr('fill', d => colorScale(d.key));
    
    // 绘制长条图
    g.selectAll('rect')
      .data(d=>d)
      .join('rect')
      .attr('width', d => d[1] - d[0]) // 长度为终点值减掉起始值
      .attr('x', d => d[0]) // x 座标设定为起始值
      .attr('y', (d, i) => i *30) // y 座标用 index 来处理,乘上每条bar想拉开的距离
      .attr('height', 20);

我们先把不同的 keys 所组合的资料集设定不同颜色,接着使用 d3.stack( ) 返还的起始值跟终点值去设定每条bar中,不同资料集的起迄位置,最後用 < rect > 来绘制横向长条图,就完成啦!!!
https://ithelp.ithome.com.tw/upload/images/20210921/20134930xIPCQqoGdJ.jpg

其实了解 d3 API 的运作原理、需要的资料以及返还什麽东西之後,是不是觉得图表也没有很难画了呀~虽然 Layout 这一类的 API 相比 Generators 跟 Components 更难懂一些,但只要搞懂这些 API 返还什麽值、要怎麽运用,其实就能轻松画出想要的图表了。

今天的 Helper Functions 就讲到这边~看完之後相信大家对於 d3 用来绘制形状的 API 都有一定的认识啦,明天开始要进入 d3 动画跟互动的部分罗!敬请期待!


Github Page 图表与 Github 程序码

最後的最後,一样附上本章的程序码与图表 GithubGithub Page,需要的人请自行取用~


<<:  CRUD的UD / ICON / confirmDialog - day06

>>:  06 - Uptime - 掌握系统的生命徵象 (4/4) - 使用合成监控 (Synthetics Monitor) 从使用者情境验证服务的运作状态

Day 18 - Isomorphic Strings

大家好,我是毛毛。ヾ(´∀ ˋ)ノ 废话不多说开始今天的解题Day~ 205. Isomorphic...

[Part 3 ] Vue.js 的精随-元件 provide/inject

利用 provide & inject 机制,父元件当作所有其子元件的 provider,无...

Day18 参加职训(机器学习与资料分析工程师培训班),Django

今日一样教学Django class registration_info(models.Model)...

9. 你过劳了吗?

前言 这篇比较是一般的课题,我觉得适合所有人观看。而如果你是领导者的话,你也可以开始关注一下你的团...

java 类别方法

「类别」 中的一班 「方法」(方法成员),先建立该「类别」的「物件」,再使用 「物件名称.方法名称」...