D3:Data-Driven Documents(数据驱动文档),是一个基于数据变换 DOM 的库。D3 认为自己填补了的一个领域是:动态图形的快速、增量更新。
原理
数据驱动
D3 的数据驱动是增量更新的。相对于同类竞品(ProtoVis),其具有明显的性能优势(2-3倍)。
工作流
当数据变化时(相关示例):
- 判断需要新增/更新/移除哪些 Elements;
- 遍历数组中每条数据项
- 根据 key 函数判断 每条数据和 Element 的关联关系:
- 如果新老数据都存在于某个 Element 相关联的数据项,则为更新;
- 如果只有新数据存在,则为新增;
- 如果只有老数据存在,则为移除;
- 根据 key 函数判断 每条数据和 Element 的关联关系:
- 遍历数组中每条数据项
- 新增 Elements;
- 更新 Elements:根据数据中某些属性的变化,更新 Element;
-
相关 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 方法也会提供默认的处理行为,方便提高开发效率:
- append enter 部分
- remove exit 部分
- 合并 update 和 enter 部分,进行排序、返回。
- 处理 update 和 enter 部分。比如我们可以同时为 update 和 enter 部分设置属性。
示例:
关于 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() 的参数可以是个方法,用以处理嵌套数据。
示例:
const matrix = [[11975, 5871, 8916, 2868],[ 1951, 10048, 2060, 6171],[ 8010, 16145, 8090, 8045],[ 1013, 990, 940, 6907]];d3.select("body").append("table").selectAll("tr").data(matrix).join("tr").selectAll("td").data(d => d).join("td").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 等过程式编程;
- 使用声明式的可视化组件;
