动态折线图开发问题汇总

最近在给业务方开发动态折线图,期间遇到N多问题,这里做个记录,就当是技术复盘了,避免下次重复踩坑。

出现问题,先盯着问题看10遍,把问题明确了、想清楚了,把接下去应该如何调试的步骤想清楚了,再去动代码调试。

花在思考上的时间越多,后面花在盲目调试上的时间就越少。

需求列表

折线图

  • 找产品经理拿测试数据
  • 确定数据结构
  • 公司logo的实现(如何解耦?组件化的思维、装饰器模式,参考蔡东的思路),这个也关系到数据结构(这个必须继承+组合,否则之前的组件会受影响没法用)
  • 没有logo的时候的展示方案:首字母-业务方自己实现
  • (P1)logo是个formatter-整个div都是自定义的
  • 数据较少的情况下,动画是否不连贯?需要做一个插值处理
  • (P1)文字重叠的解决方案
  • (P1)注意下方的图,坐标是反向的;加个reverse参数
  • Y轴刻度值是在图表内部显示的
  • (高风险,大活儿)缺少tooltip的样式和交互逻辑,是长按还是按上去就出现?-参考ECharts的来,不延迟展示
  • 没有滑动跟手(touchmove)的展示交互
  • axisPointer的线条是虚线
  • tooltip的节流
  • (高风险)tooltip没有自动位于绘图区域内部-需要一个边界检测通用方法
  • axisPointer没有每条线都绘制
  • 两图合一后,0刻度的值和刻度线需要删掉一个才行,trick删除吧
  • 所有横线都很长,通过transform: scaleX(1.5);实现
  • 0刻度值不显示“亿”字
  • 播放过程中,只显示logo,不显示数值,播放完毕才显示数值
  • 单独搞个插件文件,做这些trick操作吧
  • 隐藏/显示坐标轴和刻度的配置项
  • 将写死的chart移出去,增加chart的position检测
  • x轴线会被刻度线遮住的问题;每次刻度线变更后
  • Y轴0刻度没显示-业务方自己设置显示什么范围、显示哪几个数值
  • 小心纵坐标取整的问题,提供一个默认的取整策略
  • 横坐标的线宽度是很长的-这个太另类了,业务方自己划一根背景色
  • 有个问题,0轴如果用同一个,下面的线条会和X轴刻度值重叠
  • 按照UI稿还原样式
  • tooltip需要有,回调数据记得排序
  • 黑白风格-业务方自己修改吧
  • 背景刻度线和grid是虚线
  • 圆点的层级不是最高的
  • X轴没显示刻度-是我字体设置太小了
  • 圆点的颜色是和线条一致的
  • (重要)内存泄漏问题
  • (重要)各种测试,自己各种使用,参考我们的自测checklist
  • 操控栏(播放、重播)
  • iOS10和Android5测试
  • 无动画模式(默认进来就全部绘制好了)
  • 坐标轴的计算,我放在logo.html中了,得移动进代码中
  • DynamicLine中的逻辑下移,这里只做流程管控
  • 将代码改为游戏loop循环的机制
  • 做个在线页面,直接导入数据,生成动态图表,并且可以有一个链接
  • 比例尺移出去
  • 混乱的数据结构,压根没有数据结构(帧数据结构、整体的数据结构。。。。。。);得改为TS。。。
  • 1、我的Debug模式呢?VConsole、Stat.js
  • 2、增加通用的SVG和DOM的样式配置工具-抽象为函数,这个很通用的,SVG和DOM有哪些属性上的差异?,我传入一个option,自动生成右侧配置面板!
  • 3、上传Excel查看效果
  • 4、我的武器库,拆分细粒度单元和算法
  • 如何解耦?尽量不用state;将dynamic的this作为参数传递到函数内部,比如scale
  • 处理事件交互的不可见矩形也可以移动出去

