Author: arcsin1
Time: 2020-04-22 00:01
一直以来,都想写D3可视化系列文章,不用担心我会结合D3的V4,V5版本写下去。 让更多喜欢可视化的能够轻松用D3上手定制自己喜欢的可视化。
我分为4个部分:(目前只介绍svg篇,后续再介绍canvas篇) 1. svg入门(包括svg的基本元素等) 2. d3中的数据驱动和dom的操作 3. d3中数据的绑定 4. d3中的比例尺 复制代码
在对D3.js
进行学习之前,有必要先了解一下SVG,因为D3.js
基本上是对SVG元素进行操作从而进行可视化展示的。当然现在D3.js
同样支持canvas
输出,不过我们更建议先从SVG进行入门。
网上对于SVG的教程非常多,你只需了解SVG中的基础元素,线、圆、矩形、三角形等等。并熟悉SVG的DOM结构。推荐几个基本教程:
本节我们使用d3.js
在svg
中利用基础元素画一只猫。
首先添加一张svg
画布:(demo示例地址在后面)
const svg = d3.select('body') .append('svg') .attr('width', 600) .attr('height', 300) 复制代码
利用rect
元素绘制猫的头部:
const head = svg.append('g') .attr('id', 'head') .attr('transform', 'translate(60, 80)') head.append('rect') .attr('width', 100) .attr('height', 60) .attr('stroke-width', 1) .attr('stroke', '#000') .attr('fill', '#fff') 复制代码
利用circle
元素绘制猫的眼睛和嘴巴:
// eyes head.append('circle') .attr('cx', 20) .attr('cy', 20) .attr('r', 10) .attr('fill', '#529fca') head.append('circle') .attr('cx', 80) .attr('cy', 20) .attr('r', 10) .attr('fill', '#529fca') // mouse head.append('circle') .attr('cx', 50) .attr('cy', 50) .attr('r', 5) .attr('fill', '#529fca') 复制代码
通过path
元素绘制猫的两只耳朵:
// ears svg.append('path') .attr('d', 'M65 80 L80 60 L95 80 Z') .attr('fill', '#fff') .attr('stroke', '#000') svg.append('path') .attr('d', 'M125 80 L140 60 L155 80 Z') .attr('fill', '#fff') .attr('stroke', '#000') 复制代码
绘制猫的身体:
// body const body = svg.append('g') .attr('id', 'body') .attr('transform', 'translate(110, 110)') body.append('rect') .attr('width', 120) .attr('height', 60) .attr('stroke-width', 1) .attr('stroke', '#000') .attr('fill', '#fff') 复制代码
四只脚:
// legs svg.append('line') .attr('x1', '110') .attr('y1', '170') .attr('x2', '100') .attr('y2', '200') .attr('stroke', '#000') .attr('stroke-width', 1.5) svg.append('line') .attr('x1', '130') .attr('y1', '170') .attr('x2', '140') .attr('y2', '200') .attr('stroke', '#000') .attr('stroke-width', 1.5) svg.append('line') .attr('x1', '210') .attr('y1', '170') .attr('x2', '200') .attr('y2', '200') .attr('stroke', '#000') .attr('stroke-width', 1.5) svg.append('line') .attr('x1', '230') .attr('y1', '170') .attr('x2', '240') .attr('y2', '200') .attr('stroke', '#000') .attr('stroke-width', 1.5) 复制代码
加上尾巴:
// tail svg.append('path') .attr('d', 'M230 140 L250 140 L250 120 L270 120 L270 100') .attr('fill', '#fff') .attr('stroke', '#000') svg.append('circle') .attr('cx', 270) .attr('cy', 95) .attr('r', 5) .attr('fill', '#fff') .attr('stroke', '#000') 复制代码
最后再加点小小的星星花纹:
body.append('polygon') .attr('points', '9.9, 1.1, 3.3, 21.78, 19.8, 8.58, 0, 8.58, 16.5, 21.78') .attr('transform', 'translate(80, 20)') 复制代码
这样,一只小猫就画完了:
demo地址codepen
代码主要的逻辑就是添加元素,并给元素赋予多种属性,最后组合在一起。在绘制的js
代码中你会发现,头部的绘制是放在身体之后的,这是因为你需要将猫脸都露出来。如果先绘制身体,后绘制头部,那么猫脸就会被身体遮盖一部分。如果你熟知css
样式,可能很容易就想到这是元素堆叠的问题,与z-index
有关系。
不过很遗憾,z-index
在svg
元素中不起作用。svg
中元素的堆叠关系只跟绘制的先后顺序有关。先绘制的在下面,后绘制的在上面。
d3.js
使用了链式语法,让操作变得十分顺畅,相信这种语法对你来说已经非常熟悉了,在jQuery,Lodash等库中也有使用。当然你也可以把它拆开,在你需要的时候:
const body = d3.select('body') const svg = body.append('svg') 复制代码
在阅读完SVG的教程之后,我们可以开始对D3.js
的学习。
本章阐述如何将数据和DOM结构关联在一起
数据可视化的关键就在于将数据反应到视图上,也就是DOM结构上,D3.js
就是天生为此而生的一件工具。
选中某个DOM,该语句是最为基本也最为常用的,用法基本与$.select()
相同。
d3.select('body').style('background-color', 'black') 复制代码
选中一个往往不够用,所以也可以使用d3.selectAll()
选中所有的DOM。
d3.selectAll('p').style('color', 'white') 复制代码
在body
标签中插入p
标签,注意是在末尾。
d3.select('body').append('p') 复制代码
同样也是在选择集中插入标签,和d3.append()
唯一一点不同之处在于多了一个用于指示插入位置的参数。对于如下DOM结构进行操作:
<ul class="list-group" id="list-group"> <li class="list-group-item">0001</li> <li class="list-group-item">0002</li> <li class="list-group-item">0003</li> </ul> 复制代码
d3.select('#list-group') // 在#list-group的第2个子元素之前插入一个节点 .insert('li', '.list-group-item:nth-child(2)') 复制代码
删除选中的元素。
d3.select('p').remove() 复制代码
将数据与DOM绑定是核心之所在,那么如何绑定呢?
对于如下代码:
先看看例子
在body中插入了5个p标签,并在p标签中插入了我们想要的数据。
这里d3.v5版本把下面api合并到了d3.join()
在上一例中,我们在body元素中没有p元素的情况下,插入了5个p元素,并写入了对应的文本。实际上,选择的元素个数与绑定的数据个数存在3种关系,d3.data()返回三个函数进行对应:
当两者长度相等时,可以使用数据对原本元素中的数据进行更新;当数据的长度大于选择的元素的数量,那么就可以进行数据插入;相反,小于的情况时,可以获取多余的元素。见下图:
update 部分的处理办法一般是:更新属性值 enter 部分的处理办法一般是:添加元素后,赋予属性值 exit 一般适用于删除多余元素
d3.datum()
同样可以用来处理数据与元素的绑定
datum()
绑定的是数组,那么整个数组会绑定到每个被选择的元素上。而使用data()
的话,那么会依次绑定数据。传入的数据类型的不同
关于data()
传入的数据。
最好传入数组。
不可传入Object
。
d3.select('text').data({id: 2}) // 无效,错误 复制代码
可传入字符串,不过只输入第一个字符。
d3.select('text').data('234').text(e => { return e }) // 只展示 '2' 复制代码
关于datum()
传入的数据。
可以传入字符串,并且全部输出。
d3.select('text').data('234').text(e => { return e }) // 展示 '234' 复制代码
也可以传入数组,对象。
datum()
会将传入的值全部绑定到元素上。
更多区别
D3中有个重要的概念就是比例尺。比例尺就是把一组输入域映射到输出域的函数。映射就是两个数据集之间元素相互对应的关系。比如输入是1,输出是100;输入是5,输出是10000,那么这其中的映射关系就是你所定义的比例尺。
D3中有相当数量的比例尺函数,能满足你的各种需求,有连续性的,非连续性的,本文对于常用比例尺进行一一介绍。
d3.scaleLinear()
线性比例尺使用d3.scaleLinear()
创造一个线性比例尺,而domain()
是输入域,range()
是输出域,相当于将domain
的数据集映射到range
的数据集中。
const scale = d3.scaleLinear().domain([1,5]).range([0,100]) 复制代码
映射关系:
接下来,研究这个比例尺的输入与输出:
scale(1) // 输出:0 scale(4) // 输出:75 scale(5) // 输出:100 复制代码
如果使用区域外的数据会得出什么结果呢?
scale(-1) // 输出:-50 scale(10) // 输出:225 复制代码
所以,scale
只是定义了一个映射规则,映射的输入值并不局限于domain()
中的输入域。可以使用clamp()
函数来限制输出的值域。该函数只针对连续性的比例尺有效。
scale.clamp(true) scale(-1) // 输出:0 scale(10) // 输出:100 复制代码
d3.scaleBand()
序数比例尺d3.scaleBand()
并不是一个连续性的比例尺,domain()
中使用的是四个值,而range()
需要的是一个连续域:
const scale = d3.scaleBand().domain([1,2,3,4]).range([0,100]) 复制代码
映射关系:
看一下输入与输出:
scale(1) // 输出:0 scale(2) // 输出:25 scale(4) // 输出:75 复制代码
当输入不属于domain()
中的数据集时:
scale(0) // 输出:undefined scale(10) // 输出:undefined 复制代码
由此可见,d3.scaleBand()
只针对domain()
中的数据集映射相应的值。
d3.scaleOrdinal()
序数比例尺d3.scaleOrdinal()
的输入域和输出域都使用离散的数据。
const scale = d3.scaleOrdinal().domain(['jack', 'rose', 'john']).range([10, 20, 30]) 复制代码
映射关系:
输入与输出:
scale('jack') // 输出:10 scale('rose') // 输出:20 scale('john') // 输出:30 复制代码
当输入不是domain()
中的数据集时:
scale('tom') // 输出:10 scale('trump') // 输出:20 复制代码
输入不相关的数据依然有输出值。所以在使用时,要注意输入数据的正确性。
我们从上面的映射关系中可以看出,domain()
和range()
的数据是一一对应的。如果两边的值不一样呢?下面两张图说明这个问题:
domain()
的值按照顺序循环依次对应range()
的值。
d3.scaleQuantize()
量化比例尺d3.scaleQuantize()
也属于连续性的比例尺。定义域是连续的,而输出域是离散的。
const scale = d3.scaleQuantize().domain([0, 10]).range(['small', 'medium', 'long']) 复制代码
映射关系:
输入与输出:
scale(1) // 输出:small scale(5.5) // 输出:medium scale(8) // 输出:long 复制代码
而对于domain()
域外的情况:
scale(-10) // 输出:small scale(30) // 输出:long 复制代码
大概就是对domain()
域的两侧的延展。
d3.scaleTime()
时间比例尺d3.scaleTime()
类似于d3.scaleLinear()
线性比例尺,只不过输入域变成了一个时间轴。
const scale = d3.scaleTime() .domain([new Date(2017, 0, 1, 0), new Date(2017, 0, 1, 2)]) .range([0,100]) 复制代码
输入与输出:
scale(new Date(2017, 0, 1, 0)) // 输出:0 scale(new Date(2017, 0, 1, 1)) // 输出:50 复制代码
时间比例尺较多用在根据时间顺序变化的数据上。另外还有一个d3.scaleUtc()
是根据世界标准时间(UTC)来计算的。
D3提供了一些颜色比例尺。10就是10种颜色,20就是20种:
d3.schemeCategory10 d3.schemeCategory20 d3.schemeCategory20b d3.schemeCategory20c // 定义一个序数颜色比例尺 let color = d3.scaleOrdinal(d3.schemeCategory10) 复制代码
另外有一些函数比例尺的功能,从名称上就可见一斑。产生的效果可以在实际操作中实验一下。
d3.scaleIdentity() // 恒等比例尺 d3.scaleSqrt() // 乘方比例尺 d3.scalePow() // 类似scaleSqrt的乘方比例尺 d3.scaleLog() // 对数比例尺 d3.scaleQuantile() // 分位数比例尺 复制代码
invert()
与invertExtent()
方法上述的各种使用比例尺的例子都是一个正序的过程,从domain
的数据集映射到range
的数据集中,那么有没有逆序的过程呢?D3中提供了invert()
以及invertExtent()
方法可以实现这个过程。
let scale = d3.scaleLinear().domain([1,5]).range([0,100]) scale.invert(50) // 输出:3 let scale2 = d3.scaleQuantize().domain([0,10]).range(['small', 'big']) scale2.invertExtent('small') // 输出:[0,5] 复制代码
不过,值得注意的是,这两种方法只针对连续性比例尺有效,即domain()
域为连续性数据集的比例尺。
到此,d3基本的一些概念大概是这些,下一篇文章我会一步步用D3去画一个折线图的步骤。 希望我也能写下去。