(TODO)技术拆解-时间轴柱状图
最近业务方有一个需求,需要一个展示排名变化的时间轴柱状图。之前我们用网上一个开源的代码做过类似的功能,但是当时只是为了满足一次性需求,代码写得非常的定制化,不够通用。考虑到这种时间轴动态图的需求应该后续还有不少,因此这次我准备借助这个需求,在这个开源项目的基础上进行代码优化和封装,使其更加灵活、更加配置化、支持的功能更加丰富。
基本信息
项目是在一个开源项目基础上加工而成的,技术方面采用了 D3.js
这里有个魔改项目,有一些程序说明信息:https://github.com/TangliziGit/ColumnsAnimation,仅用200多行代码就实现了大部分核心功能,可以参考下。
(TODO)这里有个 Observer 上的 D3 作者写的基于Scrubber的示例,可以好好琢磨下:https://observablehq.com/@mbostock/bar-chart-race-with-scrubber
技术点
D3.js、SVG
把这几个属性精通后,动画就比较好弄了吧:
transform, x, y, width, height, fill-opacity, class(带有颜色类), text, stroke-width
数据结构
这个项目的数据结构比较简单,就是普通的柱状图数据结构,再加上时间属性,这里就略过了。
页面元素拆分
| 模块 | 元素 | 交互 | 动画 | 实现方案 |
|---|---|---|---|---|
SVG 元素的结构
摘自魔改项目作者的说明:
barEnter就是柱子,可以重点看一下这个对象相关的设置。
1 | |
流程图
请求数据
整理数据(排序、格式化、转为 key-value 等等)
每一帧中:
算出当前需要展示的数据、下一次需要展示的数据
根据上一步的内容,设置 transition 的动画参数
动画
横向延长柱子
这个应该容易,变更数据即可。
可以查看 barUpdate 这个对象,这就是需要更新数据的柱子。
是怎么对这些柱子分组的?即如何将其标注为需要更新?
柱子上下翻飞
算出下一个位置的排行,根据行高调整 y 的数值?
通过 change()这个函数,计算上下偏移的数值,然后通过transform来实现的:
1 | |
代码总结
很多代码其实是设置样式,是可以独立出来的。
配置项也可以做个层级拆分,不要只是一级。
代码没有做函数封装,基本上一个流水账直接下来。
我对于 D3 和 SVG 画基础的柱状图的元素,还不是很熟悉。
1 | |
更改记录
25 号下班前提供一版,包含核心功能。
27 号再提供一版,包含所有功能。
把每个作图模块拆分开
工欲善其事,必先利其器。
一定要注意页面元素初始化的顺序,这个也可以弄个经验流程图
感觉要坑,这个拆分远比我想象的耗时。先只拆分我需要修改的几项吧,其他的靠后拆分。
基本上每一项拆分都要耗费 1 个小时,远比我想象的慢;不过后面越来越快了,形成套路就好了。
画个模块拆分和命名的图,一图看懂概念和组件设计!
BUG-提前一点
内存泄漏
刚开始是 140M 左右,后面会升到 300+M;动画播放完毕后,静止状态内存仍然会缓慢上升,可以重点看下 loop 里面的逻辑。另外该释放的变量就主动释放掉。
如何快速定位内存泄漏问题?可以单独写个经验博客。
(Done)当前日期动画没完成,点跳转某个日期,会出现白色柱子
播放慢也是跳转日期,白色柱子也是跳转日期,这个肯定有问题。
是因为 playByDate 主动调用了 getCurrentData,把循环打乱了么?loop 里面会调用 getCurrentData 的;应该全部都统一用 loop,不能手动调用 getCurrentData
但是我得有一个立即触发下一步动画的方法,即修改上一次的执行时间大于动画 interval,让下一次 loop 立刻执行。
【Warning】我这边有个偶现 bug,就是动态修改数据之后调用重绘,会出现图显示不出来的问题,但是左上角的 Y 轴单位可以正常显示
我看此时 svg 里面 g 的大小为 0*0
调用 resume 可以修复,那 resume 干了什么事情?
resume 就是修改了 playable,是因为这个在有些情况下没被重新设置的缘故么?
(Done)重头播放经常出问题,白色柱子
是因为同时调用 loop 冲突了么?
playByDate 里面调用了 getCurrentData
是因为当前日期的动画没有完成,就开始调用 resume 了;
得搞个动画的回调函数,我现在加了这个,还是有问题,会停顿一会
得有个清空当前计数器,直接开始的功能
动画过程中点击暂停,出现过左侧 title 文字重叠的问题
(Done)切换浏览器 tab 页后,动画会乱掉
感觉是 setInterval 在切换后仍然在跑,但是动画并未画图的缘故。
可能改为 requestAnimationFrame 可以解决?
在播放过程中点击跳到某一个日期,左侧 title 文本会重叠
(Done)跳转某个日期后,点恢复,要等一阵才播放动画
看日志,是重新播放了一遍当前日期,比如 2015
交互稿的样式-乐观估计 10 小时,加上代码优化,起码两天
样式还原还有很多地方要弄,高风险啊
我 TM 太乐观了,瞎估
组员感觉不爽是有原因的。
实际上周六我花了 6 小时,周日花了 5 小时,勉强将业务需求搞定了;优化和组件抽取还没搞。
【精】动画的优化
动画应该分为两部分:
1、页面元素的切换动画
2、操控到下一个日期的动作,这个通过 requestAnimationFrame 来实现会更好一些,可以从动画里面独立出去
动画也可以分为:
1、无缝动画,即一直播放
2、有缝动画,即播完当前日期的动画,停顿一会,再播放下一个日期
这一块可以抽取出来形成一个套路和组件
左侧有一根竖线
自定义功能?缩放可能出问题
还是集成到作图组件中,配置成可显示吧
还原设计稿
(Done)顶部坐标轴的刻度文字,需要能支持设置偏移量
开放设置字体的配置项
(Done)tick 的颜色需要能够设置
(Done)自定义渐变颜色列表
有问题,如何保证相邻的 2 个柱子一定不会取到同一个渐变色?
(Done)用户需求
1、点击柱子之后需要去更改标题名称,是不是要开放一个回调或者暴露出一个全局变量
2、当前自然播放结束状态,柱子不可点击,我看自然播放结束之后 playable 依然是 true,所以点击事件 return 了
3、提供的数据自然播放结束会出现两个白色的柱子,还有三个柱子没有国家名称的,看下
4、根据当前播放状态现实操作按钮,所以需要一个全局变量或者一个 api 获取当前状态(播放中,暂停中,播放结束)
5、有没有页面重绘的方法,现在数据修改了,需要组件重新绘制
需要注意别绑了 2 次 loop;还有各个元素别初始化了 2 次
这就是为什么每个小组件都必须有 init 方法和 destroy 方法,且 init 中需要先清空现有的组件,即调用 destroy 方法
等等,不用一个个清空,直接整个清空就可以了啊
自定义每个柱子的颜色
根据 ID 做唯一标记,不可重复
圆角四个角自定义
导出 gif 和视频的功能
resize 事件
(Done)右下角自定义标签-从 text 改为 div
回调返回的数据不够,需要返回排名截取前 N 之前的、当前日期所有的数据
思考:这种和图形独立的元素,是否直接用普通的 DOM 元素更好?比如直接用 DIV。
这样对于样式、定位,都会更加方便可控一些。
(TODO)如何设置相对位置呢?不设置固定的数值,直接右对齐、下对齐,否则数字长度变更后,会超出界面范围
(Done)点击柱子的事件
注意:仅在动画没有播放的时候才支持点击事件。
选中的柱子高亮、其他柱子淡化,得记住每根柱子 hover 之前的渐变色才行;也不用吧,按照顺序来?还是绑定到柱子的属性上?
且只能在静止状态点击吧?
还得有回调函数,选中的柱子的数据、年份
(Done)鼠标 hover 柱子时出数据浮层(tooltip)-我优先级弄错了,花了大半天做了一个优先级低的功能
这个 tooltip 也需要能够自定义样式,完全开放出去吧?
(Done)可以抽取 div 自定义的配置规范和 d3 的 customDiv 函数
(Done)跳到指定某个日期的数据
这种情况得去掉动画吧
这个是高风险点
将 intervalTime 从 1 改为 0.001 就可以了,这样动画都是毫秒级别的,用户就感知不到动画了。
有时候感觉很难的事情,换个思路,就变简单了。
(Done)默认一进来就可以初始化到某个日期
new DynamicHistogram(el, data, option)
比如默认进来展示最后一个日期的静态图
为什么默认进来有个空白期间?因为第一个播放,走了 setTimeout 这个逻辑,进行了延时
(Done)样式冲突问题
把这个给忽略了
全部样式加上前缀吧
(Done)柱子之间的间隙、柱子的宽度做成配置化
将一个柱子的元素放入一个 g 中:本身就是放入了一个 g 了
宽度可以配置,但是还需优化,文字没有自动对齐
柱子之间的空隙是自动根据画布高度计算的
(Done)提供配置文档给对方开发
先只提供页面需要的那部分
(Done)回调里面把全量数据的引用返回
(Done)横坐标放到顶部
可以由用户配置方向和具体位置吧
top、right
position
坐标轴出问题了,好像是用了对数轴??
(Done)柱子、文字的大小、样式配置独立出来
搞一个通用的设置文本样式的组件函数
(Done)渐变色-线性还是?
从左到右,和我现在的不一致
需要开放数组给对方配置
(Done)柱子右侧文本的格式自定义
需要开放函数出来
图例 legend-这期先不加
可以控制显示隐藏
用了渐变色,需要考虑下图例的颜色可能不具备辨识度了
设计师:因为我们的图支持转为静态图的,所以图例这种东西应该需要给到
时间轴
业务方自己做?
应该我们做,并且做成一个通用的时间轴,单独的一个组件
需要支持拖拽
写配置文档,提供 demo
静态图
可以选中某个柱子
(Done)发布到我的服务器测试下
D3.js 放到本地
优化:上下翻动的时候,文字样式可以先淡化,后加浓,避免重叠的时候很难看
优化:性能问题,我的手机居然卡
优化
项目不在于多,而在于精。
改为符合 ESLint 规范的格式
之前居然忘记安装 ESLint 了
Sonar 检查
有 eslint, tslint 等工具,还要 sonar 干嘛:
首先需要说的是,这两者不是一个层级的东西,eslint, tslint 是 js 代码,ts 代码的风格检查工具,其定义一些代码编写风格,主要通过这些风格规范个人的代码。而 sonar 是一个代码质量管理平台,其支持多种语言,多种检查工具,并将这些工具的结果统一化展示,比如对于 js,ts 代码,sonar 就有 eslint,tslint 等的插件可以集成进去,统一检查。
改写为 TypeScript
用 Airbnb 的插件先自动转一次
整理下这两天写组件的收获
流程、套路、规范、技巧等等
该怎么写文档呢?也给个规范吧;先看看之前我们整理的 confluence 上的文档规范
怎么还原 UI 稿子
怎么在开发过程中,方便的跟业务方对接?
我这边会持续修改配置,每次都让业务方调整,很麻烦的
组的设计:将柱子及相关的文字放到一个组中,方便操控动画
动画的操控不应该散落在各个小模块中,必须集中管理。
浏览器兼容性测试
可以支持暂停、播放、直接显示最终排名、显示某个指定日期的排名等
缺少:Legend
需要支持点击后显示、隐藏某些数据
配置顺序重新规划下
最常用到的配置提到前面来
动画参数写死了
注意:这个得优先抽离出来,不然要调死了,代码中散落很多。
需要改为配置,暴露出来。
顶部横坐标
动画的播放从 setInterval 改为 requestAnimation
多图叠加(底部折线图)
BUG:左侧文字过长,会往下掉
好像不是这个问题,待确认
logo 自定义
可以是文字,也可以是图片,目前已经支持了
足够多的 Demo
参考 ZHQ 的 Demo 页,专门搞一个页面展示各种配置出来的效果。
只专注于柱状图,其他的文字都通过自定义回调函数实现
命名全部改为驼峰,且做到见文知意
技巧
x、y、dx、dy
dx 和 dy,和 x、y 类似,除了它们的值表示的是相对于前一个字符的长度,而不是相对于整个视窗的绝对定位。
bind 的用法
组件内的 DOM 元素的定位
建议 position 都用 relative,不然就得给组件外层的 div 设置 position 属性了,不便于应用方自己控制这个 div 的位置。
一个技巧:子绝父相,套 DIV
为什么 bar 左侧的 title 文本不显示了?
看来我还是没把他画柱子的逻辑完全弄明白。
1 | |
这里本来是 state.container,我换成 this.container 就不行了。
看下transition()的 API,弄清楚具体是什么。
是因为对象的缘故吧?这个也是有范围的,绑定在哪个对象上,就只在这个对象下面的范围内查询;而我是放在了一个 g 下面,在另外一个文件中,是直接通过 d3.transition(‘2’)查的,就导致查不到?
错了,是我初始化元素的时候写错了,把 g 写成了 svg:
1 | |
SVG 元素和 HTML 元素的样式设置存在差异
比如设置字体,针对 SVG 元素,这样是可以的:
1 | |
但是针对 HTML 元素就不行了,必须写明单位,比如 px:
1 | |
其他还有更多差异待探索,感觉应该带单位的都是不兼容的;不带单位的,比如 opacity 这种,是兼容的。
另外还有一些特殊的属性,比如fill,针对 SVG 的 text 是生效的,但是针对普通 HTML 中的文本是不生效的。这部分还有待尝试探索。
注意:还有一个比较特殊的,就是坐标轴的刻度文本,颜色不能通过 fill 设置,得通过 color 设置。
d3.selectAll 不能用数字作为样式名首字母
1 | |
通过 d3.event 对象获取事件处理相关的信息
比如当前触发事件的坐标位置
比如 tooltip,一般用 d3.event.pageX 和 d3.event.pageY
坐标轴设置样式
还是没搞明白,这个太危险了,一定要抽时间完全摸透!
为什么我的 Y 轴的刻度线画不出来??
1 | |
注意:d3 画坐标轴的时候,会默认加上一个 class 为 domain 的 path 元素:
1 | |
如果想要设置该元素的样式,可以参考 d3 作者的回答,用post-selection:
https://github.com/d3/d3-axis/issues/48
1 | |
观察者模式
参考这里的 updateMaterial 方法:
1 | |
css 的样式优先级高于后续设定的样式
比如这里设置的.value,优先级就高于下面设置的 style 中的 font-size
1 | |
1 | |
局部圆角
得用 path,比较麻烦,这次就算了吧
代码拆分
先重构成 class
把 draw 改为 class,其他的先扔到 init 方法里面去?
function 先抽离出来
d3 的回调中无法使用 this 的问题
这次用到了三种方式来解决这个问题.
在回调之前缓存 this 变量
1 | |
采用全局唯一的变量来挂载数据
这次我是用了一个单独的 state 模块来挂载数据,这个有点类似 Vuex 的概念:
1 | |
这种方式其实并不好,适合用于具体的项目,不适合用于通用组件,因为这是和模块化背道而驰。
采用 bind 方法
这个是用于回调函数内部调用其他回调函数的场景,比如这样:
1 | |
配置项、数据的传递问题
多为用到了配置项,可以考虑直接用一个独立模块 option 来管理这些配置项,将用户的个性化配置追加到这个模块上。
但是这样感觉并不是最优解,因为这会导致一个全局的配置对象在各个模块之间传递,破坏了模块的独立性,让程序耦合度很高。
可以考虑通过参数传递的方式来进行一定程度上的解耦,比如这样:
1 | |
这种情况下,TypeScript 的优势就体现出来了,可以指明参数的对象类型,极大增强代码可读性。
时间统计
本来我评估的时间是 10 个小时,这是在基本理清楚原有的代码下评估的,包括所有的功能扩展和代码优化。可实际上我耗费的时间远远不止 10 个小时。
如果某个功能不是 100%做过,那么评估时间的时候就一定要画 XMind 图,把功能点、实现方案细化到极致,否则就会有风险。
“细节决定成败”这句话,在软件开发上体现得淋漓尽致。
| 分类 | 耗时(小时) | 备注 |
|---|---|---|
| 看懂代码逻辑 | 1.5 | |
| 将代码改为类 | 3 | |
| 将代码改为模块化 | 3 | 有很多需要全局传递的数据,这些数据对于模块化造成了很大的影响 |
| 将需要自定义的功能抽取为配置项 | 5 | 这个其实还没抽取完,这个属于可以一直持续优化的内容 |
| 扩展自定义文本 DIV | 2 | |
| 优化 bar 和 axis | 4 | 不熟悉 D3 的 API,造成了一定的学习成本时间消耗 |
| 修正 BUG | 3 | 因为是自己写的代码,知根知底,BUG 修复起来非常快,很多甚至能达到分钟级的修复速度 |
| 改为 ESLint 规范 | 2 | 因为一些规范不知道设计缘由,需要一边查看 ESLint 文档一边修改,耗时略久 |