D3 基本流程

d3

D3 基本流程

参考

D3 开发者创建了一个在线开发环境 Observable,除了可以查看官方的教程样例,它也是一个可视化作品分享社区,在里面可以找到很多优秀的可视化作品

本文介绍 D3.js 基于数据操作 DOM 的基本流程,涉及几个核心概念:

  1. 选择元素:选择需要操作的 DOM 节点(一般是 SVG 元素)
  2. 绑定数据:将数据与 DOM 节点相关联,这样就实现了数据驱动 DOM 元素的属性样式更新
  3. 增删元素:使用 join 操作(或使用 enter-update-exit 操作)增删 DOM 节点
提示

这是 D3 将数据映射为页面元素的最基本流程,主要使用 selection 选择模块,除此之外 D3 还提供其他模块用以实现更复杂的数据可视化效果。

选择器

为了基于数据「驱动」DOM,首先要选中所需操作的元素。JS 原生提供的操作 DOM 的方法代码十分冗长。D3 提供了一种声明式的方法 d3.select(query)d3.selectAll(query) 来选择 DOM 节点或节点集合。

方法 d3.select(query) 选中符合条件的第一个 DOM 元素,而方法 selectAll(query) 则选择所有符合条件的 DOM 元素。如果没有元素被选中则返回空的选择集。

该方法接收的入参 queryCSS 所支持的选择器,如标签选择器、类选择器、ID 选择器、属性值选择器等。

该方法返回的选择集对象(或数组)具有丰富的方法,如设置样式属性,更改 HTML 或 文本内容,注册事件监听器,添加、移除、排序节点等,这样就可以==通过链式调用的方式操作 DOM==。

js
// 操作单个节点
d3.select('body')
  .style('background-color', 'black'); // 修改 <body> 元素的背景色

// 也可以操作节点集合
d3.selectAll('p')
  .style('color', 'white'); // 修改 <p> 元素的字体颜色

第二个操作等价的原生操作

js
const paragraphs = document.getElementByTagName('p');
for (let i=0; i<paragraphs.length; i++) {
  const paragraph = paragraph.item(i);
  paragraph.style.setProperty('color', 'white');
}
提示

在 D3 中大部分情况下,在设置属性值时除了支持传递静态常量值,还支持传递函数,返回动态计算得到的值,例如 D3 的图形模块 shape 提供了相关函数,基于数据计算出 <path> 元素的属性 d 的值,用于绘制折线图。

