初识 d3 selection

d3.js 是一个用于数据图形化的 javascript 库, 有点类似于 jQuery. 与 jQuery 不同的是, 它除了能操作 dom 之外, 还具有数据绑定的功能, 这对于动态数据的展示非常友好.

刚开始看 d3 的文档可能会比较难以理解, 因为官方文档是按照模块一个一个排列展示的, 这种方式对于已经了解 d3 的人比较友好, 可以直接查看想要的模块, 但是对于初学者来说,因为模块之间并不是渐进的关系, 所以并不利于形成初步认识.

这篇文章让我们先来认识一下 d3 selection 模块, 这是一个非常重要的模块, 我们用例子的方式一步一步展开, 这对初学者来说无疑是最友好的.

在开始 d3 之前我们先用 js 来创建一个 svg 图形. 因为 d3 本质上只是 js 的封装.

用 js 画一个柱子

在学习 d3 之前我们需要简单了解一下 svg 的知识, svg 的属性与 html 稍有不同, 以下面的例子为例, 我们要画一个矩形就要用到 rect 元素.

<rect width=20 height=100 x=20 y=0 fill='green' />

svg 的知识不在本篇的重点讨论范围内

与 html 一样, js 可以创建和修改 svg.

const svg = document.querySelector('svg');

const ns = 'http://www.w3.org/2000/svg' // namespace
const rect = document.createElementNS(ns, 'rect')
rect.setAttribute('width', 20)
rect.setAttribute('height', 100)
rect.setAttribute('x', 50)
rect.setAttribute('fill', 'green')

svg.appendChild(rect)

用js画柱子

用 d3 画一个柱子

同样的例子也可以用 d3 来操作, 直接看语法就能明白它在做什么.

const svg = d3.select("svg");

svg
  .append("rect")
  .attr("x", 20)
  .attr("y", 0)
  .attr("width", 20)
  .attr("height", 100)
  .attr("fill", "green");

d3.selectdocument.querySelector 类似返回第一个匹配项. 对应的也有返回所有匹配项的方法 d3.selectAll.

d3.select/selectAll 方法返回的是一个 selection 对象. 这个对象继承了很多 方法, 比如 append , attr 等.

调完 appendattr 等这些方法后又会返回一个新的 selection 对象, 因此它们是可以链式调用的.

假设 html 上有两个 svg 元素, 每个 svg 元素里又有两个 g 元素.

<html>
  <svg>
    <g />
    <g />
  </svg>
  
  <svg>
    <g />
    <g />
  </svg>
</html>
// 代码块1号
d3.selectAll("svg") // 返回 selection 对象, 其对应内容是多个 svg
  .selectAll('g') // 返回 selection 对象, 其对应内容是多个 g
  .append('rect') // 返回 selection 对象, 其对应内容是多个 rect
    .attr("x", (d, i) => i * 50) // 修改 rect 元素属性, 返回 selection 对象, 其对应内容是多个 rect
    .attr('width', 20) // 修改 rect 元素属性, 返回 selection 对象, 其对应内容是多个 rect
    ...

执行上面的 代码块1号, 页面上会出现 4 根柱子.

4个柱子

光看这个代码理解起来好像很简单, 先选择多个 svg 元素, 再选择 svg 下面的多个 g 元素, 然后给每个 g 元素插入 rect 元素.

这看起来很酷, selectAll 似乎帮我们做了循环的事情, 不需要 forEach 就创建了多个元素, 这让我们的代码在使用层面看起来非常简洁.

很好奇这个 selection 对象的数据结构是怎么样的? 每次 selectAll 后它是如何变化的? 让我们来深入看一下这个 selection 对象.

selection 对象

以这个例子为例, 我们把每一次选择拆开来, 然后打印出来看看这个对象是什么:

<!-- html -->
<svg class="first" width=200 height=200 >
  <g />
  <g />
</svg>
<svg class='second' width=200 height=200 >
  <g />
  <g />
</svg>
// js
const svgs = d3.selectAll('svg') // 第一步

const groups = svgs.selectAll('g') // 第二步

const rects = groups.append('rect') // 第三步

