(精)可视化组件设计
经验之谈
数据、图形/渲染、交互
这是数据可视化的三大核心。
我们的组件设计也应该将重点放在这三块上。
生命周期的设计
应该先画图,有哪几个生命周期,每个阶段应该做什么,不应该做什么,把这个想清楚,形成原则,就是设计了,框架也就出来了。
如何管理图元的实例化:静态对象 VS. 类
静态对象+动态加载+注册+域
这是平台类(比如短视频平台)管理组件的最优解决方案,因为对平台而言,组件是独立的资产,是需要按需引入和按需加载的。
静态对象的弊端,在于它是全局唯一的,因此如果你想在一个页面上画多个图形,就会存在问题,因此我们需要引入域的概念,即对每个图形,指定一个域来区分其下的对象,保证不同图表相互不影响。
Class + 单例
这是组件库的解决方案,因为对组件库而言,你支持哪些图形元素,是预先就知道的,是固定不变的,不需要动态加载。
基于组件的程序设计
可以参考这个文章。
有几个要注意的地方:
组件的实例化不再是明文new,而是必须通过反射来加载
组件需要先注册反射机制
使用组件的实体(Entity/GameObject)必须有一个数组容器,存放所有的组件实例
组件内部需要有个entity或者gameObject属性,存放实体或者对象的引用
提升扩展性-必不可少的IoC
摘自这个文章:
https://zhuanlan.zhihu.com/p/53832991
IoC 的全称叫做 Inversion of Control,可翻译为为「控制反转」或「依赖倒置」,它主要包含了三个准则:
- 高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象
- 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
- 面向接口编程 而不要面向实现编程
依赖注入(Dependency Injection)
所谓的依赖注入,简单来说就是把高层模块所依赖的模块通过传参的方式把依赖「注入」到模块内部
use机制
1 | |
从这个方法中可以看出,要实现一个可以被 App.use() 的模块,就必须满足两个约定:
- 模块必须包含
init属性 init必须是一个函数
这其实就是 IoC 思想中对「面向接口编程 而不要面向实现编程」这一准则的很好的体现。App 不关心模块具体实现了什么,只要满足对 接口 init 的「约定」就可以了。
(精)ECS架构:实体-组件-系统
https://zhuanlan.zhihu.com/p/30538626
这个设计太经典了,对于动画类的组件,应该算是最优解了。
云风关于ECS的博文:
https://blog.codingnow.com/eo/ecs/
资料:
C#版本的Entitas:
https://github.com/sschmid/Entitas-CSharp
TS版本的Entitas:
https://github.com/darkoverlordofdata/entitas-ts
TS在线Demo(加入了dat.UI进行调试):
https://darkoverlordofdata.com/entitas-ts-example/
初级架构师的性能三要素
把数据放一起
尽量顺序访问
尽量一起读写
程序设计考虑到这三点,并将其做好,就可以成为一名初级的架构师了。
可视化设计的最优解只有一个
可视化程序设计的发展路线:计算机图形学->游戏开发->数据可视化开发
可以看到,做可视化开发的高手,都关注计算机图形学,且可能超过50%的可视化开发高手,都关注游戏领域。
好的可视化程序设计,一定有这几个元素:
- 底层技术的隔离(Canvas、SVG、DOM、WebGL)
- 图元(Element),在这一层实现事件、动画,且图元一定有Group的设计
- 一定有图层(Layer)的设计
- Chart层
归一化(normalize)
归一化就是计算单位向量
可视化页面可能会展示在不同的终端上,因此我们在计算布局和图元位置的时候,就不能写死固定的长度。所以我们引入了归一化的概念来解决这个问题。
我们将绘图区域看做一个容器,所有的元素,在计算阶段,都是以容器中的百分比作为距离衡量单位。等所有的计算都完成后,我们再用图元的归一化坐标值,乘以容器实际的高宽值,就可以得到最终这个图元在当前终端下的绘图坐标值了。
归一化可以看成是坐标系的跨终端抽象,或者我们可以将其称为虚拟坐标系。
归一化 + 比例尺 = 跨终端自适应
数据锚点
数据锚点其实和归一化是强关联的,即每组图形元素在虚拟坐标系的坐标,范围是[0, 1]。
图形元素的绘制
牢记一个原则:图形元素都是以组为单位进行绘制的。
比如我们要画一个散点图,每一个散点,虽然只是一个圆圈,但是实际绘制的时候,我们一定会用Group来画,而不是直接画一个Circle。这是为了扩展考虑的。
试想一下:如果后面需要给每个散点,在鼠标悬浮的时候绘制一个标签,如果没有这个Group的设计,你会怎么绘制?如何解决数据绑定的问题?如何解决平移缩放后这个标签和散点的跟随移动问题?
事件机制
图元模式(图形语法学)和配置模式(D3Charts)在设计上的差异
图元模式,需要传入layer,layer和EventManager绑定,在layer.add(Element)的时候,执行事件的绑定操作
配置模式,则是直接将EventManager挂到了整体的图形对象(chart)上面。
从程序设计的角度来讲,肯定是图形语法学的模式更好,耦合度更低,扩展性更强。
图层layer的设计
图层的设计很好,判断事件的时候,应该是先判断触发了哪一层,然后判断触发了该图层的哪个元素(目前XCharts没有做这个处理,叠加的情况是无法准确判断的,会点中的所有元素都触发事件)
这也是典型的Canvas对浏览器事件的冒泡捕获的实现
目前的浏览器、DOM、JS的很多实现,本身就蕴含了很强的设计,把这些设计搞懂,然后在合适的场合重新实现,比如Canvas的事件,就是很不错的设计了。
XChart的EventManager中,就是将DOM的事件都实现了一遍。
很多不同软件的设计都是相通的,比如图层的设计,在PS中就有很好的体现。
先有鸡还是先有蛋的问题
图元初始化如果没有绑定canvas,那么图元的事件注册,就必须在layer.add(element)之后才能进行,否则此时图元内部没有关联到layer,也就没有eventManager,也就无法将事件加入eventManager。
但是如果初始化图元的时候就传入canvas,那么后面还得执行一次layer.add(element),就显得冗余了。
这是图形语法学设计的一个弊端,会导致耦合度升高。
设计模式
对于每种设计模式想要解决的问题、应用的场景,如果没有真正理解,就会不明确该不该用、该用哪种,最终导致为了用而用,不解决问题。
不解决实际问题的设计模式,对项目来说等于毒药。
装饰器模式
这可以说是可视化开发中应用最多的设计模式了。
因为可视化的开发,基本上都是先确定绘图位置(数据锚点),然后在该位置上绘制图形元素,因此我们可以给数据锚点引用装饰器模式,来实现各种各样的可视化效果。
装饰器模式有个很好的地方,在于它可以针对已有的组件(即使这个组件不是你写的),扩展新的表现形式。这在应对业务方个性化需求的时候,非常有用。比如我们的散点图,默认是一个数据锚点只能画一个散点,但是我们通过装饰器模式,可以对它进行扩展,额外再绘制一个标签,展示更多信息。
多态和工厂的区别
多态一定有传入对象,是针对传入对象的类型进行判断,调用对象的方法
工厂是针对传入参数去实例化对象。
这是多态和工厂的区别,一定要牢记。
代理和AOP的区别
代理是为了让管理者可以加入自定义逻辑;代理所做的操作,一定都是用户不需要关注的。
AOP是为了让用户可以加入自定义逻辑
控制反转(IoC)
控制反转是针对小模块拼装大模块的场景,更多的是组装,与结构相关(比如组装一个汽车)。
别把控制反转和代理弄混了。
单例
单例是为了复用,比如用户切换布局。
配置项的设计
配置项的开放其实也是有套路的,比如ZRender,就是把绘图的所有CSS属性给开放出来,这样自由度就非常高了,想实现什么样式都可以。
D3Charts也是,开放的配置项很多,自由度很高,不过对用户来说,成本也会高一些。
我们将配置分类,比如基于文字的配置、基于形状的配置、定位等等,这样就容易理解了,这也属于业务建模吧。
立即触发事件和延迟触发事件(DelayAction、DelayAnimation)
之前看mir3的代码,里面通过DelayAction实现了连续型的技能效果。
ECharts也有DelayAnimation的设计,用于实现延迟动画,可以做很多炫酷的效果,比如历史轨迹。
图解方案
我们需要找个好用的画几何图形的工具,方便解释原理。
比如我画圆角三角形,就会涉及几何计算,如果没有图形作为解释,光看代码是非常难以理解的。
正面案例
分红融资旭日图
组件大致长这样:

