D3.js基础
概述
D3
在操作元素时的方式是转换而不是表示,即 D3
在操作元素时的语法描述是指将当前的状态转换为期望的状态所需要的更改(插入、更新和删除),而不是直接表示元素的期望状态;例如,在 div
元素中插入 span
元素,在 D3
中的语法是这样的:
d3.select('div') // 选中要插入 span 的 div 元素
.selectAll('span') // 选中 div 元素中的所有 span 元素
.data([1,2,3,4,5]) // 为 span 绑定数据
.enter() // 获取所有没有绑定数据的空元素
.append('span') // 将 span 插入空元素的位置
虽然这样很繁琐,但这样允许更精细的操作。
DOM对象
:DOM对象
是 D3
定义的用以描述 DOM
元素的对象,包含 DOM
的信息和其他一些方法属性;在选择元素时,D3
将 DOM
元素翻译为 DOM对象
;渲染时,D3
将 DOM对象
翻译成供浏览器渲染的 DOM
。
选择器
D3
的选择器选中 DOM
元素时,会返回一个对象,该对象包含 _group(分组)
和 _parent(父级)
。
分组
是一个数组,由 DOM对象
组成的数组组成。
父级
也是一个数组,由所有分组中元素的父级的 DOM对象
组成。
分组的多少不是由真实元素的父元素数量来决定的,而是由 D3
的选择器来决定的,如:
<div>
<span>1</span>
</div>
<div>
<span>2</span>
</div>
d3.selectAll('span') // 这时会获得一个分组,分组中有两个span元素对象,父级为空。
d3.selectAll('div').selectAll('span') // 这时会获得两个分组,每个分组中包含一个span元素对象,父级包含两个div元素对象。
数据绑定
D3
将数据与 DOM
绑定是通过在 DOM对象
中创建 __data__
属性来实现的。
D3
中的数据是由数组构成的,如:const data = [1, 2, 3]
,使用 selection.data(data)
,可以使数据和元素进行绑定,但需要注意,数组中的项是和 分组
进行绑定,而不是和每个 元素
绑定。
如:const data = [0, 1, 2]
的绑定结果是这样的:
如:const data = [[3, 4, 5], [6, 7, 8, 9]]
的绑定结果:
数据和 DOM对象
之间默认以数据的 index
进行识别和关联,当然也可以自定义关联关系。
Enter
、Update
、Exit
将 data
与 DOM对象
关联的时候,存在三种关系:
-
Enter
:进入,即数据与DOM对象
没有关联。这时要创建新的DOM对象
与数据进行关联。示例:
首先在
div
中创建四个span
,并为span
绑定数据,key
值为数据内容,颜色为blue
。d3.select('div') .selectAll('span') .data('ABCD'.split(''), d => d) .enter() .append('span') .style('width', '20px') .style('height', '20px') .style('margin-right', '5px') .style('display', 'inline-block') .style('background', 'blue')
再次
div
中的span
,并绑定数据ACGH
,由于原有的span
的key = G
和key = H
的不在ABCD
中(即这两个数据与DOM对象
没有关联),所以会得到两个需要enter
的DOM
元素,使用enter().append()
将元素加入(颜色为red
),最终得到六个span
(原有的四个蓝色的和新加入的两个红色的)。d3.select('div') .selectAll('span') .data('ACEF'.split(''), d => d) .enter() .append('span') .style('width', '20px') .style('height', '20px') .style('margin-right', '5px') .style('display', 'inline-block') .style('background', 'red')
-
Update
:更新,即DOM对象
的数据在data
中存在,这时要根据数据更新DOM对象
。示例:
首先在
div
中创建四个span
,并为span
绑定数据,key
值为数据内容,颜色为blue
。d3.select('div') .selectAll('span') .data('ABCD'.split(''), d => d) .enter() .append('span') .style('width', '20px') .style('height', '20px') .style('margin-right', '5px') .style('display', 'inline-block') .style('background', 'blue')
再次
div
中的span
,并绑定数据AC
,由于原有的span
的key
值有key = A
和key = C
(即数据与DOM对象
有关联),这时会得到两个需要Update
的DOM
元素。将这两个元素改为红色,最终得到四个span
(原有的两个蓝色的和两个改为红色的)。d3.select('div') .selectAll('span') .data('AC'.split(''), d => d) .style('background', 'red')
-
Exit
:退出,即DOM对象
的数据在data
中不存在,这时要移除多余的DOM对象
。示例:
首先在
div
中创建四个span
,并为span
绑定数据,key
值为数据内容,颜色为blue
。d3.select('div') .selectAll('span') .data('ABCD'.split(''), d => d) .enter() .append('span') .style('width', '20px') .style('height', '20px') .style('margin-right', '5px') .style('display', 'inline-block') .style('background', 'blue')
再次
div
中的span
,并绑定数据AC
,由于原有的span
的key
值有key = A
和key = C
(即数据与DOM对象
有关联),这时会得到两个需要Exit
的DOM
元素,使用enter().append()
将元素移除,最后剩下两个span
。d3.select('div') .selectAll('span') .data('AC'.split(''), d => d) .exit() .remove()
-
元素更新示例
初始化
d3.select('div') .selectAll('span') .data('ABCD'.split(''), d => d) .enter() .append('span') .style('width', '20px') .style('height', '20px') .style('margin-right', '5px') .style('display', 'inline-block') .style('background', 'blue')
-
修改
const span = d3.select('div') .selectAll('span') .data('ACEF'.split(''), d => d) span.exit().remove() // 退出 span.style('background', 'yellow') // 更新 span.enter() // 进入 .append('span') .style('width', '40px') .style('height', '40px') .style('margin-right', '5px') .style('display', 'inline-block') .style('background', 'red')
动画
概述:连续动画通常由离散的关键帧和插值或补间生成的中间帧来定义。D3
中的动画也是这样生成,并提供了 Interpolators(插值器)
来生成中间帧,中间帧和关键帧的时间是 0-1
。
Interpolators(插值器)
d3.interpolate(a, b)
:接收两个参数,返回中间值。他会根据参数 b
的类型使用相应的算法。如:当 b
是 null
、undefined
、boolean
时,直接使用常熟 b
;当 b
是颜色时,使用 d3.interpolateRgb
; 具体可查看官方文档。
示例:创建一个从红色到蓝色的插值器。
const fx = d3.interpolate('red', 'blue') // 创建一个插值器
fx(0) // 'rgb(255, 0, 0)' // 起始值,即 red
fx(0.5) // 'rgb(128, 0, 128)' // 中间值
fx(1) // 'rgb(0, 0, 255)' // 结束值,即 blue
fx(1.1) // 'rgb(0, 0, 255)' // 结束值,即 blue
生成动画
-
Transitions(过渡)
:使用D3
的Transitions
生成动画,Transitions
类似于Selections
用于在DOM
更改时生成动画,它支持大多数的选择方法,但在开始之前必须插入元素和绑定数据。Transitions
可以自动使用D3
的Interpolators(插值器)
计算中间帧,也可以自定义中间帧。示例:将
div
的颜色由red
过渡到blue
,持续时间为1s
。注意,假如div
元素没有设置background
属性的话,在过渡之前要先设置background
属性,不然transition
会找不到起始帧,导致没有过渡效果。d3.select('div') .style('background', 'red') .transition() .duration(1000) .style('background', 'blue')
-
Generators(生成器)
:自定义中间帧,每次生成中间帧时刷新整个视图的方法。示例:将
div
由red
变为blue
,中间帧为black
、yellow
。const generators = () => { const color = ['red', 'black', 'yellow', 'blue'] let index = 0 let timer = null const div = d3.select(divRef.current) timer = d3.interval( () => { div.style('background', color[index]) index += 1 console.log(99999999999) if(index === 4 ){ timer.stop() } }, 1000) }
Scales(尺度)
D3
中的 scale
的作用是将抽象的维度数据映射为可视的变量,抽象的维度数据指我们手中的数据,可视的变量指图纸中的坐标点。
如有以下数据:
const data = [
{name: '张三', age: 18},
{name: '李四', age: 20},
{name: '王五', age: 22}
]
生成的条形图:
name
属性映射为条形图的 Y轴点
, age
属性映射为条形图的 X轴点
。
D3
中有多种 scale
使用哪一种取决维度数据要映射的可视变量。上面的 X轴
是 linear scale
,因为 age
是量化数据;Y轴
是 band scale
,因为 name
是指标数据。
维度变量与可视变量之间的映射关系
D3
中的 Scales
系列函数,会跟据抽象的维度数据和可视的变量数据,生成一个映射函数,映射函数接收抽象的维度数据返回可视的变量数据。
以 d3.scaleLinear
为例,将 [0, 9]
的范围映射到 [18, 36]
的范围中去,得到映射函数 x
,这样的话,每个单位是 2
,起点是 18
,则 x(4) = 18 + 4 * 2 = 26
,x(10) = 18 + 10 * 2 =38
,D3
代码如下:
const x = d3.scaleLinear()
.domain([0, 9])
.range([18, 36])
x(4) // 输出 26
x(10) // 输出 38
在 d3.scaleLinear()
中维度写在 domain
中,将可视变量写在 range
, 这样就可以建立起维度数据与可视变量之间的映射关系了。
以 d3.scaleBand
为例,将 [张三, 李四, 王五]
的映射到 [18, 36]
的范围中去,得到映射函数 x
,这样的话,将[18, 36]
的范围分为三份,[张三, 李四, 王五]
分别对应 [18, 27, 36]
,则 x(张三) = 18
,x(李四) = 27
,而 x(马六) = undefined
,应为 马六
在建立映射的时候不存在,所以没有相对应的可视变量,这点是 量化映射
和 指标映射
的区别,D3
代码如下:
const x = d3.scaleBand()
.domain(['张三', '李四', '王五'])
.range([18, 36])
x('张三') // 输出 18
x('李四') // 输出 27
x('马六') // 输出 undefined
思考:维度变量
既可以是 指标变量
也可以是 量化变量
,可视变量
则一定是 量化变量
。
Axis(坐标轴)
D3
提供了将 维度变量和可视变量的映射关系
以 Axis(坐标轴)
的形式表示的方法。
示例:
// 生成维度变量和可视变量的映射关系
const x = d3.scaleLinear()
.domain([0, 9])
.range([18, 360])
// 将映射关系以坐标轴表示
d3.select(svgRef.current)
.append('g')
.call(d3.axisBottom(x))