rects.attr("x", (d, i) => i * 50)
  .attr("y", 0)
  .attr("width", 20)
  .attr("height", 100)
  .attr("fill", "green")

console.log({svgs, groups, rects})
  1. 第一步, d3.selectAll('svg'): 第一步
    如截图所示, d3.selectAll('svg') 后返回的就是一个纯粹的 js 对象, 它看起来很吓人, 其实就是两个键值, 分别是 _groups 和 _parents.
    _parents 和 _groups 有对应关系, _parents[i] 是 _groups[i] 的父元素.
    对于第一步来说, _parents[0] 是 html.
    _groups[0] 是一个数组, 里面是选到的两个 svg 元素 (它们是纯粹的 js dom 节点)

  2. 第二步 svgs.selectAll('g')第二步
    到了第二步, 选择范围是基于第一步中的 _groups 来选择的. 所以第一步中的 _groups 变成了第二步中的_parents.
    同样的 _parents[i] 是 _groups[i] 的父元素, 它们是对应关系.

  3. 第三步 rects.append('rect')
    第三步
    第三步是在第二步的基础上 append rect 元素.
    append 方法也是返回一个 selection 对象. 它会给每个选择的元素里添加一个新的元素.

    append 方法会有点不一样,它会遍历 _group 中的元素,给它们添加进元素然后再返回成 selection 对象。文章最后我放上了 append 方法的源码解读, 会有点绕, 但是没关系, 会一步一步的往下看, 这对理解 selection 是有帮助的. 有兴趣的可以看一下.

    但是不管怎么说我们已经明白了 selection 对象的大体结构了. 它把选择的元素放在了 _parents 和 _groups 数组里, 并且维护了他们之间的父子关系.

data 数据绑定

在上一个例子中我们通过 selectAll 选中了多个 g 元素, 然后给他们添加了 rect 元素. 这个看起来已经比用 js 简化了, 但是前提是需要在 html 中先添加若干个 g 元素. 也就是说 selectAll 需要选中已经存在的元素.

假如我们有一个数组 data = [10, 20, 30], 我们想画三个矩形, 高度分别是 10, 20, 30.

最简单的我们可以用 js 循环遍历的方法来画它们.

const svg = d3.select('svg')
data.forEach((d, i) => {
  svg.append('rect').attr('height', d).attr('x', 25 * i).attr('width', 20)
})

除了这个还有没有其它的方法来实现呢? d3 为我们提供了数据绑定 data 方法.

d3.select('svg').selectAll('rect').data(data).enter().append('rect').attr('height', d => d).attr('x', (d, i) => i * 25).attr('width', 20)

在这里插入图片描述
等一下! 页面上还没有 rect 元素怎么能 selectAll('rect') 呢,还有 data 方法是做什么的, enter 方法又是做什么的?

看一下官方文档, 文档上写了一大堆, 一下子不知道什么意思. 数据绑定其实就是把数据绑定到相应的元素上. 同时又维护了 _enter 和 _exit 两个数组, 当数据数量大于元素数量时, 往 _enter 数组里添加元素. 当数据数量小于元素数量时, 往 _exit 数组里添加要被移除的元素.