开发反馈的问题-2022.03.16

  • 第一帧会停止一段时间
  • tooltip的触发,不是在两帧之间的位置,只要小于下一帧位置就会触发上一帧-反向比例尺的二分查找,没有考虑做中间距离的判断
  • tooltip的显示位置,没有和手炒一致,即手位于左边就显示右侧、手位于右边就显示左侧
  • tooltip改为手按住一直展示,离开后就马上消失
  • tooltip无法选中最后一根-是因为反向比例尺缺少最后一个导致的
  • tooltip和axis轴上下互通
  • x轴刻度值内侧对齐
  • 左侧坐标轴文字的层级问题,会被axisPointer遮住

开发反馈的问题-2022.03.17

  • 初始会停止一个周期的时间-去掉playByDate的强制延时
  • 两帧之间的文本数值,增加一个插值运算:我又想错了,这个应该是数据和绘制分离,不应该直接在Anchor中写的;还是应该在Line中计算好数据,然后扔给Anchor只负责绘制;我之前有个做得不错的地方,就是数据是作为参数传递给anchor的update方法的,这样解耦了数据计算和渲染
  • chart为了两个合一起,设置了一个top值,需要动态计算-将图表的top设置为grid.margin的上下外边距之和即可
  • 绘图不从0开始,靠右一点;目前x轴的位置实际上是没对齐的;需要设置4个地方:x轴的axis的offset、endPointer的formatter中的margin-left、series.line.style.transform、postProcess中把X轴0轴线左移
  • 中间某一帧的折线会一下子画完了,和playByDate去掉了setTimeout有关系;还是loop的问题-增加50ms延时来解决
  • 手机上折线总是突然就出现了,没有动画;会不会是date计算出错了?算到下一个日期了?但是也不会没动画呀;是哪里触发了什么绘制函数么?需要把每个元素的绘制独立开,不然找问题要从一大堆东西里面找,很麻烦,效率极低;感觉是因为计算耗时的问题,手机性能比PC差,算得慢,所以出现这个问题;PC性能好,所以没这个问题;和我的插值可能有关系,改为300ms就没出现了;不过低端手机上可能还是会出现;就第一帧算不赢

是stroke-dashoffset插值后的值变成0了

和我用Promise有关系么?宏任务、微任务?

开发反馈的问题-2022.03.21

  • 最后一个点对不上

产品反馈的交互-2022.03.23

1.折线图增加播放器,按年度划分,其中

  • 播放器样式参照行业对比,显示初始年份,播放年份。注意点:因为选中股票上市年份为非固定,所以不可按照行业对比一样十等分,需要按照实际年份等分
  • 播放过程中可暂停,可前后年份拖拽定位,但拖拽时无过场动画。举例,从2021年拖回到2015年,折现不会回缩,手指松开后点直接从选中年份开始。
  • 另页面内“融资展开收起”、“tab切换”、“切换股票的”、拖拽年份后播放停止。操作完成后需要重新点击才可继续播放

2.折线图页面融资象限默认收起,可点击展开按钮后展开,展开后为分红融资双象限模式。分红象限无需展开收起操作。

  • 若融资为收起状态下,浮窗仅显示分红数据,如“2010年分红”,无需展示融资数据
  • 派现能力排名TAB下优先展示“累计净分红”(“派现融资比”可通过右上角tab调出),在“累计净分红”下添加播放组件。播放器默认年限上限为10年,不足10年按照实际年份等分。“派现融资比”无需加播放器。播放时未上市股票显示“未上市”,同行业对比一致。
  • 加入Timeline控件
  • 封装为一个更高层的组件

开发反馈的问题-2022.03.25

  • X轴0轴遮挡住了文字-给文字元素设置个positon:relative,然后设置下z-index即可
  • 数值比较集中时,5次碰撞检测不够:是0轴的文字重叠了,沃日
  • 初始几个周期没数据的点,也会绘图
  • logo的示例,碰撞检测的结果不美观,应该居中才合适
  • 程序设计有问题,动画播放过程中如何直接调用playByDate()跳转到其他地方?

开发反馈的问题-2022.03.26

  • 指南针这只股票的碰撞检测还是不对-是不是Y比例尺计算有误?怎么指南针跑到0轴下面去了?下午去隐藏下面的线看看,是比例尺不对,还是div样式有问题,还是换行了?
  • anchor圆点没了
  • 轴线把折线遮住了:层级问题出现很频繁,得有个设计原则来解决!应该设计**生命周期(看下Unity的那个生命周期图)**,明确在框架画完的时候,再设置顺序!等等,我记得这个顺序只针对同一个父元素下有效,那么我得设置父元素的才对。

