D3.js 学习 - 1 - D3基本使用 (d3-selection 选择集)
D3 简介
D3 是一个 Javascript 库,用于创建数据可视化图形,全称 Data-Driven Documents 也就是数据驱动文档,文档指的就是基于 web 的文档,代表可以在浏览器中展示的一切,包括 SVG 以及 一般的 HTML , D3扮演的是一个驱动程序的角色,联系着数据和文档
与 ECharts 图表相比,D3.js 不会生成事先定义好的图表,而是给你提供一些方法用来生成带数据的标签,绑定可视化的属性,如何制作图表由你自己来定义,D3.js 还提供很多数据处理的方法,用来生成可以更好的显示的数据模型。总的来说 ECharts 等可以提供更方便的图表组件,满足大部分的需求,D3.js 可以提供更丰富的自定义功能,适合定制化。
学习D3.js 更多的时候是学习它如何加载、绑定数据、数据处理、变换和过渡等这些方法的过程,官方文档的 API 非常的多,但很多都不是常用的,而且当前 D3.js 版本为 v5.5.0, 网上很多教程却都是针对 v3.x.x 版本的, D3.js 在 v4.x.x 开始 API 有了很大的不同 v5.5.0 API 与 v4.x.x 基本一致(更改的是将一些将回调转换为 promise ),于是,接下来的学习会将主要的经常被用到的 API 做下总结,然后结合实际的图表案例制作,学习其他 API ,使用的版本是最新的 v5.5.0 因此,学习过程中需要借助 这篇文章 来查询 v3.x.x 与 v4.x.x 的差异。
一些概念
数据就是结构化的信息,反映某些事实,可以是文本格式、二进制格式、图像、音视频、流等,但是对于 web 和 D3,js 来讲,更多的是使用文本数据
D3.js 用了一种与 jQuery 一样的 链式语法,这样通过
.
就把多个操作链接起来,执行逻辑上更加清晰,链式语法的关键就是每个操作函数都有返回值,这个返回值可以执行当前操作的对象,也可以是其他对象,在 D3.js 中要注意.append('path')
方法的返回值是新创建的元素对象,而不是调用这个方法的元素对象
常用 d3-selection 选择集 api
d3-selection 是 D3.js 的一系列选择集 API,这些方法都有返回值,有的返回的是当前的选择集,有的则返回新的选择集。
1..select()
选取符合条件的第一个元素,返回选取的元素
select() 方法的参数是一个 css 选择器,返回的是符合选择器的第一个元素,如果没有元素被选中则返回空集
select() 方法的参数除了是字符串外,也可以是一个指定节点的引用,例如事件监听函数中的this d
d3.select('#container')
d3.selectAll('p').on('click', function () {
d3.select(this).style('color', 'red')
})
- 1
- 2
- 3
- 4
- 5
除了 d3 对象外,选择集也可以调用
select()
方法,也就是在当前选择集下边继续选择元素,这个时候除了传入选择器字符串外,还可以传入一个函数, 这个函数中的this 表示当前的元素,函数会被传入三个参数 d: 元素数据 i: 元素索引 nodes 元素集
// 不能用 () => {} 否则 this 指向 window
d3.selectAll('p').select(function (d, i, nodes) {
console.log(this, d, i, nodes);
console.log(this === nodes[i]); // true
return this;
})
- 1
- 2
- 3
- 4
- 5
- 6
用来选择符合条件的所有元素,元素顺序按照其在文档中的顺序排列,如果没有元素则返回空集,与
select()
一样参数也可以是节点数组
除了 d3 对象外,选择集也可以调用
selectAll()
方法,意为进一步选择其所有符合条件的后代元素
d3.selectAll('p').selectAll('span').selectAll(function (d, i, nodes) {
console.log(d, i, nodes);
})
- 1
- 2
- 3
过滤并返回新的选择集,参数可以是字符串或者函数
d3.selectAll('p').filter('.none').style('color', 'red')
- 1
append() 方法用来添加新元素,参数就是新元素的标签名,他的返回值也是新添加的这个元素,参数还可以是一个函数 提供 d, i, nodes 三个参数,需要返回DOM元素
d3.select('#container')
.append('div')
- 1
- 2
指定的标签名可能需要包含命名空间前缀 比如
svg:text
指定命名空间,如果指定标签名的命名空间是已知的则可以省略
5. .insert()
插入元素到选择集中每个选中的元素前部
有两个参数,第一个参数定义要插入的元素标签名,第二个定义了一个选择器,指定在那个元素前添加
d3.select('#container')
.append('p')
.text('blue')
.style('color', 'blue')
.append('mark')
.text('mark')
.classed('blue', true);
d3.select('#container')
.selectAll('p')
.insert('span', '.blue') // 被插入到 类名为 blue 的 mark 标签前
.text('嘻嘻嘻嘻嘻')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
移除当前选择集
d3.select('.blue')
.remove()
- 1
- 2
7. .clone()
克隆当前选择集并添加到当前选择集后边
有一个可选参数 deep 表示是否 将所选元素的子元素页克隆了
d3.select('#container')
.append('p')
.text('blue')
.style('color', 'blue')
.append('mark')
.text('mark')
.classed('blue', true)
.clone(true)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
参数是一段文本字符串或者是动态更新的函数,提供 d, i, nodes 三个参数,返回值即为文本值, 没有参数时则返回选择集第一个元素的文本值
d3.select('#container')
.append('div')
.text('this is div')
- 1
- 2
- 3
有两个参数 分别是 属性名和属性值,表示设置属性,如果参数只有属性名则表示获取属性值
d3.select('#container')
.append('div')
.attr('id', 'box')
.attr('id'); // box
- 1
- 2
- 3
- 4
属性值也可以是一个函数,接受 d: 数据 i: 索引 nodes 组节点 三个参数,函数中 this 指向当前元素,函数返回值就是属性值,设置为 null 会将其移除
d3.select('#container')
.selectAll('p')
.data(data)
.enter()
.append('p')
.text((d, i) => `index: ${i}; org: ${d.org}; value: ${d.value};`)
.append('span')
.attr('class', 'to')
.attr('class', function (d, i, nodes) {
console.log(d, i, nodes, this);
return this.className;
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
classed() 有两个参数,第一个参数是一个或多个类名用空格链接的字符串,表示要操作的类名,第二个参数可以是一个布尔值或者返回值为布尔值的函数,同样获得 d, i, nodes 三个参数
用来判断操作的类名是添加(true)还是删除(false)
d3.select('#container')
.selectAll('p')
.data(data)
.enter()
.append('p')
.text((d, i) => `index: ${i}; org: ${d.org}; value: ${d.value};`)
.append('span')
.attr('class', 'to')
.attr('class', 'post')
.classed('to get', false)
.attr('class', function () {
console.log(this); // 只剩 post class
return this.className;
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
两个参数,样式名和样式值,表示设置样式,如果参数只有 样式名,表示获取指定的样式值
注意对于SVG 元素设置 style 的属性还是要 加上单位的,因为有些浏览器不支持默认单位
d3.select('#container')
.append('p')
.attr('class', 'blusse')
.text('blue')
.style('color', 'blue')
.style('color'); // blue
- 1
- 2
- 3
- 4
- 5
- 6
将指定数组的数据 data 与已经选中的元素进行绑定并返回一个新的选择集,新的选择集使用 update 表示:此时数据已经成功与元素绑定,并且定义了 enter 和 exit 方法用来返回需要加入元素和移除元素的选择集,参数 data 可以是一个数组,也可以是一个返回数组的函数
后续的操作如果有需要计算的属性值,就可以将属性值设置为有返回值的函数,这个函数会被传入参数 d 代表这个元素被分配的数据
var matrix = [
[11975, 5871, 8916, 2868],
[ 1951, 10048, 2060, 6171],
[ 8010, 16145, 8090, 8045],
[ 1013, 990, 940, 6907]
];
const p = d3.select('#container')
.selectAll('p')
.data(matrix)
.enter()
.append('p')
.text((d, i) => `index: ${i}; d:`)
p.selectAll('span')
.data(function (d, i, nodes) {
console.log(d, i); // 这个时候 d 是 matrix 的每一行 [11975, 5871, 8916, 2868]
return d
})
.enter()
.append('span')
.text(d => '--' + d + '--')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
data() 方法的第二个参数 key 函数,用来指定第几个数据被分配给第几个索引的元素,返回值即为 标识,代替索引
d3.select('#container')
.selectAll('p')
.data(data, d => d.id)
- 1
- 2
- 3
13. .enter()
返回没有 DOM 节点的占位节点选择集
在 enter() 之前调用 data() 传入数据后再调用 enter() 方法,可以生成当前没有与 data 数据对应的 DOM 的占位节点,通常用来在数据比节点多的时候创建缺失的节点
const oldP = d3.select('#container')
.selectAll('p')
.data(matrix)
.text((d, i) => `old index: ${i}; d:`)
console.log(oldP);
const p = oldP.enter() // enter() 后 append 的元素操作的仅仅是新添加的元素,旧的元素不会被操作
.append('p')
.text((d, i) => `new index: ${i}; d:`)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
14. .exit()
返回没有对应数据的,曾经存在的DOM节点选择集
exit 选择集通常用来移除多余的元素
const data = [
{name: 'xxx1', age: 19, id: 123},
{name: 'xxx2', age: 21, id: 124},
{name: 'xxx3', age: 34, id: 125},
{name: 'xx4x', age: 12, id: 126},
{name: 'xxx5', age: 1, id: 127}
];
d3.select('#container')
.selectAll('p')
.data(data, d => d.id)
.enter()
.append('p')
.text((d, i) => `F index: ${i}; d: ${JSON.stringify(d)}`)
const newData = [
{name: 'xxx2', age: 21, id: 124},
{name: 'xx4x', age: 80, id: 126}, // 这里实际修改了数据,但是是否更新的标准是 key(id) 是否一致,所以旧的没有被更新
{name: 'xxx7', age: 212, id: 129},
{name: 'xx48', age: 122, id: 128},
];
// 按照新数据重新渲染
const selectP = d3.select('#container')
.selectAll('p')
.data(newData, d => d.id); // 指定 按照 key 索引,这样旧的数据 key 没有对应上的就会被清除到 exit 的集合
// 将新的数据补上
selectP.enter()
.append('p')
.text((d, i) => `S index: ${i}; d: ${JSON.stringify(d)}`);
// 清除不用的数据
selectP.exit().remove();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
15. .datum()
获取或者 设置每个选中元素上绑定的数据
与
data()
方法不同,这个方法不会进行数据链式计算,不影响索引,不影响 enter 和 exit 选择集,参数为要设置的值,或者一个函数,返回要设置的值
// 按照新数据重新渲染
const selectP = d3.select('#container')
.selectAll('p')
.data(newData, d => d.id); // 指定 按照 key 索引,这样旧的数据 key 没有对应上的就会被清除到 exit 的集合
// 将新的数据补上
selectP.enter()
.append('p')
.datum((d, i) => `name: ${d.name}`)
.text((d, i) => `S index: ${i}; d: ${JSON.stringify(d)}`)
// 清除不用的数据
selectP.exit().remove();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
d3-selection 选择集的事件
为了交互,选择集允许监听和分派事件
1. .on(type, listener, capture)
添加事件监听器
- 为每个选中元素添加或者移除指定的事件监听器(浏览器支持的DOM事件都可用)
- listener 设置为 null 则监听器移除
- 同一个元素上注册同一个事件的监听函数会被覆盖
- 同一个元素注册同一个事件的监听器,可以在事件名后通过
.
指定分类,这样就不会被覆盖了 - capture 是个布尔值,表示是否启动事件捕获
d3.select('#container')
.selectAll('p')
.on('click.foo', function (d) {
console.log('foo click', d);
})
.on('click.bar', function (d) {
console.log('bar click', d);
})
.on('click.remove', function (d) {
console.log('remove click', d);
})
.on('click.remove', null)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
2. d3.touches(container[, touches])
返回触摸事件的坐标
返回触摸事件相对于 container 的坐标 container 是一个DOM元素
d3.select('#container')
.selectAll('p')
.on('touchstart', function (d) {
const touches = d3.touches(document.getElementById('container')); // [Array(2)]
console.log(touches)
})
- 1
- 2
- 3
- 4
- 5
- 6
定制化的控制流
为每个选中的元素依次调用指定的 function ,将会传入 d, i, nodes this指向当前DOM 元素
d3.select('#container')
.selectAll('p')
.each(function (d, i) {
console.log(d, i)
})
- 1
- 2
- 3
- 4
- 5
调用一个指定函数,函数的第一个参数是当前的选择集,其余参数就是 arguments
d3.select('#container')
.selectAll('p')
.call(function (selection, a, b) {
selection.append('span')
.text(a + b);
}, 'aaa', 'bbb')
- 1
- 2
- 3
- 4
- 5
- 6
const empty = d3.select('#container')
.selectAll('div')
.empty()
console.log(empty)
const nodes = d3.select('#container')
.selectAll('p')
.nodes()
console.log(nodes)
const size = d3.select('#container')
.selectAll('p')
.size()
console.log(size)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17