让我们先来看几个例子, 看一看 data 方法的返回值:

  1. 当数据数量与元素数量相等 ( 3 对 3, 数据数量 3 对元素数量 3 )

    <!-- html 文件 -->
    <svg width=200 height=200>
      <rect id='one' />
      <rect id='two' />
      <rect id='three' />
    </svg>
    
    // js 文件
    const data = [10, 20, 30]
    const one = document.querySelector('#one')
    const two = document.querySelector('#two')
    const three = document.querySelector('#three')
    d3.select('svg').selectAll('rect').data(data)
    
    /**
    data 方法会返回一个 selection 对象, 这个对象有额外两个键值 _enter 和 _exit:
    在这个例子里这两个数组都是空, 因为没有元素的添加和移除.
    {
     _parents: [svg],
     _groups: [[one, two, three]],
     _enter: [,,],
     _exit: [,,],
    }
    
    同时每一个 rect dom 节点会挂载上 __data__ 数据:
    one.__data__ = 10
    two.__data__ = 20
    three.__data__ = 30
    **/
    
  2. 当数据数量大于元素数量 ( 3 对 2, 数据数量 3 对元素数量 2 )

    <!-- html 文件 -->
    <svg width=200 height=200>
      <rect id='one' />
      <rect id='two' />
    </svg>
    
    // js 文件
    const data = [10, 20, 30]
    const one = document.querySelector('#one')
    const two = document.querySelector('#two')
    
    d3.select('svg').selectAll('rect').data(data)
    /**
    返回:
    {
     _parents: [svg],
     _groups: [[one, two, ,]],
     _enter: [,, EnterNode],
     _exit: [,,],
    }
    _enter 数组中有新元素加入, 因为数据的数量比元素数量多一个
    
    one.__data__ = 10
    two.__data__ = 20
    **/
    
  3. 数据数量小于元素数量 ( 2 对 3, 数据数量 2 对元素数量 3 )

    <!-- html 文件 -->
    <svg width=200 height=200>
      <rect id='one' />
      <rect id='two' />
      <rect id='three' />
    </svg>
    
    // js 文件
    const data = [10, 20]
    const one = document.querySelector('#one')
    const two = document.querySelector('#two')
    const three = document.querySelector('#three')
    d3.select('svg').selectAll('rect').data(data)
    /**
    返回:
    {
     _parents: [svg],
     _groups: [[one, two]],
     _enter: [,,],
     _exit: [,,Three],
    }
    _exit 数组中有要移除的元素加入.
    
    one.__data__ = 10
    two.__data__ = 20
    **/
    

附上 data 方法的测试用例, 看用例其实比官方文档还要清晰.

回到上面的例子, 我们可以把语句拆开来执行:

 <!-- html 文件 -->
 <svg width=200 height=200>
 </svg>
const data = [10, 20, 30]

// 3 对 0, 数据数量 3 对元素数量 0, 对应上面的第 2 点, _exit 数组里会有三个元素。
const rects = d3.select('svg').selectAll('rect').data(data) 

// enter() 方法能够拿到 _exit 数组的内容并转化成 selection 对象
rects.enter().append('rect').attr('height', (d) => d))

所有 selection 方法的返回值都是一个 selection 对象, 所以能链式调用.

元素移除
在上面的基础上如果 data 又变成 [10, 20], 那么移除的元素会添加进 _exit 数组, 用 exit 方法可以拿到.

rects.exit().remove()

看一下这个例子, 我写了一个循环来模拟数据的不断变化, 利用 enter 和 exit 方法就可以很好的展示这些变化.
在这里插入图片描述

动画
在此基础上我们还能给进入的元素添加 x 方向的动画, 给移除的元素添加 y 方向的动画来更好的区分.
在这里插入图片描述


这看起来很酷 !

现在让我们更进一步.

上面的例子我们只关注了 enter 和 exit 也就是添加和移除的元素. 更新的元素我们并没有考虑, 假设数据的变化是这样的:
在这里插入图片描述
如果用上面例子的代码, 我们会发现第一个柱子的高度根本没有变化.

这是自然的, 因为我们只用了 enter 和 exit, 那更新的元素怎么来控制呢? 其实更新的元素就是 data 方法的返回值本身. 可以观察上面 1, 2, 3 例子中的 _groups.

所以再加上一行代码就可以了

rects.attr('height', (d) => d)

在这里插入图片描述
让我们加入高度的动画

rects.transition()
     .duration(800)
     .attr('height', d => d)

在这里插入图片描述

Join 方法
d3 还提供了一个 join 方法, 相当于是 enter, update, exit 的合并简写形式.

svg.selectAll('rect').data(data).join(
  enter => ...,
  update => ...,
  exit => ...
)

data 数据绑定的高级形式 key 函数

从上面的例子可以看出, data 方法默认是按照数组的顺序来绑定元素的. 数组的第一位绑定第一个元素, 第二位绑定第二个元素.

但是有些情况按照数组顺序的绑定方式是实现不了的.

比如有一个数组 [10, 20, 30] 对应了三个元素 one, two, three

<svg>
  <rect id='one' />
  <rect id='two' />
  <rect id='three' />