不对,这是因为两个图叠加导致的,这个似乎没法改了;可以修改,将第二个绘图的div设置z-index就可以了。

  • X轴0轴虚线和实线重合了,应该是没有加postProcess

  • pointer在跳转到具体某个日期后,还会播放动画

开发反馈的问题-2022.03.29

  • 组件不支持传入dom对象,同一个页面绘制多个图形时会出问题
  • 暂停的时候,也需要增加碰撞检测,否则文本重叠很严重
  • 股票名称带星号,会报错:这是因为我把股票名称作为css选择器的一部分了;改为转换为ASII码

开发反馈的问题-2022.03.30

  • 2.7.1引用报错:是因为我build的时候给js文件名加了hash值,导致找不到入口文件了
  • 数据只有一个时报错:程序没判断,直接取了frameData[1]
  • 线条跑到绘图区域外面去了
  • 播放完毕后,再点播放,之前的线条没消失:没有清除

开发反馈的问题-2022.03.31

  • Y轴的刻度线没法延长和删除了:是因为动态变更Y坐标轴后,没有在每次轴范围变更后删除线条和延长线条;这个无需修改组件,修改postProcess()的调用时机即可
  • 柱状图的右下角label和滑动条的pointer样式不对:是因为我写了很多通过id定位的情况。。。
  • 还有一个季报不支持,导致我们这边梳理的宏观数据出现1年对应多个数据的情况无法展示。我们现在只能搞一个GDP的。
  • 折线图点击上一期下一期,文字会重叠:因为没有判断数据缺失的情况
  • 柱状图绘制多个图表时,defaultDate不生效;后面的timeline的update会影响前面的对象

开发反馈的问题-2022.04.01

  • 柱状图的tooltip无法显示
  • 柱状图的北美风格的名称位置如何调整?
  • 时间线的刻度,已经播放完的刻度线样式可以自定义
  • 折线图在PC下的tooltip的定位模式需要修改下
  • 删除axis的ticks的line,会导致播放后全部ticks都没了:

陈锦宏反馈的:

1.配置坐标轴和折线使用axis和series数据结构和传统echarts类似,比较清晰。
2.两个图表叠加问题。在echarts是可以通过配置在一个dom实现。目前的话是通过两个dom绘制两个图表再通过d3api去合并。使用d3的api会提升
使用成本。
3.timeLine这个对象内置会好一点,现在每次重新绘制也要再初始化一次timeline,这个放在内部更新好点。
时间轴向外暴露事件即可不需要绘制dom的api,可以利用webcomponet在外部写一个通用的组件作
参考。

总结:目前如果单个图表是能通过配置项解决,但叠加图的话有点像是业务人员在外拓展,定制化。如果可以的话利用配置项向外拓展功能。
时间轴的初始化可以在内部消化,不需要在外部调用。时间轴向外暴露一些基本的数据和事件。时间轴UI层提供通用的webcomponet组件。不要混在图表功能内。

开发反馈的问题-2022.04.02

  • 只有一条线,且前面几个值都是0的时候,报错了

  • 不支持常规日期,只支持数字年份

timeline优化

  • playing的状态,不应该由timeline关注,应该组件自己搞定这个播放中的状态切换操作,即可以从任何一个周期的任何一帧,切换到另外一个周期的动画
  • 队列应该移动到组件内部,不是timeline维护

让杰哥慢慢接手起来吧,我腾出时间,不然要GG了

柱状图

  • 支持所有公司都展示logo(不改,前端反馈这个组件和和行业对比保持一致,不做额外样式)
  • 右侧显示logo

程序设计

灵魂拷问

你设计分组了么?

你创建g对象了么?

你的位移是操作g对象么?

数据结构

先确定名词概念:

  • 周期
  • 循环loop
  • 周期数据
  • 常量定义

折线图

不合理的地方:

