[数据可视化] D3.js 前置知识

[数据可视化] D3.js 前置知识

·

4 min read

d3最有特点的应该就是它的selection机制和基于selection机制的数据绑定了

D3 selection

一、选择元素

d3selection是所选择到的元素的集合,下面是一个简单示例

// 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元素集合中的每一个,这个函数会为datadom都返回一个key,然后domdata会根据这个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元素

首先需要准备nodeslinks的数据,以下为示例:

// 节点数据
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:每个节点的下标

  • xy:节点的横纵坐标

  • vxvy:节点的横向和纵向的速度

你还可以自己加上fxfy两个属性,用于将节点的坐标固定在某个位置,官方文档描述:

二、添加连接和力

使用了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,官方文档描述如下:

image-20240119110604659

此外,还可以添加静电力和向心力:

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完成后,如果nodefx属性,那么nodex的值就会被设置为fxfyy同理,如果需要让一个点不再固定,那么就可以将fxfy设置为null,官方文档的描述如下:

image-20240119142753940

理解这一点对于后面通过drag事件来修改node节点的数据从而触发tick事件,然后修改连线和文字的位置很有帮助

四、alpha 和 restart

d3中常用的两个与alpha相关的apialphaalphaTargetalpha这个词代表了能量,如果参数为0,那么就会直接停下来,如果参数不为0,那么就会逐渐停下来

那么alphaalphaTarget又有什么区别呢?alpha的话是直接将能量设置为某个值,设置了为0之后就不会再触发tick,而alphaTarget是设定一个目标值,alphaTarget(0)表示能量会逐渐减缓为0,设定了之后仍会在一定时间内触发tick

它们会在drag事件中与restart结合在一起使用,例如:

if (!d3.event.active) simulation.alphaTarget(0.1).restart()

restart()表示以当前的状态重新激活力导向图,如果设置了alphaTarget(0)但是在后面拖拽事件start的时候没有重新激活这个力导向图,那么就不会触发tick事件了