通过 D3 文档,学习数据可视化

1,281 阅读8分钟

Notion 原文稿 image.png

前言

Echarts 的文档像 ppt,让人跃跃欲试。

D3 的文档像是满黑板的数学公式,很多人被吸引,很多人直接被劝退。

这让 D3一直是处于“钟无艳”的地位。 image.png

D3 - 入门文档

官方文档

  • D3 官方文档: d3/d3
  • D3 官方示例: 这是最好的学习资源,你可以从这里进行复制+粘贴,快速上手Observable / Observable

中文翻译

第三方

  • 数据可视化教程: freeCodeCamp 提供很多交互式演练场,免费帮助对计算机感兴趣的人自学编程 freeCodeCamp.org 数据可视化教学
  • awesome-d3: 想了解一门技术,直接上 github 搜索 "awesome-xxx" ,准能找着许多有趣的东西wbkd/awesome-d3
  • 探索 d3:image.png这是上面 👆 开源项目的站点,帮你更快速的搜索可视化示例 d3-discovery.net

以下篇幅,我以初学者的身份,面向文档开发,探索 D3 的的“奥秘”

D3 - API 概览

高度模块化

image.png 通过 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)。

d3/d3-fetch

常见的 dsv 格式文本有 csv(逗号分隔符值)和 tsv(tab 分隔符值)。d3.dsv 还支持指定其他的分隔符来加载、解析成 JSON 格式数据,基于 d3.dsv 方法,d3-fetch 提供了单独的 d3.csvd3.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.ascendingd3.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() 接收两个数组作为集合,看下面扑克牌的示例就容易懂了 image.png
    • 重新排列: **d3.permute() ,**可以根据索引排序数组,也可以根据 key 排序对象
    • 打乱数组: d3.shuffle() 接着上面 cross 的示例,实现洗牌 image.png
    • 创建一个等差数列: d3.range() 这个在制作刻度轴或者比例尺的时候会常用到
    • 矩阵行列转置: d3.zip() , 接收多个数组参数[[1,2],[3,4]]=>[[1,3],[2,4]][[1,2], [3,4]] => [[1,3], [2,4]]

随机数

有时数据可视化可能需要一些随机因素 - 比如你想生成一些测试数据。d3-random 提供了比 Math.random 多得多的随机因子生成方式。


D3 - 操作 DOM

DOM(Document Object Model)是网页上的元素树。

D3 名称是(Data-Driven Documents)的缩写,即数据驱动文档。D3 和 jQuery 是同年代的产物,那时候的原生 DOM API 还没有 querySelectorquerySelectorAll, 如果要你想选择文档,需要记住几个又臭又长 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 原文稿

image.png