Line和Area不应该只有一个,应该是有多个实例。

数据管理应该放DynamicLine中,包括回调参数的管理

归根结底,还是数据和渲染没分开的缘故;应该在DynamicLine中处理好所有数据才对

动画部分单独抽离出来

chart元素需要设置position:relative,然后将用户自定义元素设置为position:absolute

tooltip和axis轴上下互通

  • 暴露axisPointer的显示和隐藏方法,参数是date
  • 用户可以自己注册touchmove事件,装饰器模式
  • 只显示第一个图的tooltip,第二个图的隐藏掉
  • 禁用默认的tooltip事件,完全由画布自己注册事件,传入事件event,通过event获取x坐标,调用axisPointer和第一个tooltip
  • axisPointer

待解决的问题

廖造光:

1
2
3
4
5
6
7
8
9
1.横向柱状图:股票名称在柱左,logo在柱右
--回复不改,前端反馈这个组件和和行业对比保持一致,不做额外样式
2.只用黑底的
--回复,同类型和先上线的行业对比保持一致,如果行业对比改成黑的,我们就不做白底的了,如果行业对比适配或者还是固定白版的话,分红也做黑白版
3.(折线图)上下同时走,间隔需要前端调试
--回复,是一起动的,折线加载效果3-5秒比较好
4.折线自左向右推进不能一卡一卡的
--回复,技术预演是顺畅的,要完全保险需要开发可视化自己的组件
5.其他细碎点略

不能做的事情

发布NPM包不能加hash后缀

组件打成NPM包之后,加载是根据package.json中的main配置的路径去寻找入口文件的,因此build时加上hash值,就会导致组件找不到入口文件。

现在改为新起一个script,专门用于发布npm仓库时的打包。

可操控的时间轴类组件,动画不应该放在具体组件层

否则用户在动画播放过程中操作,你就很难处理了。

不能通过唯一ID或者样式去选择DOM元素

因为用户实际应用场景,可能存在一个页面绘制多个图形的情况,比如结果页。

时间轴不能用线性比例尺,必须用scalePoint

否则无法适应季度、日期等形式

经验总结

移动端事件和PC端的差异

touchmove和mouseover

移动端获取touch的坐标:

1
2
event.touches[0].pageX;
event.touches[0].pageY;

PC端:

1
2
event.pageX;
event.pageY;

数据计算和渲染分离

在做两帧之间的文本数值时,我又想错了,这个应该是数据和绘制分离,不应该直接在Anchor中写的;还是应该在Line中计算好数据,然后扔给Anchor只负责绘制;我之前有个做得不错的地方,就是数据是作为参数传递给anchor的update方法的,这样解耦了数据计算和渲染

后来我在Line中做处理,10分钟就搞定了。。。。。

有时候效率太低,是方法和程序设计的问题。

有业务方使用和测试的阶段,基本做不了其他事情,都在答疑和解决BUG

想办法弄一下,不然没法专注做事。

今天从中午到下午5点半,自己计划的事情,啥也没写。

复现业务方的问题!!!!如何快速拿到他们的数据和配置?????????

不能复现效率太低了,要把老子整死了。。。

流程梳理

  • UI提供视觉稿
  • 开发确定数据结构,整理Excel模板给产品经理
  • 产品经理导出各种数据给开发

案例分析

引用传递导致出问题后很难排查,因为关联影响太多了

我用了太多引用传递了,而且还到处修改这些属性的值。

比如比例尺。

一旦出问题,就像打地鼠一样,很难排查;稍微修改点东西,也不知道会不会其他地方又受到关联影响,进而出错了。

程序写得好不好的判断依据之一,就是耦合度、复杂度的控制是否优秀。

3.30日排查F10问题耗费几个小时

绝大部分时间都花在了搭建环境上:

  • 我去F10搞了几十分钟,因为我的代码是NPM中的,难以调试,结果一无所获。

  • 然后让F10同事把数据和配置项上传到我的代码库,又过去了1小时

  • 我再根据F10上传的内容还原页面,花费20分钟

  • 调试问题花费大约40分钟

前面三步都是环境还原,后面一步才是调试,两者都可以极大优化。

