前端同学们对 d3 肯定是相当的熟悉了,在我们制作复杂的,重交互的图表时,常常会用到它。同时我们在使用它的时候,往往会结合 vue
/react
这类前端框架,往 d3-selection
里传送 VNode
/ReactNode
,从而达成水乳交融,灵活多变的效果。
但是 d3
整个家族是非常庞大的,光官方的子依赖包就有 30
个。这导致一开始暴露给我们的 api
就过多了,新手很难上手。于是,本篇文章就只介绍 d3
30 个子包 中的 d3-selection
,本文所有的实现只依赖此包,使用 d3
的版本为最新的 7.3.0
。
学习 d3
最快的方式就是多写多想,接下来我们来实现一下 Github ContributionCalendar
:
相信大家对这个肯定是非常熟悉了,它是用 svg
画的一个年度日历,包含 2 个坐标轴 (月份,星期几),鼠标移上去会显示一个 popper
上面会展示那一天 git
提交的次数。
流程式写法最简单易懂,因为它是顺着svg
构造一步一步来的。
首先观察图形就是一个 svg
,带了一堆的 rect
和 text
。那么我们就能写出下列代码: (代码太长做阉割处理,只展示核心部分,完整代码见附录)
// 一列 7 个正方形
const colItemCount = 7
// 创建一个 svg ,里面一个 g
const wrapper = d3
.select(boxRef.value as HTMLDivElement)
.append('svg')
.attr('width', 722)
.attr('height', 112)
.append('g')
.attr('transform', 'translate(10, 20)')
let colIndex = -1
let g: d3.Selection<SVGGElement, unknown, null, undefined>
// 一年有 365 个日出,我送你 365 个祝福
for (let i = 0; i < 365; i++) {
// y 轴的索引
const yIdx = i % colItemCount
if (yIdx === 0) {
// 当第一个的时候,指针指到第一列,添加一个 g 对 rect 进行包裹
colIndex++
g = wrapper
.append('g')
.attr('transform', `translate(${colIndex * 14}, 0)`)
}
// 正方形
g!
.append('rect')
.attr('width', 10)
.attr('height', 10)
.attr('x', 14)
// y轴偏移
.attr('y', yIdx * 13)
.attr('rx', 2)
.attr('ry', 2)
.attr('data-level', i % 4)
// 添加 class , attr('class','ContributionCalendar-day')也行
.classed('ContributionCalendar-day', true)
.each(function () {
// 鼠标移上去显示 Popper
addPopper(this as SVGRectElement)
})
}
// 添加 x 轴
addXAxis()
// 添加 y 轴
addYAxis()
这样一个简单的 Github ContributionCalendar
就完成了。
里面已经包含了 d3-selection
中大量的 api,同时也暴露出很多的问题:
render
方法,当我们改变其中的值后,再次调用重新渲染,它会再添加一个 svg
并进行一些 dom
操作。显然此时把一些 selection
缓存起来是比较好的选择。function
虽然有拆分,但是变量之间的耦合度太高。d3-selection
中除了一些选择赋值这些基础的 dom 操作 API,另一个极其重要的就是要掌握 Joining Data
,即:
selection.data([data[, key]])
selection.join(enter[, update][, exit])
data
在很多场景下极其有用,比如我们处理自引用树状结构时,就要用它结合 d3-hierarchy
一起使用。
join
本身就是 enter
和 exit
,在回调部分通过数据的变化对选择集合进行归类,并执行自定义回调。
datum
则可以从现有的数据集合中抽取更改链式调用的数据源。
接下来我们就可以对刚才的代码进行改造:
首先这个图形看上去就像一个二维数组,它有像 count
,level
,date
,index
,id
这样的属性,分别用来表示,提交的次数,像素块的亮度,提交的时间,像素块的索引,渲染的唯一 key。 于是我们就能预先准备这样一笔数据了:
type MatrixItem = {
id: string
index: number
level: number
count: number
date: string
}
let matrix: MatrixItem[][]
// then fill the matrix
matrix = fillMatrix(seed)
接着让这个二维数组,来接管我们 svg
的渲染:
// render 时接管 svg 的宽度
const width = Math.floor(matrix.length + 1) * 14 + 24
svg.attr('width', width).attr('height', height)
wrapper
.selectAll('g')
// 注入数据
.data(data)
// 二维数组 第一层,选中 MatrixItem[] , enter -> g
.join('g')
.attr('transform', function (d, i, g) {
return `translate(${(i + 1) * 14}, 0)`
})
.selectAll('rect')
// 二维数组 第二层,选中 MatrixItem
.data((d) => d)
.join(
// enter
(enter) => {
// 添加小方块
return enter
.append('rect')
.attr('width', 10)
.attr('height', 10)
.attr('x', 0)
.attr('y', function (d, i) {
return i * 13
})
.attr('rx', 2)
.attr('ry', 2)
.attr('data-count', function (d, i) {
return d.count
})
.attr('data-date', function (d, i) {
return d.date
})
.classed('ContributionCalendar-day', true)
.each(function (d, i, g) {
addPopper(this as SVGRectElement)
})
.attr('data-level', function (d, i) {
return d.level
})
},
// update 时 level更改,进行变色
(update) =>
update.attr('data-level', function (d, i) {
d.level = (d.level + seed) % 4
return d.level
}),
// exit 时,移除 dom
(exit) => exit.remove()
)
效果如图所示:
需要好好理解的是 join
这个概念,这里需要贴一段原文自行体会一下:
D3’s data join lets you specify exactly what happens to the DOM as data changes. This makes it fast—you can minimize DOM operations—and expressive—you can animate enter, update and exit separately. Yet power comes at a cost: the data join’s generality makes it hard to learn and easy to forget.
就个人理解而言,这个 join
的本质就是一个 diff
。每次在执行的时候,它会和现存的 node
集合进行比较,根据绑定的情况,把它们归为三类:
update = new Selection(update, parents);
update._enter = enter;
update._exit = exit;
return update;
来交给后续的 join
(enter
/exit
) ,执行它们的回调方法。值得注意的是,data
是否声明 key
,走的绑定方法(bind)是 2 个不同的分支:(bindIndex
,bindKey
)。后者会创建一个 nodeByKeyValue
的 Map<Key,Node>
结构来缓存原先的数据映射。
就这样 Selecting Elements
,Modifying Elements
,Joining Data
的部分,通过这个示例已经很明白了。
出于篇幅限制,下一篇文章,我才会讲解一下极其重要的 Handling Events
,Control Flow
,Local Variables
,Namespaces
等等内容。
其中 Handling Events
也是很重要,经常结合其他包使用,比如 d3-zoom
最后说一句,笔者随缘更新....