d3
最有特点的应该就是它的selection
机制和基于selection
机制的数据绑定了
D3 selection
一、选择元素
d3
的selection
是所选择到的元素的集合,下面是一个简单示例
// dom 元素
<div></div>
<script>
// 选择集
const selection = d3.select("div")
</script>
一个selection
就是一个Pt
对象,每个Pt
对象会有一个_groups_
属性,_groups_
属性是一个数组,包含到你选择的元素
此外还有一个_parents_
属性,是父节点
二、操作元素
选择到selection
后,可以对selection
进行操作,包括样式、插入子元素、设置属性等等,这里比较简单,详情可以参考官方文档:Selecting elements | D3 by Observable (d3js.org)
d3.select("body")
.append("svg")
.attr("width", 960)
.attr("height", 500)
.append("g")
.attr("transform", "translate(20,20)")
.append("rect")
.attr("width", 920)
.attr("height", 460);
三、绑定数据
1. selecion.data()
这是d3
做数据可视化的核心api
,对selection
使用data()
函数可以向selection
绑定数据
const data = [1, 2, 3, 4 ,5]
const update_selection = d3.slect("svg")
.data(data)
使用data()
后,会返回一个update selection
,这就是绑定了数据的selection
,查看该Pt
对象中的_groups_
数组中的每个元素,会发现它们都有一个_data_
属性,这就对应了data
数组中的一项数据
并且绑定了data
后,在后面的链式调用中,其子元素仍然可以通过function(d) {}
访问到该中的属性d
来访问到该data
,这个后面会说
2. 多组数据绑定
如果有多组数据,例如selectAll
后又跟了selectAll
,那么就需要用到函数属性(d) => {return d}
来访问数据,这个函数在绑定数据时会被每个组都遍历一遍,如下:
const matrix = [
[11975, 5871, 8916, 2868],
[ 1951, 10048, 2060, 6171],
[ 8010, 16145, 8090, 8045],
[ 1013, 990, 940, 6907]
]
const tr = d3.select("body")
.append("table")
.selectAll("tr")
.data(matrix)
.join("tr")
// 此时 tr 上已经绑定了数据,所以链式调用的时候是可以通过 d 访问到的
const td = tr
.selectAll("td")
.data(d => d) // 这里的 data() 需要传入一个函数,这个函数的返回值就是要绑定的 datum 所组成的数组
.join("td")
.text(d => d)
这就说明了,每select
一层子元素,数据就会解构一层
3. 控制 data()中的绑定对应关系
直接来看官方文档的示例,如下:
<div id="Ford"></div>
<div id="Jarrah"></div>
<div id="Kwon"></div>
<div id="Locke"></div>
<div id="Reyes"></div>
<div id="Shephard"></div>
<script>
const data = [
{name: "Locke", number: 4},
{name: "Reyes", number: 8},
{name: "Ford", number: 15},
{name: "Jarrah", number: 16},
{name: "Shephard", number: 23},
{name: "Kwon", number: 42}
];
d3.selectAll("div")
.data(data, function(d) {
return d ? d.name : this.id;
})
.text(d => d.number);
</script>
可以看到`data()
函数的第二个参数为一个函数,这个叫做key
函数,在进行数据绑定时,所有的dom
元素和data
数据都会“评估”(即使用) 这个函数,其中的d
参数就代表了data
中的每一项或者dom
元素集合中的每一个,这个函数会为data
和dom
都返回一个key
,然后dom
和data
会根据这个key
进行数据绑定,从而达到对数据绑定对应关系的控制
4. selection.enter()、seletion.exit()
在绑定数据后,有可能遇到三种情况:数据量大于节点数量、节点数量大于数据量、节点数量刚好与数据量相等,官方文档的描述的对应情况是这样的:
entry
表示数据量大于节点量的selection
exit
表示节点量大于数据量selection
update
就用来表示被绑定了数据的selection
四、属性函数
这是在d3
中非常常用的设置属性的方式,如果selection
已经绑定了data
,那么可以用(d) => { return d }
来访问到对应的datum
,也就是属性函数中的参数d
,如下:
const data = [1, 2, 3, 4, 5]
d3.selectAll("svg")
.data(data) // 已经绑定了 data
.attr("transform", (d) => {
return `translate(${d}, ${d})` // 这里的 d 就对应了 data 数组中的每一项
})
D3 drag
D3 simulation
一、创建力导向图
创建力导向图需要d3.forceSimulation()
和d3.links()
结合使用,随后你可以根据这个力导向图的数据来绘制你的svg
元素
首先需要准备nodes
和links
的数据,以下为示例:
// 节点数据
const nodes = [
{
name: "张三",
},
{
name: "李四",
},
{
name: "王二",
},
{
name: "小李",
},
{
name: "陈二",
},
{
name: "某人",
},
{
name: "小二",
},
{
name: "郭靖",
},
];
/**
source 和 target 属性是必须要有的
用于后面 forceLink 的时候作为 key 来指定与哪些节点连接
*/
const links = [
{
source: 0,
target: 1,
relation: "关系1",
},
{
source: 1,
target: 2,
relation: "关系2",
},
{
source: 0,
target: 3,
relation: "关系3",
},
{
source: 0,
target: 4,
relation: "关系4",
},
{
source: 0,
target: 5,
relation: "关系5",
},
{
source: 0,
target: 6,
relation: "关系6",
},
{
source: 0,
target: 7,
relation: "关系7",
},
];
下面创建一个力导向图:
s3.forceSimulation(nodes)
可以输出来看一下力导向图的节点(nodes
):
创建了这个力导向图后,是没有力的连接的,然后nodes
会被d3
添加以下的属性:
index
:每个节点的下标x
、y
:节点的横纵坐标vx
、vy
:节点的横向和纵向的速度
你还可以自己加上fx
和fy
两个属性,用于将节点的坐标固定在某个位置,官方文档描述:
二、添加连接和力
使用了simulation()
创建了力导向图后,就可以使用force()
来添加对应的连接和力
// 在这里 forceLink 了之后 links 就会把 source 和 target 设置成相应的 node
const linkForce = d3.linkForce(links).distance(100)
d3.forceSimulation(nodes)
.force("link", linkForce)
此时可以将links
输出来看一下:
这里还需要提一下,我们可以在d3.linkForce()
的返回值上设置link.id()
,来手动声明每一个link
连接哪两个node
,在默认情况下,也就是不设置link.id()
的话,它相当于访问器函数直接返回d.index
,如下:
// 这里的 id 的 d 是 nodes 的数据,return 的值可以指定 links 根据什么来连接对应的 node
// 在这里 forceLink 了之后 links 就会把 source 和 targets 设置成相应的 node
const linkForce = d3.inlkForce(links).id(d => {
console.log(d)
return d.index // 默认情况下返回 d.index,也就是根据 index 的对应关系来连接 node
})
d3.forceSimulation(nodes)
.force("link", linkForce)
你可以自己指定返回值,来自定义要连接哪些node
,官方文档描述如下:
此外,还可以添加静电力和向心力:
let simulation = d3
.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-20)) // 电荷力 相互之间的作用力
.force("center", d3.forceCenter(width / 2, height / 2)) // 用指定的 x 坐标和 y 坐标创建一个居中力
.force("link", d3.forceLink(links).id(d => { console.log(d); return d.index }).distance(100))
三、添加 on tick 事件
在力导向图的节点数据发生变化时,会触发tick
事件,这时候我们可以通过监听tick
来对links
以及其他的部分作出改变
在一个tick
完成后,如果node
有fx
属性,那么node
的x
的值就会被设置为fx
,fy
和y
同理,如果需要让一个点不再固定,那么就可以将fx
和fy
设置为null
,官方文档的描述如下:
理解这一点对于后面通过drag
事件来修改node
节点的数据从而触发tick
事件,然后修改连线和文字的位置很有帮助
四、alpha 和 restart
d3
中常用的两个与alpha
相关的api
有alpha
和alphaTarget
,alpha
这个词代表了能量,如果参数为0,那么就会直接停下来,如果参数不为0,那么就会逐渐停下来
那么alpha
和alphaTarget
又有什么区别呢?alpha
的话是直接将能量设置为某个值,设置了为0之后就不会再触发tick
,而alphaTarget
是设定一个目标值,alphaTarget(0)
表示能量会逐渐减缓为0,设定了之后仍会在一定时间内触发tick
它们会在drag
事件中与restart
结合在一起使用,例如:
if (!d3.event.active) simulation.alphaTarget(0.1).restart()
restart()
表示以当前的状态重新激活力导向图,如果设置了alphaTarget(0)
但是在后面拖拽事件start
的时候没有重新激活这个力导向图,那么就不会触发tick
事件了