比例尺初始设计是周期级别的,后面动态Y轴要求帧级别的

导致基本推翻重写比例尺。

另外因为之前柱状图已经在使用了,现在得维护两份了。

Timeline和图表耦合度太高,后面只能通过Timeline2来重写

又得维护两份了。

自己控制每一帧动画时,务必做好最后一帧的补帧操作

否则会因为最后一帧无法到达,而缺少部分绘图。

不对,d3的transition有end回调,已经考虑到这一点了。是我自己写错了,end中没有扫尾操作吧?

比例尺类型的选择,针对同一个维护,比如x轴,有些时候可能需要2个比例尺

比如x轴因为我们交易日可能是非连续的,所以不能用Linear比例尺;但是用Point比例尺的话,计算过渡动画的位置时,无法算出两个X坐标之间的数据,因此还得增加一个两个数据之间的小范围的线性比例尺:

1
const xPeriodScale = d3.scaleLinear().domain(previousTime, nextTime).domain(previousX, nextX);

这样就可以计算两个周期之间的过渡动画每一帧的X坐标了。

最终,我总共写了5个比例尺:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 初始化比例尺
* 而且这个不应该写在Line内部,应该是独立出去的
* 应该是需要提前处理好data数据,将数据处理从比例尺中解耦
*/
_initScale() {
this.state.xScale = this.xScale = this._initXScale();

// TODO:注意这里有风险,因为X数值可能不是一个连续数字,可能是季度(比如202203),这种情况下会有问题
this.state.xTickScale = this.xTickScale = this._initXTickScale();

this.state.yScale = this.yScale = this._initYScale(this.data);

// Y轴的帧比例尺
this.state.yTickScale = this.yTickScale = this._initYScale(this.data);

// 当前tick的终态的比例尺,用于给axis设置transition
this.state.yTickFinalScale = this.yTickFinalScale = this._initYScale(this.data);
}

过渡动画中每一帧的终点数据的补值

x和y都需要对两个动画之间的过渡数值做手动的计算,算出当前tick最后一个数据的date和value值,类似这样:

1
2
x = (current - prev) * t + prev
y = (current - prev) * t + prev

逻辑顺序导致的BUG

最好先做方案设计,把计算逻辑计算流程给画出来。

比如这里的顺序,一旦放错了就会出问题了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fixYDomain(minDomainOfY, maxDomainOfY) {
// TODO:临时处理,目前如果存在多个Y轴,每个Y轴显示的数据范围都必须是一致的
this.state.option.axis.forEach((axisConfig) => {
if (axisConfig.position === 'left') {
if (typeof axisConfig.max !== 'undefined') {
maxDomainOfY = axisConfig.max;
}
if (typeof axisConfig.min !== 'undefined') {
minDomainOfY = axisConfig.min;
}

if (axisConfig.tickNumber && axisConfig.tickNumber > 0) {
// 自动向上取整
maxDomainOfY = ceilByMinAndTickNumber(maxDomainOfY, minDomainOfY, axisConfig.tickNumber);
}
}
});
// 这个如果放错位置,就出问题了
if (this.state.option.reverse) {
const tmp = maxDomainOfY;
maxDomainOfY = minDomainOfY;
minDomainOfY = tmp;
}

return {
minDomainOfY,
maxDomainOfY,
}
}

没有测试异常数据的情况

包括:

  • 负数值
  • 部分周期的数据缺失
  • 同一周期的不同数据,量级差异巨大

删除刻度的g下面的line,下次call不会重新绘制line的问题

怀疑是:

1
this.group.call(this.axis);

这个如果发现tick没了,就会重新绘制;如果发现tick还在,就会重复利用。

所以:

1、我直接删除class=tick的g,然后call(axis),发现没有tick了,就重新绘制了,所以下一次仍然是全的tick

2、我只删除line,下次call(axis)时,发现tick是有的,就不再重新创建tick的g了,因此g内部的line也不会创建,就没有了

没有考虑到只有一条线、前面数据全为0的情况

导致比例尺的最大值为NaN了

这种边界判断,是我的弱项。

不支持季度、日期格式的date

