前言
Echarts
的文档像 ppt,让人跃跃欲试。
D3
的文档像是满黑板的数学公式,很多人被吸引,很多人直接被劝退。这让
D3
一直是处于“钟无艳”的地位。
D3 - 入门文档
官方文档
- D3 官方文档: d3/d3
- D3 官方示例: 这是最好的学习资源,你可以从这里进行复制+粘贴,快速上手Observable / Observable
中文翻译
- D3 文档中文翻译: 下面我收集的一些中文文档,记录在 notion 笔记中:- D3js: 数据驱动文档
第三方
- 数据可视化教程: freeCodeCamp 提供很多交互式演练场,免费帮助对计算机感兴趣的人自学编程 freeCodeCamp.org 数据可视化教学
- awesome-d3: 想了解一门技术,直接上 github 搜索 "awesome-xxx" ,准能找着许多有趣的东西wbkd/awesome-d3
- 探索 d3:
这是上面 👆 开源项目的站点,帮你更快速的搜索可视化示例 d3-discovery.net
以下篇幅,我以初学者的身份,面向文档开发,探索 D3
的的“奥秘”
D3 - API 概览
高度模块化
通过
npm info d3
查到 d3 在 npm 库中的版本信息和依赖组成。我们可以看到它在上个月已经发布了 7.0 版本,其中 30 个依赖项都是 d3 自己维护的子模块。个人统计,d3 这个核心库,它的全局命名空间下,现在至少挂载了 550 个 api 方法或常量值,内部高度模块化。
API 模块展示
参考 d3.pack
,我制作了一个 d3 api 可视化。d3 modules vue (📦codepen)
D3 - 获取数据
当我们做数据可视化,要做的第一件事是什么?
没错,正是获取数据。
前端有很多途径获取数据
- 直接硬编码在程序里,比如声明一个 JSON 结构的变量,或者 mock 方式模拟数据
- 通过调用后端接口,后端返回数据库里经过处理后的数据给前端
- 直接从静态文件中读取数据(json,dsv,xml,svg,image,txt,buffer,blob)
d3-fetch
d3-fetch
是一个很小巧的模块,它对原生 fetch
函数进行了一层简单封装,用途是从静态文本文件中读取数据。
- json
JSON (JavaScript Object Notation )是现在最常用的前后端数据交换格式。它灵活,可读性强,支持嵌套结构。 d3-fetch JSON (📦codepen)
- dsv DSV (Delimiter-Separated Values)分隔符值文本。这种文本格式是对数据库中表结构的模仿,数据的第一行用来定义表的列(columns),剩余行都是用来表示数据(data)。
常见的 dsv 格式文本有 csv(逗号分隔符值)和 tsv(tab 分隔符值)。d3.dsv
还支持指定其他的分隔符来加载、解析成 JSON 格式数据,基于 d3.dsv
方法,d3-fetch
提供了单独的 d3.csv
和 d3.tsv
方法。
顺便说一下,dsv 文件的解析,依赖了 d3 的另一个模块 — d3-dsv
。这也体现了 d3 高度模块化设计哲学。
- 其他
除了 JSON 和 DSV 格式,
d3-fetch
还提供了svg,image,blob,buffer 和 xml
的加载方法。它们都很简单,每个方法都是 10 行代码左右,基本上都是接收两个参数,一个是 url,另一个是 fetch 的配置参数,返回值是Promise
。
应用
d3-fetch
小巧玲珑,简单的业务场景上够用了。
但有时问题就在于它太小了,在应对生产环境上对请求有特殊需求(如超时处理、拦截器、取消请求等)的场景,就不够打了,这时建议自己封装 xhr,或者选择专业的 ajax 库比较好。
D3 - 处理数据
现在我们拿到的一手数据,但是往往还需要再加工一下,对数据进行一些变形和筛选,才能用来做可视化的数据结构。d3-array
就是这样的一个模块。
题外话 🔕复杂和高开销的数据算法:高开销的数据处理,就应当在数据给前端之前就处理好,因为它会拖慢甚至拖垮用户的浏览设备,你可能无法想象用户的设备性能有多低。
d3-array
- 基础数学统计
这里的基础数学都是小学的数学内容,都是很好理解的。
d3.max()
, d3.min()
,d3.extent()
, d3.mean()
, d3.median()
, d3.quantile()
, d3.variance()
, d3.deviation()
是常见的数学处理函数,依次分别表示获取数组中的最大值、最小值、值域、平均数、中位数、分位数、方差、偏差(方差的算数平方根)。
d3-array (📦codepen)
上面的基础数学统计函数,都支持额外的一个“访问器” accessor
函数,类似 Array.map ,方便从一个对象数组中提取其中的数值。
- 处理数组顺序
d3-array
还提供了很多方法,方便我们按照特定的规则,获取指定元素在数组中的位置索引。
-
-
d3.least、d3.leastIndex
🆚d3.greatest、d3.greatestIndex
这两对函数是相辅相成的,理解其中的一对就够了。就说
d3.least
它返回数组中最小那个元素,d3.leastIndex
返回数组中最小的那个元素的索引值。
-
-
d3.bisect
系列
bisect
是“二分法”的意思,d3.bisect
方法接收一个已经排好序的数组,以及一个数值,返回该数值在数组中适当的插入位置。 -
d3.ascending
和d3.descending
这是一对比较器函数,用于传递给 Array.sort 方法,去处理数组元素排序。 它们的实现也很简单
function ascending(a, b) { return a == null || b == null ? NaN : a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN; } function descending(a, b) { return a == null || b == null ? NaN : b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN; }
但说到底,还是要先掌握 JS 内置数组方法 ,这些方法足够可以应付八成以上的问题。
-
数据转换 数组转换涉及到“线性代数”和“行列式”的数学知识领域了。就像下面这些:
-
- 指定数组中的一个 key,以其进行分组: 其中 d3.group() 会返回一个分组后的 Map 格式的元素, d3.rollup() 则返回 Map 格式元素的索引。
-
- 返回两个数组的笛卡尔乘积: d3.cross() , 接收两个数组作为集合,看下面扑克牌的示例就容易懂了
- 返回两个数组的笛卡尔乘积: d3.cross() , 接收两个数组作为集合,看下面扑克牌的示例就容易懂了
-
- 重新排列: **d3.permute() ,**可以根据索引排序数组,也可以根据 key 排序对象
-
- 打乱数组: d3.shuffle() 接着上面 cross 的示例,实现洗牌
- 打乱数组: d3.shuffle() 接着上面 cross 的示例,实现洗牌
-
- 创建一个等差数列: d3.range() , 这个在制作刻度轴或者比例尺的时候会常用到
-
- 矩阵行列转置: d3.zip() , 接收多个数组参数
随机数
有时数据可视化可能需要一些随机因素 - 比如你想生成一些测试数据。d3-random
提供了比 Math.random
多得多的随机因子生成方式。
D3 - 操作 DOM
DOM(Document Object Model)是网页上的元素树。
D3 名称是(Data-Driven Documents)的缩写,即数据驱动文档。D3 和 jQuery 是同年代的产物,那时候的原生 DOM API 还没有 querySelector
和 querySelectorAll
, 如果要你想选择文档,需要记住几个又臭又长 getXXXbyYYY
的 api(懂得都懂,不懂的也不要去懂它们了🥱),这是一件很痛苦的事情,这时候 d3-selection
就英雄登场了。
d3-selection
d3-selection 模块提供了一个 d3.select()
方法,对标原生的 document.querySelector()
。
原生 api 返回的是选中元素本身,而 d3.select 是创建了一个 selection
对象,该对象上绑定了很多辅助函数,作用匹配到的第一个元素。
模块还提供了一个 d3.selectAll()
方法,对标原生的 document.querySelectorAll()
。selectAll
。依旧是返回一个 selection
对象,只不过它上面的辅助会作用所有选中的元素。
用 d3-selection
操作 DOM 会是这样的,告别了种类繁多的选择器,告别了循环体,这就是初代目的数据驱动,链式调用就很酷。
import * as d3Selection from "https://cdn.skypack.dev/d3-selection@3.0.0";
const scope = d3Selection.select("#demo");
scope
.select(".demo__button")
// 支持多种事件
.on("touch click", () => {
const paragraph = scope.select("p");
const array = [1, 2, 3, 4, 5];
paragraph
.style("color", "cornflowerblue")
.style("font-style", "italic")
.style("font-weight", "bold")
.selectAll("span")
.data(array)
.enter()
.append("span")
.text((d) => d);
setTimeout(() => {
const newArray = ["你", "好", "世", "界"];
paragraph
.style("color", "tomato")
.selectAll("span")
.data(newArray)
.text((d) => d)
.exit()
.remove();
}, 2000);
});
d3 selection 对象
调用 select
或者 selectAll
后,生成的一个 selection
对象,该对象上提供了一系列可以进行链式调用的方法。
- 处理 dom 事件:
.on()
&.dispatch()
监听事件 & 发布事件d3.pointer()
&d3.touch()
两个函数可以接收 js 事件对象,返回触摸事件对象,便于做移动端的事件兼容 - 处理 dom 属性:
.attr()
&.style()
&.property()
- 处理 dom 内容:
.text()
&.html()
- 处理 dom 排序:
.sort()
&.order()
&.raise()
&.lower()
- 处理 dom 动效:
.transition()
迎接新时代
题外话:D3 和 jQuery 的选择器设计,改变了当初的游戏规则,最后慢慢变成了 DOM 标准。看看 Web Components 标准, 我似乎望见了
R-V-A
三大框架的最终归宿 🤔。 现在是虚拟DOM 的主场,d3-selection
的用途也有所示弱,很多场景 jsx 和 vue 的模版语法,使用起来会更加方便和直观。这看个人喜好了,你用或者不用,它都在那里,不离不弃。
未完待续 TODO
还剩下一些篇幅,以后再补上吧,我自己还在边看文档边学 Notion 原文稿