一、前言
1.1、D3.js 是什么?
D3.js 是一个基于 JavaScript
开发的库,主要是用于在浏览器中操作 SVG
、HTML
、CSS
,通常我们可以利用它来进行一些图表绘制的工作。
1.2、为什么要学 D3.js
在我们学某种技术之前,最好是能够带有一些比较明确的目的,那么在学习的过程中就不容易丧失目标,最后只学到皮毛。
在我看来,学 D3
主要是有这几个方面:
- 感兴趣,希望在业余时间能够学习自己感兴趣的一些技术;同时
D3
也具有一定的复杂度,也可以拓宽自己的技术广度 D3
可以很方便的绘制一些交互式的图表,同时相比业界现成的组件库更加的定制化,可以灵活的根据自己的需求来实现一些功能- 工作中的一部分内容涉及到了图表交互,苦于一些组件库的定制化程度不高,或者是文档太过于繁杂,难以满足一些需求
因此,在这些动机的促使下我决定学习 D3
这个库是怎么使用的。
二、基础知识
2.1、常用 API
2.1.1、选择节点
API | 功能描述 |
---|---|
d3.select | 用于选中某个 dom 元素,类似于 document.querySelector |
d3.selectAll | 用于选中多个 dom 元素,类似于 document.querySelectorAll |
2.1.2、修改节点
API | 功能描述 |
---|---|
selection.text("content") | 获取或者返回当前选中节点的文本内容 |
selection.append("element name") | 添加节点到当前已选中节点的末尾处 |
selection.insert("element name") | 插入节点到当前已选中节点 |
selection.remove | 移除指定节点 |
selection.html("content") | 获取或返回当前已选中节点的 html |
selection.attr("name", value) | 获取或设置当前已选中节点的属性 |
selection.property("name", value) | 同上 |
selection.style("name", value) | 获取或设置当前已选中节点的样式 |
selection.classed("css class", bool) | 获取、删除或者添加类名到当前已选中的节点 |
2.1.3、增删节点
API | 功能描述 |
---|---|
selection.data | 将 data 绑定到节点上 |
selection.join | 基于绑定的 data 可实现 enter、update、exit 三种函数,更加精细的控制实际的效果 |
selection.enter | 获得进入的 selection |
selection.exit | 获得退出的 selection |
selection.datum | 获取/设置节点数据 |
更多 API
请参考 官方文档
2.2、链式调用
为了方便使用,D3
支持了链式调用。如上面的 API
所示,D3
里面分成这两类方法:
d3.select
/d3.selectionAll
d3.selection.function
- 其他(以后再讲)
因此,我们必须要有一个 selection
对象才能够使用其他操作方法。如:
d3.select('div')
.append('svg')
.attr('width', 100)
.attr('height', 100)
2.3、坐标系
需要注意的是,在 D3.js
绘制的 svg 中,坐标原点是在左上角的。
三、实战
3.1、第一步 —— 创建 svg
创建一个宽高为 400 x 33 的 svg
const width = 400
const height = 33
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", `0 -20 ${width} 33`);
3.2、第二步 —— 生成随机字符
定义一个生成随机字符的函数。如下所示,我们会生成随机的字符,最多为 26 个大写的字母。代码如下:
function randomLetters() {
return d3
.shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''))
.slice(0, Math.ceil(Math.random() * 26))
.sort()
}
目前到这里还算比较简单。
3.3、第三步 —— 更新字符串
根据生成的随机字符,把数据绑定到对应的节点上。其中,我们把新增、更新、删除的节点颜色分别设置为:
绿色、红色、金色
const t = svg.transition().duration(750);
svg.selectAll("text")
// 绑定数据
.data(randomLetters(), d => d)
.join(
// 设置新增的字符
enter => enter.append("text")
// 设置颜色
.attr("fill", "green")
// 设置位置
.attr("x", (d, i) => i * 16)
.attr("y", -30)
// 设置字符到节点上
.text(d => d)
.call(enter => enter.transition(t)
.attr("y", 0)),
// 设置更新的字符
update => update
.attr("fill", "red")
.attr("y", 0)
.call(update => update.transition(t)
.attr("x", (d, i) => i * 16)),
// 设置删除的字符
exit => exit
.attr("fill", "gold")
.call(exit => exit.transition(t)
.attr("y", 30)
.remove())
);
当然,这里的代码比较长并且理解成本也比较高,我们可以形成几个问题并尝试通过解决问题来理解他。
3.3.1、为什么 text
节点还没有就能够调用 selectAll
关于这点一开始我在学习 D3.js
也曾经想过,后来我去翻了下对应的源码感觉就能够 get 到了。
如下所示,我们可以看到调用 selectAll
的时候,实际上会返回一个 Selection
对象。而传入的 selector
则会被作为第一个参数接受,第二个参数是默认的 document.documentElement
。
import array from "./array.js";
import {Selection, root} from "./selection/index.js";
export default function(selector) {
return typeof selector === "string"
? new Selection([document.querySelectorAll(selector)], [document.documentElement])
: new Selection([array(selector)], root);
}
然后对象本身会保存着两个参数到 _groups
和 _parents
这两个变量中,如下所示:
export function Selection(groups, parents) {
this._groups = groups;
this._parents = parents;
}
所以这一步本身只是生成了 Selection
的实例并且把 selector
及其父节点绑定到这个实例上。
那么问题就来了:
- 这一步可以用
select
函数来替代吗? 答:不行,因为select
和selectAll
函数的区别在于:前者调用的是document.querySelector
,结果返回的是对象;后者则调用的是document.querySelectorAll
,结果返回的是“数组”。那么最后的数据结构一个是对象,另外一个是二维数组。最直接的表现是,替换之后程序马上就会报错~ - 可以用其他函数来代替
selectAll
么? 答:不行,虽然本质上只需要拿到一个Selecton
对象并且绑定数据就可以了,但是目前D3.js
并没有把这个功能直接暴露出来。
综上,我们只能使用 selectAll
来获取 Selection
对象,即使没有预先生成节点而调用而导致看起来很别扭。
3.3.2、data
函数是怎么实现数据绑定的?
提到这个问题那么就必须看一下 data
函数的内部实现了:
data
会根据传入的key
来进行绑定,调用bindKey
函数;否则则调用bindIndex
函数bindIndex
按照下标分别将节点存入update
、enter
、exit
数组中bindKey
传入的key
是一个函数,利用生成的keyValue
来将节点存入update
、enter
、exit
数组中
值得注意的是,如果需要针对 update
阶段做处理(比如说动画),那么传 key
是非常重要的。因为默认传 index
的话,即使是节点更新了,那么也有可能触发不了 update
函数。
- 返回一个新的
Selection
对象
export default function(value, key) {
if (!arguments.length) return Array.from(this, datum);
var bind = key ? bindKey : bindIndex,
parents = this._parents,
groups = this._groups;
if (typeof value !== "function") value = constant(value);
for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {
var parent = parents[j],
group = groups[j],
groupLength = group.length,
data = arraylike(value.call(parent, parent && parent.__data__, j, parents)),
dataLength = data.length,
enterGroup = enter[j] = new Array(dataLength),
updateGroup = update[j] = new Array(dataLength),
exitGroup = exit[j] = new Array(groupLength);
bind(parent, group, enterGroup, updateGroup, exitGroup, data, key);
// Now connect the enter nodes to their following update node, such that
// appendChild can insert the materialized enter node before this node,
// rather than at the end of the parent node.
for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {
if (previous = enterGroup[i0]) {
if (i0 >= i1) i1 = i0 + 1;
while (!(next = updateGroup[i1]) && ++i1 < dataLength);
previous._next = next || null;
}
}
}
update = new Selection(update, parents);
update._enter = enter;
update._exit = exit;
return update;
}
3.4、第四步 —— 收尾
Okay, 如果上面这些代码都看懂了,最后一步就很简单了 —— 添加循环。
;(async () => {
// ...
while (true) {
// ...
await new Promise(resolve => setTimeout(resolve, 3000));
document.body.appendChild(svg.node());
}
那么经过以上这些步骤,我们就实现了一个简单的 demo。通过这个 demo,可以将前面提到的一些知识点融会贯通。
四、总结
这篇文章我们花了一些篇幅来了解 D3.js
中常见的 API
及其用法。目前涉及到的部分比较少,主要是为了为下一篇带大家绘制柱状图做一下铺垫。