D3Charts-给legend实现动画分页效果
这是开发过程中记录的笔记,很乱,后面等开发完需要重新整理下。
走过的弯路
没理解数据结构
比如Group,之前都忽略了,直接导致方案设计有误,试图控制每一个LegendItem去实现动画。
没有理清楚类的继承关系和调用流程
在代码中绕来绕去,花了很久。最后自己画了一个关系图,瞬间就清晰了好多。
调试手段低效
一开始没有采用debugger,直接在代码里面埋点,效率太低了。
而且因为JS的弱类型语言特性,很多地方无法通过ctrl+鼠标点击,跟进代码内部,这又进一步增加了理解成本。
陷入代码细节
跟代码很容易陷入细节,而忽视全局的设计思路。
就比如动画,稍一思考,肯定能想到这是view层的,应该从view相关的代码中通过渲染、事件去找到实现原理。结果我调代码时将注意力都放在了代码细节上,被代码牵着走了。
另外,如果调试了一定时间(比如半小时或者1小时)仍然没有任何头绪,得赶紧停一下,要么问下其他同事,要么换个别的思路进行尝试。
一些问题
注意:要建个分支来进行开发,不要直接在主干上开发。
以ifind的需求:扩展Legend为例(Legend多的时候,可以左右滚动)。
先要明确具体需求,我这次是先参考了Echarts的效果,阅读了其文档中和该功能相关的配置项,然后去实现这些配置功能。
1、仔细阅读现有的d3charts文档,知道哪些配置是已经有了的:
http://datav.iwencai.com/platform/doc/configdoc/#legend
2、在demo/legend下面创建一个测试页面
3、查看src/component/legend的代码
流程:LegendModel处理配置信息->调用LegendView的render()方法->设置LegendItem
核心就是LegendView.render()这个方法,将其逻辑理清楚。
思路:每个图例都是一个shape,我应该根据配置,发现如果是scroll模式,则调整每个shape的位置的计算逻辑,将其展示成一个长条,不再换行;然后添加对应的翻页按钮和页码数据,并给其加上事件监听。
控制位置的,是position这个属性,其值是一个包含2个元素的数组,第一个元素是x坐标,第二个是y坐标。
LegendItem.group是什么,为什么position一直在变?打印出来看,感觉就是上一个元素的位置。这个数值达到页面边界就会被重置,因此我只需要找到这个重置的逻辑,将其改掉,这样就会将legend画成一行,不再换行了。代码在LegendItem中:
1 | |
注意:
先明确我应该控制什么内容:我应该控制的是单个元素,即item的位置数据,以及item外面的这个容器框的显示范围。
等等,好像不对,我可以控制外面整个legend的样式来实现吧?哪个更好呢?
应该是控制外面整个legend,因为有动画效果;但是好像也不对,legend宽度是固定的吧
都实现一遍好了
待解决问题:
1、移动哪个比较好
2、动画怎么解决
通过ZRender来实现,参考这个文档。
在外部拿不到LegendItem对象,因此动画只能在LegendView内部实现。
LegendView可以对外暴露几个控制动画的API,用于测试验证。
我得把上次的位置信息记录下来,便于设置动画的效果?
注意这个不是重新绘制,只是改变某些元素的位置而已,因此不能用setOption()触发。
3、如何修改item数据后重新设置进去
需要扩展下LegendView,增加position的透传。
5、如何获取legend中每个item的数据
1 | |
4、如何获取每个item的宽度(用于计算平移量,避免图例被截断)
1 | |
6、如何重新设置每个item的样式信息?
LegendView中,初始化LegendItem时,是取了LegendModel的data属性,然后将里面的每个元素这样处理的:
1 | |
可以看到根本就没有解析position数据,因此我在外部修改了legend的data数据,加了position属性,也是没用的,需要将其改成这样才行:
1 | |
然后在程序渲染逻辑中,判断如果传入了position,就不再自动计算position了。
7、view里面怎么取到每个item的shape的引用对象?
现在遇到的问题,就是修改了LegendItem,不生效,不管是直接改变值,还是通过动画来修改
等会看看ECharts文档和问问同事,别瞎折腾了
看看setShape这个方法的逻辑,我感觉是我取的对象不正确,没有真正取到图上的Shape对象
有setShape,是不是有getShape???有是有,但是也没用,和我自己从_shapeMap获取的一样
看了源码,setShape内置了动画,可以尝试下
关于数据驱动:
view里面的数据都是不能拿到的,都是通过model计算得来了,我们要操作只能操作model,这是数据与渲染分离、数据驱动的设计。
反思:我这是不了解其设计初衷,就纯面向目标的开始搞了,结果走错了。
数据驱动的代码我还没搞清楚,这一条线得画个流程图出来才行。
关于动画:
ZZY提到这个可能是默认会执行的,我先修改model,然后调试跟踪下,看看为什么没有进入动画,然后再修改对应的配置。
几个关键的类和方法:
ShapeStorage、update、model.set({})
BarView.js中有animate的参数示例
接下去:
1、尝试setShapeGroup
看BarView.js的案例,感觉有点出入,group是把全部都放进去了
还是要一个一个把position算好才行啊,算好了整体放进group中去。
LegendItem的attr()在处理动画的时候,有问题。ShapeStorage里面会调用这个方法,但是传入的animateList结构不对。
动画是在ZRender的mixin/Animatable.js里面的animateTo()方法中实现的,看其代码示例,是传入shape的属性即可:
1 | |
而我们实际上传入的是一个类似这样的数组:
1 | |
感觉结构都传错了,当然就不行了。
ZRender这个animateTo是怎么被调用到的呢?
可以看下zrender/src/Element.js:
1 | |
怪不得单个shape中没有定义animateTo方法,原来是通过call调用的。
而zrender/src/container/Group.js中,也是通过call调用Element的:
1 | |
这样的写法,单凭肉眼跟踪代码很麻烦,必须通过debugger调试才能快捷的进行定位。
好像是得弄成group才行,把结构层级简化掉。
没搞懂,感觉不大对啊
实在不行,只能去看ECharts源码了。
临死一搏,我再看看Bar的实现源码。
animateFrom和animateList到底是干什么的???
明天的工作
关于Group:
没有会自动创建Group实例,有位置信息可以移动,debugger跟踪下setShapeGroup()就可以看到了。在Transformable.js中设置位置信息(position、rotation、scale),可以看下各个属性的键名是什么。
Group 是一个容器,可以插入子节点,Group 的变换也会被应用到子节点上。应该是监控到Group有变更,就会自动修改子节点的属性,触发子节点的变更,这个设计很棒。
要理解设计、理解设计、理解设计!不然就是瞎用,进而导致错用!
那么我只需要:
1、将数据整理好,通过setShapeGroup,生成一个初始化的Group实例。
还可以手动初始化一个空的Group,然后通过add()方法把子节点加进去
用add的方式比较好,因为现在每个子节点在setShape后,需要获取其属性,计算后续子节点的位置
注意:ShapeStorage缺少一个getShapeStorage的方法
2、修改model时,触发Group实例的位置变更。
注意不是setShapeGroup,而是取到这个Group,变更其position属性的值。
2、扩展LegendModel,增加分页属性
1、了解Shape的数据结构、绘图和动画机制
要是能把数据结构相关的类的继承关系可视化展现出来就好了。
(核心)Legend的Item到底是怎么画上去的?
方案一:
我们没有实现动画,用的都是ZRender的,因此看ZRender的API去实现吧
ZRender动画API+不断设置LegendModel的option来实现动画。
方案二:
用div实现legend。这个方案对用户来说不方便。
TODO : 接下来要做的事情:
1、新增一个类似LegendItem的类,用来处理滑动按钮和页码显示
2、数据处理+事件绑定(先通过额外的按钮来处理这个)
3、调试样式
我先自己瞎写一版,然后找下Echarts关于这个动效的实现源码,作为参考,再改进我自己的代码。
我的实现方案
1、直接在现有的Legendmodel上加一些配置项
Echarts的实现方案
1、新增一个ScrolllabelLegendModel,继承自LengendModel,并扩展其配置
分页按钮的事件:
1 | |
代码对比
Echarts的小而美,不像我们一个方法一大堆
Echarts逻辑很清晰,比如render,就是先渲染内部元素(renderInner),然后确定布局(layoutInner),然后渲染整体;而且命名能体现实际含义,代码可读性强
Echarts的View里面,前面就是render方法,主要体现的是逻辑,而不是具体功能的实现代码,可以看到里面大多都是方法调用,类似于后端开发的控制器。这个设计很好。具体的代码实现,可以放后面,这样阅读体验也好,如果用不着看代码实现,那么只阅读文件最前面的内容即可。
Echarts类似建造金字塔,从最底层的Canvas,到ZRender引擎,到Echarts基础元素,再到上面的组件实现,通过
设计+工具库的方式层层支撑。要想玩的溜,必须自下而上都掌握好才行。把Echarts这个金字塔真正吸收,那么Canvas可视化所需的技术、程序设计,基本就差不多了。
Echarts里面也有很多硬编码,这是否说明一味的强调常量,不一定是好方案?
__DEV__常量的设计
Echarts的代码很多都可以拷贝来直接用(比如createPageButton()),说明其耦合度低,值得学习。
var me = this这个变量命名,绝了。我们可以用知名的电影角色来命名了。
滚动动画在哪里实现的?
Action么?不像。
1 | |
是这个eachComponent,给每个组件都触发了事件么?
跑起来,断点调试!搞啥呢
做事和搞技术的共性和区别是什么?哪个容易哪个难?
注意事项
得先弄清楚设计理念才行,不然理解成本太高了
还有名词,一些名词变量需要提前理解
Canvas的常识也要弄清楚,比如画布里面的元素的x、y值的设置
做的过程中,经常这样思考:如果我来实现这个功能,我会怎么做?然后再看具体实现的代码,和自己的想法做个验证。
有几个基础对象要摸透,比如Text,这个是引自ZRender的,是通用的文本对象。
另外我的调试方法是不对的,很低效;应该用debugger进行调试。
用debugger走一遍,清晰明了,ROI极高
TODO : 必须要把这个调试先弄熟练
另外,我又忘记了,应该先把数据模型理清楚,即model的结构
registerComponent的目的是什么?
registerClass:只是存入storage对象
后续工作
给GroupModel添加animate相关的方法
猜测应该是用类似call之类的方式
我们的代码中没收到isAnimationEnabled,找了项目下的node_modules也没有,看来是真没有。
看了ECharts源码,发现是在model中自己定义的,比如src/model/Series.js文件中就有定义。
扩展2个方法即可:
isAnimationEnabled
getShallow:这是在Model基类中定义的,用于获取配置信息;如果当前对象取不到,就取其父对象的配置:
1 | |
我们自己的Model中没有定义getShallow这个方法,抄了一半,晕倒……
还是仿照ECharts,单独扩展3个Scroll的Legend类吧
1、扩展3个类
2、给Model添加animate相关的方法,给action添加滚动事件
3、修改render方法的逻辑,实现动画和分页
4、拆分render方法,参考ECharts,拆分成不同的部分,进行渲染
思考:如何让d3charts调用到这个新增加的ScrollableLegend呢?
方案一:
Scrollable包含原始Legend的功能,在Scrollable内部判断option,做差异化渲染
这种比较好,无侵入
不过为什么legend没有画出来?经过排查,发现是我的ScrollableLegendView的TYPE属性继承错了,没有继承自ScrollableLegendModel,而是继承自了LegendModel,因此导致doRender()画图的时候找不到对应的view,所以没有画出来。
方案二:
找到调用Legend的地方,增加条件判断,初始化Legend或者ScrollableLegend
组件是怎么注册的?
组件是通过src/component/xxx/index.js中,通过registerComponent(ScrollableLegendModel, ScrollableLegendView)进行注册的。注册后会用Model.type的属性值作为组件的名称,因此初始化组件的时候,配置option的键名,就是组件的type值;后面获取model,也是通过type值来获取。
比如我扩展了一个名为ScrollableLegendModel和ScrollableLegendView的组件,其type设置为scrollableLegend,那么配置option就应该这样配置:
1 | |
获取model则写成这样:
1 | |
static怎么用?
学到一招,调用父类构造方法
动画的触发原理设计
一定要牢记数据驱动这个原则。
因此动画的触发逻辑,应该是这样的:
点击分页按钮->触发scroll事件->修改legendModel的属性->重新绘制图形->执行动画
注意核心是修改legendModel的属性,然后在重新渲染中,自动实现动画。