D3:Data-Driven Documents(数据驱动文档),是一个基于数据变换 DOM 的库。D3 认为自己填补了的一个领域是:动态图形的快速、增量更新。

原理

数据驱动

D3 的数据驱动是增量更新的。相对于同类竞品(ProtoVis),其具有明显的性能优势(2-3倍)。

工作流

当数据变化时(相关示例):

  • 判断需要新增/更新/移除哪些 Elements;
    • 遍历数组中每条数据项
      • 根据 key 函数判断 每条数据和 Element 的关联关系:
        • 如果新老数据都存在于某个 Element 相关联的数据项,则为更新;
        • 如果只有新数据存在,则为新增;
        • 如果只有老数据存在,则为移除;
  • 新增 Elements;
  • 更新 Elements:根据数据中某些属性的变化,更新 Element;
  • 移除 Elements。

    相关 APIs

  • selection.data() - bind elements to data.

  • selection.join() - enter, update or exit elements based on data.
  • selection.enter - get the enter selection (data missing elements).
  • selection.exit - get the exit selection (elements missing data).
  • selection.datum - get or set element data (without joining).

selection.join()

虽然可以自定义传入 3 种 handlers,但 joint 方法也会提供默认的处理行为,方便提高开发效率:

  1. append enter 部分
  2. remove exit 部分
  3. 合并 update 和 enter 部分,进行排序、返回。
  4. 处理 update 和 enter 部分。比如我们可以同时为 update 和 enter 部分设置属性。

示例:
image.png

关于 data() 与 joint() 方法的关系:

  • 为什么 data() 和 join() 方法几乎总是一起出现,但 D3 把它们分为两个方法?
    • 因为 join() 方法不是唯一一个可以定义“enter, update or exit elements based on data”行为的方法,还有 enter()/exit() 方法可以。
  • 为什么 “bind elements to data”行为 和“enter, update or exit elements based on data”行为总是一起出现,但D3 它们的方法分开。
    • 因为两者是比较正交的两个行为。D3 为了提高方法内部行为的高内聚低耦合,以及为了更灵活的组合调用,选择了分开定义方法。

设计理念

  • 所以 D3 选择的是,负责建立数据与 DOM Element 的绑定关系,当数据变更时,划分出新增/更新/移除的三个部分,然后交由用户决定如何更新可视化。
  • 当然它也提供了对新增/更新/移除的三个部分的默认处理方式,针对 DOM 可视化场景,默认的处理方式可以覆盖绝大多数场景,所以从最终效果看,用户侧的效率还是基本可接受的。

    具体应用

    Treemap 是怎么响应新增/更新/移除的?

  • 使用了默认的 join 机制,并没有自定义 enter/update/exit 策略。

  • 对于 enter/update 的统一处理逻辑是,设置各种 DOM attribute和text。
  • 说明该用例,它的数据变化中:
    • 新增,可以被 append DOM 和设置 DOM attribute 和 text 满足;
    • 更新,可以被设置 DOM attribute 和 text 满足;
    • 删除,可以被 remove DOM 满足。

如何实现嵌套 join?

  • 链式调用:链式的嵌套查询 + 链式的定义 data(), join()
  • data() 的参数可以是个方法,用以处理嵌套数据。

示例:

  1. const matrix = [
  2. [11975, 5871, 8916, 2868],
  3. [ 1951, 10048, 2060, 6171],
  4. [ 8010, 16145, 8090, 8045],
  5. [ 1013, 990, 940, 6907]
  6. ];
  7. d3.select("body")
  8. .append("table")
  9. .selectAll("tr")
  10. .data(matrix)
  11. .join("tr")
  12. .selectAll("td")
  13. .data(d => d)
  14. .join("td")
  15. .text(d => d);

备注:官方示例里实现 table 和 Treemap 都是只有两层嵌套结构,不过从 API 上看,应该是支持无限层次嵌套的。

Transformation

D3.js 的 Transformation 机制,大量运用了高阶函数。transformation 是贯穿整个D3 的一种机制,它被广泛地应用在 D3 的数据驱动、scale 、animation 等多种核心机制/模块之中。

架构

架构现状

内核层:核心原理,包括表示模型和数据驱动。

  • selection: 负责查询 DOM Elements.
  • Operators:负责操作 DOM Elements.
  • Data joins:负责维护 数据和 Elements 的关联关系,以及提供增量更新机制(数据驱动)。
  • transitions:负责实现动画过渡效果。基于 Operators 和 Data joints。

辅助层:基于内核层,针对一些常见的具体可视化任务,进一步封装简化 API,以提高开发效率。

  • layouts:声明式的可视化组件。比如 Treemap、Geographic Cartogram、Force-directed graph 等。

    设计理念

  • 表示透明:直接使用 W3C 的 DOM 作为表示模型,而未引入额外的(工具特定的、上层抽象的)表示模型。

    • 这和我们之前选择的「在渲染层,不对 Three.js 现有的表示模型进行额外的封装,而直接使用,或至多进行少量横向扩展」的思路,是不谋而合的。
  • 增量更新:以达到动态变化时的高性能。
  • 数据驱动:将具体的如何做到最小化增量更新的任务转交给了用户开发者。
  • 声明式的高层组件:用于针对特定任务提高开发效率。
  • 内核层负责保证性能和灵活性,辅助层负责针对性地提高开发效率。

    扩展方式

  • 基于 selection 和 transformation 机制向上封装帮助模块。包括:

    • 封装声明式的可视化组件。比如 Treemap。
    • 通过 enter/update/exit 处理函数,可实现用户自定义的增量更新。
    • 将扩展的 transformations 方法作为插件方法。比如, d3-geo-projection)。并且当按照 D3 官方推荐的方式实现插件包时,新增的方法会被挂在到 d3 对象上。比如:

d3.foo();

应用方式

在最终的应用项目中,存在哪些有代表性的应用方式:

  • 直接使用内核层,即使用 selection,transformation 等过程式编程;
  • 使用声明式的可视化组件;

参考资料