本篇大纲:Generator、Component、Layout
截至目前,我们已经学会 D3 如何将资料与DOM 元素绑定来呈现资料视觉化,也知道要怎麽将资料整理成想要的内容,接下来我们就要看看 D3 如何建构图形罗!
看到这边大家心里可能会有个疑问:不是用 SVG 去建构图形就好吗?为何 D3 还要设计其它建构图形的API 呢?
那是因为我们之前提到 SVG 提供的内建几何图形(圆形、矩形、线条、路径等)其实只是小小的集合,一张完整图表则是由几百个这些小元件组成的复杂集合,如果只使用 SVG 提供的图形,就需要很辛苦地一个一个建立图形,才能完成一张图表。为了省去这个麻烦,D3 创建了很多不同的 API 来协助建构复杂图形/图表,这些 API 们也因此便被称为 helper functions。
这些用来协助绘制图型的 helper functions 可以依照使用的资料复杂度与产生出来的结果来划分为三大类:
Generator
:产生 < path > 的d标签路径Component
:产生 DOM 元素Layout
:产生整张图表我们先看到下面这张图表
看完不太懂也没关系,下面我们会分别讲讲这三大类 helper functions 的特性~
这一大类的 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可以使用
接着来看看 d3.line( )官方的解说,了解可以带入那些参数、哪些方法必须搭配使用
从官网的解说得知,d3.line() 可以带入两个参数来进行运算:分别是 x 跟 y 值,而这两个参数可以是数字或是方法
举例来说,假设我们手上有一笔资料,想把它换算成 需要的 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')
成功产出线条!
是不是很简单呢? 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')
登登登~成功得到一个填满区域的图型!
这边只是基础的介绍跟使用,等之後与 scale( )、axis( ) 等其他方法结合後,就可以使用 area( ) 去绘制出类似下方的图表,是不是很好看呀~
d3.arc( )
最後再来看到另一个 Generator 中很用到的API — d3.arc( ),这个 API 主要是用来画弧线,它通常会跟 pie( ) 这个 API 结合绘制圆饼图。但它还能搭配其他API 画另外一种很酷炫的图,猜得到是什麽图表吗?
就是「车子的仪表板」!
是不是很酷呀?很想知道要怎麽画吗?先别急,我们先来看看要怎麽使用 d3.arc( )。一样先看到官方文件,得知要使用 d3.arc( ) 需要搭配另外四个 API
参数 | 解释 |
---|---|
arc.innerRadius( ) | 内圈范围 |
arc.outerRadius( ) | 外圈范围 |
arc.startAngle( ) | 起始角度 |
arc.endAngle( ) | 终点角度 |
官方文件上对於 arc( ) 跟它的定义也写得很清楚
了解这些後,我们就可以直接来使用 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 个半圆弧啦~
因此,如果想要画出方向盘的圆弧,只要调整一下 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')
接下来我们来讲讲第二类 helper functions — Components。上面提到 Generators 只建立给 < path > 用的d 的命令指令,而 Components 则完全相反。这一大类的 API 们会使用回传的方法去建立一整组图形物件,供给特定的图表使用
。就拿这一大类中最被使用的 d3.axis( )
来举例好了,.axis( ) 会接收 scale( ) 回传的方法,接着绘制出 < line >, < path >, < g > 与 < text > 等等的一堆元素,一起组成一组轴线。我们实际来看看例子会更清楚:
axis ( )
一样先开官方文件来看看有哪些 API 可以用!
知道有这些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);
透过这几个步骤,我们就能画出一条写有刻度的轴线啦!
看到这边你可能会大喊:写错了!X轴应该在下面才对!赔钱、还我时间!!
先别激动,这是因为我们之前说过,svg 的原点都在左上角
,依序由上至下、由左至右建构图形,所以这边的X轴会飘在上方也是合情合理、完全没错。那要怎麽让它导邪归正,做回一条正常的X轴线呢?一样就要派 transform 上场啦~
d3.select('.axis')
.append("g")
.call(xAxis)
.attr("transform", "translate(0,130)") // 调整X轴位置
透过修改 DOM 元素的 transform 值,我们就能将 X 轴移动到任何想要的位置上~
这边由於篇幅的关系,axis( ) 就先介绍到这边,等到後面专门讲轴线的章节时,会有更详细的解说。
看完 axis( ) 的例子後,有清楚 Components 这类的 API 在做什麽了吗? 除了 axis( ) 之外,brush( )、zoom( ) 也都归纳在 Components 这一类 Helper Functions 内。後面也会有专门的章节来讲这两个 API,有兴趣的人可以订阅按赞开启小铃铛
看完前面两类 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( ) 在干嘛就好
我们先看到 官网 上列出 stacks 有哪些 API 可以使用,以及它们的功能是什麽
接着我们来看看 d3.stack( )的解释
文件上写着当我们使用 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 值的数据会被视为同一个集合,因此这边就有四个集合
以这个例子来说明,我们先使用 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),就像这样
接着使用这些集合去计算并各自返还一个阵列
每个集合中有几笔资料,就会一一返还对应的阵列
我们把返还的阵列展开,看看里面到底是放什麽资料~展开後我们会看到每个阵列都包含三笔资料,分别代表
这样一来我们就得到需要的资料了,可以运用这些起始值跟终点值来绘制堆集图啦!
// 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 > 来绘制横向长条图,就完成啦!!!
其实了解 d3 API 的运作原理、需要的资料以及返还什麽东西之後,是不是觉得图表也没有很难画了呀~虽然 Layout 这一类的 API 相比 Generators 跟 Components 更难懂一些,但只要搞懂这些 API 返还什麽值、要怎麽运用,其实就能轻松画出想要的图表了。
今天的 Helper Functions 就讲到这边~看完之後相信大家对於 d3 用来绘制形状的 API 都有一定的认识啦,明天开始要进入 d3 动画跟互动的部分罗!敬请期待!
最後的最後,一样附上本章的程序码与图表 Github 、 Github Page,需要的人请自行取用~
<<: CRUD的UD / ICON / confirmDialog - day06
>>: 06 - Uptime - 掌握系统的生命徵象 (4/4) - 使用合成监控 (Synthetics Monitor) 从使用者情境验证服务的运作状态
大家好,我是毛毛。ヾ(´∀ ˋ)ノ 废话不多说开始今天的解题Day~ 205. Isomorphic...
利用 provide & inject 机制,父元件当作所有其子元件的 provider,无...
今日一样教学Django class registration_info(models.Model)...
前言 这篇比较是一般的课题,我觉得适合所有人观看。而如果你是领导者的话,你也可以开始关注一下你的团...
「类别」 中的一班 「方法」(方法成员),先建立该「类别」的「物件」,再使用 「物件名称.方法名称」...