</svg>

我想要移除掉 two 元素, 该怎么办呢?

我可以让数组变成 [10, 30]. 但是由于绑定是按照顺序绑定的, 所以最后只会移除掉最后一位的 three 元素. 而 two 元素会被保留, 并且它的的 __data__ 会变成 30.

那怎么办呢? 就需要用到 data 方法的第二个参数, 叫做 key 函数.

这个 key 函数光看文档很难理解它在干什么. 大概意思就是返回一个唯一 key 值, 函数的第一个参数是绑定的数据 d.

看一下这个例子, 我把数据结构改成对象数组: [ { name: ‘one’, value: 10 } ], key 函数用 (d) => d.name

在这里插入图片描述
字面意思是用数据中的 name 来绑定元素。

初看起来没有什么问题.

但其实 key 函数还是有点复杂的, 这个例子没有问题是因为它的 rect 节点是从无到有的, 因此每个节点上都有绑定值.

但是有些情况 d 可能是 null.

让我们再来看一下 key 函数的源码 😅. 请再保持点耐心, 快结束了. 这对理解 key 函数非常有帮助.

<!-- html 文件 -->
<svg>
  <rect id='one' />
  <rect id='two' />
</svg>
const data = [{name: 'one', value: 10}, {name: 'two', value: 20}]
d3.selectAll('rect').data(data, (d) => d.name)

假设我们调用了这个 data 方法, 让我们来看一下在源码中是如何运行的.

// 仅为示意
// 代码解释写在注释中

/**
selection 对象如下:
{
  _groups: [[one, two]],
  _parents: [html]
}
**/

// 遍历 _groups, 然后调用 bindKey 方法
for (var m = _groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {
  bindKey(...)
}

function bindKey(parent, group, enter, update, exit, data, key) {
 /**
 parent: html
 group: [one, two]
 enter, update, exit 为数组
 data: 调用 data 方法的第一个参数, 即 [{name: 'one', value: 10}, {name: 'two', value: 20}]
 key: 调用 data 方法的第二个参数(key 函数), 即 (d) => d.name
 **/ 

 // 定义一些变量 
 var i,
    node,
    nodeByKeyValue = new Map(),
    groupLength = group.length,
    dataLength = data.length,
    keyValues = new Array(groupLength),
    keyValue;

  // 遍历 group 中的节点.
  // 维护 key 和节点的对应关系
  for (i = 0; i < groupLength; ++i) {
    if ((node = group[i])) {
                     // 执行 key 函数 👇   意味着当 group 中有节点时, 初次执行 key 函数时 node.__data__ 为 null, 因此 key 函数要注意判空.
      keyValues[i] = keyValue = key.call(node, node.__data__, i, group) + "";
      if (nodeByKeyValue.has(keyValue)) {
        exit[i] = node;
      } else {
        nodeByKeyValue.set(keyValue, node);
      }
    }
  }

  // 遍历 data
  // 如果 nodeByKeyValue 有对应的节点, 放入 update 数组.
  // 如果没有, 放入 enter 数组.
  for (i = 0; i < dataLength; ++i) {
// 第二次执行 key 函数 👇 
    keyValue = key.call(parent, data[i], i, data) + "";
    if ((node = nodeByKeyValue.get(keyValue))) {
      update[i] = node;
      node.__data__ = data[i];
      nodeByKeyValue.delete(keyValue);
    } else {
      enter[i] = new EnterNode(parent, data[i]);
    }
  }

  // 剩余没有对应的节点放入 exit 数组.
  for (i = 0; i < groupLength; ++i) {
    if ((node = group[i]) && nodeByKeyValue.get(keyValues[i]) === node) {
      exit[i] = node;
    }
  }
}

因此当页面有初始元素时, key 函数要注意判空.

d3.selectAll('rect').data(data, (d) => d ? d.name : this.id) 
// this 是什么? 看上面的源码能理解了.

append 方法

下面是 append 方法的源码,其实理解了 selection 对象再理解 append 就能明白了。

import creator from "../creator.js";

export default function(name) {
  var create = typeof name === "function" ? name : creator(name); // 01
  return this.select(function() { // 03
    return this.appendChild(create.apply(this, arguments)); // 02
  });
}

这块代码看起来不多, 但是我们要拆开来一步一步看.

  1. create 是一个函数, 由 creator 创建而来.

    // create 函数示意
    function() {
     return this.ownerDocument.createElementNS(fullname.space, fullname.local);
    };
    

    它能创建一个 dom 元素, 注意这个里面的 this, 在第2点里能体现出来.

  2. 第二步这里要拆成两块:
    a. create.apply(this, arguments) 把 this 绑定到 create 函数.
    b. this.appendChild() 把 create 函数生成的元素添加进 this.
    那这个 this 就很重要, this 是什么呢? 我们往下看.

  3. 第三步 this.select(fn)
    首先这个 this 是指调用该方法的对象. 假如我们调用的是 d3.select('svg').append('rect'), 那么这个 this 就是指 d3.select('svg') 返回的 selection 对象.
    再来看一下 select 方法

     import {Selection} from "./index.js";
     import selector from "../selector.js";
    
     export default function(select) {
       if (typeof select !== "function") select = selector(select);
    
       for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
         for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {
                                     // 核心代码 👇
           if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {
             if ("__data__" in node) subnode.__data__ = node.__data__;
             subgroup[i] = subnode;
           }
         }
       }
    
       return new Selection(subgroups, this._parents);
     }
    

    它对 _groups 进行了两层遍历, 因为 _groups 是数组嵌数组. 我们只需要看到那一句核心代码 select.call(node, node.__data__, i, group), 就能明白了第2点中的 this 就是这个 node, 而这个 node 就是 _groups 中的 dom 元素.