一开始我以为要耗费不少功夫才行,不过鉴于最近做其他组件的经历,我准备先设计下数据结构,将数据处理和渲染分开。
然后我按照这个流程去做了下:
- 用Markdown罗列了所需的各种数据结构(下面只是个示例,实际上和这个有出入):
1 | |
- 代码中先设计好几个Class
1 | |
- 主流程方法中,先通过注释把要做的几个事情写出来
1 | |
这些分析好了之后,结果我居然在一个小时左右就把这个图给画出来了,效率远超我预期。
反面案例
循环依赖导致难以扩展和维护
写动态柱状图组件的时候,柱状图的配置依赖timeline,timeline的配置依赖柱状图,形成了循环依赖,结果导致很难控制,修改了一个地方,不知道其他关联地方会有什么问题,就像打地鼠一样。
一定要保证依赖的单向性,而最好的解决单向性的,就是生命周期控制(就像人的一生,是不可倒转的)+数据渲染分离。
我们做设计,很多时候都是为了避免形成网状关系;我们应该尽量形成树状关系。数据结构还是很重要的。
资料
AntV/G的设计:
https://zhuanlan.zhihu.com/p/469977477
这个文章非常好,里面有很多的设计:canvas、group、shape的三层设计,节流设计,更新队列设计,裁剪设计,拾取的分组识别,以及树的设计,很值得参考。