D3.js 核心概念——基本流程

2,076 阅读7分钟

这是搬运文章,请查看原文(原文会持续更新)datavis-note.benbinbin.com/article/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。

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

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

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

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 的值,用于绘制折线图。

// 为每个段落设置随机颜色
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。

// 根据数据为段落设置不同的字体大小
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 进行配对)

<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 节点进行统一的样式设置
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 选择集合并返回

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

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

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 的选择集
  // 继续进行其他链式调用......