大家好,欢迎来到IT知识分享网。
d3-selection 入门篇(1),制作响应式 Github ContributionCalendar
前言
前端同学们对 d3 肯定是相当的熟悉了,在我们制作复杂的,重交互的图表时,常常会用到它。同时我们在使用它的时候,往往会结合 vue/react 这类前端框架,往 d3-selection 里传送 VNode/ReactNode,从而达成水乳交融,灵活多变的效果。
但是 d3 整个家族是非常庞大的, 光官方的子依赖包就有 30 个。这导致一开始暴露给我们的 api 就过多了,新手很难上手。于是,本篇文章就只介绍 d3 30个子包 中的 d3-selection,本文所有的实现只依赖此包,使用 d3 的版本为最新的 7.3.0。
写一个简单的Demo
学习 d3 最快的方式就是多写多想,接下来我们来实现一下 Github ContributionCalendar:
相信大家对这个肯定是非常熟悉了,它是用 svg 画的一个年度日历,包含 2 个坐标轴(月份,星期几),鼠标移上去会显示一个 popper 上面会展示那一天 git 提交的次数。
开始撰写
流程式写法
流程式写法最简单易懂,因为它是顺着svg构造一步一步来的。
首先观察图形就是一个 svg,带了一堆的 rect 和 text。那么我们就能写出下列代码: (代码太长做阉割处理,只展示核心部分,完整代码见附录[1])
// 一列 7 个正方形const colItemCount = 7// 创建一个 svg ,里面一个 gconst wrapper = d3 .select(boxRef.value as HTMLDivElement) .append('svg') .attr('width', 722) .attr('height', 112) .append('g') .attr('transform', 'translate(10, 20)')let colIndex = -1let 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,同时也暴露出很多的问题:
1.假如这是一个 render 方法,当我们改变其中的值后,再次调用重新渲染,它会再添加一个 svg 并进行一些 dom 操作。显然此时把一些 selection 缓存起来是比较好的选择。2.function 虽然有拆分,但是变量之间的耦合度太高。
数据驱动式写法
d3-selection 中除了一些选择赋值这些基础的dom操作API,另一个极其重要的就是要掌握 Joining Data,即:
•selection.data([data[, key]])•selection.join(enter[, update][, exit])
data 在很多场景下极其有用,比如我们处理自引用树状结构时,就要用它结合 d3-hierarchy[2] 一起使用。
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 matrixmatrix = fillMatrix(seed)
接着让这个二维数组,来接管我们 svg 的渲染:
// render 时接管 svg 的宽度const width = Math.floor(matrix.length + 1) * 14 + 24svg.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[3]
最后说一句,笔者随缘更新….
附录
@d3/selection-join[4]
Thinking With Joins[5]
selection.data source[6]
selection.join source[7]
Demo folder[8]
References
[1] 附录: #addition
[2] d3-hierarchy: https://www.npmjs.com/package/d3-hierarchy
[3] d3-zoom: https://www.npmjs.com/package/d3-zoom
[4] @d3/selection-join: https://observablehq.com/@d3/selection-join
[5] Thinking With Joins: http://bost.ocks.org/mike/join/
[6] selection.data source: https://github.com/d3/d3-selection/blob/main/src/selection/data.js
[7] selection.join source: https://github.com/d3/d3-selection/blob/master/src/selection/join.js
[8] Demo folder: https://github.com/sonofmagic/icebreaker.top/tree/dev/packages/%40som/vue2/src/components/Github
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/58889.html