总结一下 append 方法就是遍历了 selection 对象中的 _groups, 然后对里的元素 appendChild 进新添的元素. 最后再返回一个 selection 对象以便能链式调用.

写在最后

d3 selection 模块写完了, 相信理解了 selection 模块之后就能够更好的开启 d3 之旅了. 回头再来看官方文档就能理解的更加深刻.

最后推荐一个我自已写的 react 组件库 react-admin-kit, 对于中后台系统中常见的表单和表格能够提升很大的开发效率. (文档布署在 github 可能需要 vpn)

PS: d3 文档最好也要开 vpn 阅读, 否则他的案例库 observablehq 不能完整呈现.

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
森林防火应急联动指挥系统是一个集成了北斗定位/GPS、GIS、RS遥感、无线网络通讯、4G网络等技术的现代化智能系统,旨在提高森林火灾的预防和扑救效率。该系统通过实时监控、地图服务、历史数据管理、调度语音等功能,实现了现场指挥调度、语音呼叫通讯、远程监控、现场直播、救火人员生命检测等工作的网络化、智能化、可视化。它能够在火灾发生后迅速组网,确保现场与指挥中心的通信畅通,同时,系统支持快速部署,适应各种极端环境,保障信息的实时传输和历史数据的安全存储。 系统的设计遵循先进性、实用性、标准性、开放性、安全性、可靠性和扩展性原则,确保了技术的领先地位和未来的发展空间。系统架构包括应急终端、无线专网、应用联动应用和服务组件,以及安全审计模块,以确保用户合法性和数据安全性。部署方案灵活,能够根据现场需求快速搭建应急指挥平台,支持高并发视频直播和大容量数据存储。 智能终端设备具备三防等级,能够在恶劣环境下稳定工作,支持北斗+GPS双模定位,提供精确的位置信息。设备搭载的操作系统和处理器能够处理复杂的任务,如高清视频拍摄和数据传输。此外,设备还配备了多种传感器和接口,以适应不同的使用场景。 自适应无线网络是系统的关键组成部分,它基于认知无线电技术,能够根据环境变化动态调整通讯参数,优化通讯效果。网络支持点对点和点对多点的组网模式,具有低功耗、长距离覆盖、强抗干扰能力等特点,易于部署和维护。 系统的售后服务保障包括安装实施服务、系统维护服务、系统完善服务、培训服务等,确保用户能够高效使用系统。提供7*24小时的实时故障响应,以及定期的系统优化和维护,确保系统的稳定运行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值