柱状图本身似乎没问题,主要是时间轴?

因为时间轴不是线性的?错了,是因为我用了++的缘故,这样季度++肯定不等于下一个季度啊

  • 时间轴的刻度值画得太多了:应该是用了线性比例尺,改为Point比例尺即可;timeline的入参要修改下,改为传入所有数据,排序后的所有数据;另外我不该对传入的x日期格式做任何处理;数字、文本,都应该是可以的;顺序也是可以自定义的才对
  • 柱子右侧的文本全部都是0
  • X轴的轴线数据也是0

弱类型语言带来的大坑

比如用户传入的date,可能是字符串,也可能是数字。这样我各个地方的判断就会很坑,比如这个:

1
2
3
4
5
6
// 每次步进的回调
onStep(date, data, container, divContainer, scale) {
dateIndex = dates.indexOf(date);

// other actions
},

dates是从用户的数据中筛选的date,可能是字符串;onStep回传的date则被我处理成数字了,这样就导致在dates中找不到这个date,dateIndex=-1了。

这里其实我不应该将date转为Number,这样限制了用户的自由度,比如“2022-04-05”这样的日期就没法用了。

必须要增加更加完善的类型校验,组件内部一定要用TS来写!

常见问题的排查方法

线或者anchor位置不对,飞出去了

这种情况一般是Y坐标值算出来是负数,基本都是比例尺不对,包括:

  • 比例尺没有正确在transition中变更,导致用了上一次的比例尺,范围是上一次的,比较小, 所以这一次的数据算出来后,点的坐标都成为负数,跑到最上面去了

部分线条没画出来

可能是数据对应的color没配置导致的

defaultOption的闭包导致的问题

应该通过工具排查问题,比如Chrome的debugger断点,可以查看闭包情况。

另外debugger可以一步一步跟踪变量情况,这样效率才最高。

TODO LIST

感觉4月份能搞定这些就不错了。

这就是积累“二十年的功夫”,所以这不是一朝一夕可以完成的,得持续改进。

清明节要搞定的

照着Games104的第三课抄!

ECS架构:https://zhuanlan.zhihu.com/p/30538626

我先把ECS的关系图画出来,先想清楚通信机制,以及状态机,再去实现。

  • 【Warning】组件和Timeline的X轴都不支持季度、YYYY-mm-dd格式的日期
  • PC端的timeline的事件bug
  • 柱状图的tooltip无法显示
  • 柱状图的北美风格的名称位置如何调整?
  • 时间线的刻度,已经播放完的刻度线样式可以自定义
  • 折线图在PC下的tooltip的定位模式需要修改下
  • 负数情况的虚线框展示功能
  • 人口金字塔的方案设计
  • TickLogic和TickRender没有抽象出来,应该放在基类中
  • 数据结构没整理出来
  • 独立出DataSet类
  • debug模式改为url传参来判断
  • 【Warnning】line.js中,默认把两个x之间的间隔时间写死为1年了
  • Demo文件整理下,太乱了
  • 没有做到数据和视图完全分离
  • enter、update、exit没应用好(压根没应用)
  • 项目结构优化下,比如data的加载、访问地址不应该有dist,清理无用的文件;参考下vue的项目
  • 【Warning】自定义的内容中如果存在图片,每一帧都会去重复加载:要么将logo和文本分开成2个配置(这个似乎好一点,对应用方更方便);要么想个不每次重新创建dom的方法
  • tooltip的定位,需要更多的类型,可以是跟随触摸点,也可以是固定左右

可以靠后处理的

  • 碰撞检测不理想
  • Y轴0轴要不要靠底部,需要改为通过配置项来设置;目前写死了
  • 动画interval设置过短时,前面的比例尺和动画的transition不执行或者掉帧的问题
  • 封装为vue组件,方便接入LowCode平台
  • 改为TypeScript
  • 底层绘图库不可切换(d3应该只做计算,底层可以切换到ZRender)
  • 用map、reduce等等简化代码
  • 计算移入webworker
  • 单元测试
  • 支持CDN形式的引用
  • 代码生成器/框架生成器
  • 引入vite