js
// 为每个段落设置随机颜色
d3.selectAll('p').style('color', function() {
  return 'hsl(' + Math.random() * 360 + ', 100%, 50%)';
}

数据绑定

由于 D3 在设置 DOM 元素的属性时,支持通过函数计算动态值,如果将图表数据作为参数,就可以实现数据「驱动」元素 Data-Driven Documents

在 D3 中提供方法 data() 可将数据与元素进行绑定,默认根据索引 join-by-index 将 DOM 节点(选择集)和数据(以数组的形式列出)一一对应(即数据数组的第一个元素与选择集的第一个节点绑定),然后在使用函数设置 DOM 节点样式属性时,分别将对应的数据作为第一个参数 d 传递到设置函数中,动态求出属性值,这样就实现了数据驱动文档 Data-Driven Documents。

js
// 根据数据为段落设置不同的字体大小
d3.selectAll('p') // 选择所有 <p> 元素
  .data([4, 8, 15, 16, 23, 42]) // 将数据与 DOM 元素绑定
  .style('font-size', function(d) { return d + 'px'; }); // 根据数据动态计算出每一个 <p> 元素的字体大小
提示

当 DOM 节点和数据绑定后,后续如果通过 D3 操作该 DOM 节点时(如更新 DOM 属性样式),D3 可以从选择集中再读取绑定的数据,即 DOM 节点一旦绑定了数据就会带有状态,选择集添加一个名为 __data__ 的属性,这样就不需要不断地进行数据的映射。

为了跟踪 DOM 节点,便于将它们分配到不同的选择集,绑定数据时一般还会提供一个 key 函数 selection.data(data, keyFunction),其返回值一般是字符串,如地名、id 等,作为 DOM 节点和数据的匹配依据(而不用默认的按照索引顺序 join-by-index 进行配对)

html
<div id="Ben"></div>
<div id="Tom"></div>
<div id="Jack"></div>
<div id="shouldBeDeletedNode"></div>

<script>
const dataset = [
  {name: 'Ben', number: 4},
  {name: 'Tom', number: 8},
  {name: 'Jack', number: 15},
];


d3.selectAll('div')
  .data(dataset, function(d) {
     // key 函数返回数据的 d.name 作为标识,如果节点没有被绑定数据就返回节点的 id 属性作为标识
     return d ? d.name : this.id;
   })
  .text(function(d) { return d.number; });
</script>

增删元素

一般情况下在 D3 中将节点与数据绑定时,选择集合中的节点和数据数组的元素一一匹配,但可能会出现节点和数据元素个数不匹配的问题。

针对这个问题,D3 提出 3 个概念:

  • 如果 DOM 节点多出来,则未绑定数据的节点会进入名为 ==exiting 选择集==(准备从页面「离去」的节点,一般在后续操作中删除)
  • 如果数据元素多出来了,则对应多出来的占位节点(虚拟节点)会进入名为 ==entering 选择集==(准备「进入」页面的节点,一般会在后续操作中实例化这些 DOM 节点,并插入在页面的相应位置)
  • 可与数据对应上的 DOM 节点,进入名为 ==updating 选择集==,它是默认选择集,即 data() 方法返回的对象就是 update 选择集(而 enter 选择集和 exit 选择集需要调用该对象的 enter()exit() 方法才能获得)
提示

在绑定数据后,D3 没有立即更新(增删)页面节点,而是生成 3 个选择集,这样为数据可视化提供了更大的灵活度和可定制性,例如对于 exiting 选择集的节点,可以在删除时设置一些淡出的动效;对于 entering 选择集的节点可以设置不一样的颜色,高亮出来它们是新增到页面上的

旧方法 enter-update-exit

然后对不同的选择集采用不同的操作,这样就可以根据数据元素动态增删 DOM 元素,流程一般如下(以下 selection 表示选择集):

  • 移除 exiting 选择集中对应的页面上的 DOM 节点 selection.exit().remove()
  • 添加 entering 选择集中的虚拟节点到页面上 enter = selection.enter().append('tagName')
  • 合并 entering 和 updating 选择集(updating 选择集就在原来的 selection 中),这样返回的选择集与新数据一一对应 enter.merge(selection),之后可以方便地对新数据对应的 DOM 节点进行统一的样式设置
js
const circle = d3.selectAll('circle').data(anotherDataset)
    .style('fill', 'blue'); // 该样式设置只会对 updating 选择集生效

circle.exit().remove(); // 移除 exiting 选择集中对应的 DOM 节点

circle = circle.enter()
    .append('circle') // 添加 entering 选择集中对应的虚拟节点到页面
    .style('fill', 'green') // 设置 entering 选择集中的节点样式
    .merge(circle)  // 合并 entering 和 updating 选择集
    .style('stroke', 'black'); // 设置合并后选择集中的节点样式
提示

一般会在使用方法 append() 添加完节点之后,将 entering 选择集与 updating 选择集进行合并 merged,这样后续操作就可以同时应用到 enter 选择集与 update 选择集(这是旧方法,请参照下面更简洁的新方法)

新方法 join

D3 新增了一种方法 join(),它将自动对 exit 选择集中的 DOM 节点进行删除,并自动为页面添加 entering 选择集中的虚拟节点,再将 entering 选择集和 updating 选择集合并返回

js
d3..selectAll('circle')
    .data(newDataset)
    .join('circle')   // 返回 entering 和 updating 选择集的合并集
    // 然后可以对绑定了新数据的 DOM 节点进行整体样式设置
    .attr('r', radius)
提示

如果希望对 exiting 选择集、entering 选择集或 updating 选择集分别进行操作,可以在方法 join 中依此传递相应的函数

js
d3.selectAll('circle')
  .data(newData, d => d)
  .join(
    // 第一个传递的函数入参是 entering 选择集
    enter => {
      //  entering selection handler
      // 最后需要返回 entering 选择集实例化的节点,以便 join 方法最后将它以 updating 选择集进行合并
      return enter.append('circle')
    },
    // 第二个传递的函数入参是 updating 选择集
    update => {
        // updating selection handler
        update.attr("fill", "blue")
    }
    exit => {
      // exiting selection handler
      exit.remove() // 将 exiting 选择集对应的节点从页面删除
    }
  )
  // 最后 join() 返回 entering 和 updating 的选择集
  // 继续进行其他链式调用......

Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes