最近接到个任务,需要用vue3实现动态折线图。之前没有用过,所以一路坎坷,现在记录一下,以后也好回忆一下。
之前不清楚echart的绘制方式,以为是在第一秒的基础上绘制第二秒,后面实验过后,发现并不是,它是每秒都重新绘制整张图。比如现在data里只有一个数据,那他就是一个点;过一秒之后data里新增一个数据,那么就有两个点,一条线段,依次类推。
话说回来,怎么实现动态的折线图,回想视频的原理,也就是一帧帧的图片快速切换,欺骗人眼产生动态的效果。所以折线图,也是如此,需要设定一个重要的maxLength,当data里的数据长度到达这个maxLength,使用shift()函数去掉data里的第一个数据,新的data数组和旧的data数组不同,这样两个画面绘制切换,就形成了动态的效果。
好了,现在有了基本理论,该干活了。任务是生成一段折线图如图所示:
t0是当前时刻点,左边实线是历史数据,右侧数据是预测数据,预测数据根据当前时刻点进行变化。但是echart没办法实现一个数据前面实线后面虚线,只能分成两个数据来拼接,假设现在只需要一个静态的画面,那么可以这样实现:建立一个数组data,[历史数据,预测数据]这样的形式划分,然后分别绘制不同的线段。但是现在需要动态展示。。。
折腾了许久,有了第一版的设计:设计历史数据只显示在x轴的2/3,预测数据全部显示,那么超出的预测数据部分就好像是对历史数据的一种预测,效果如图:
对面在看了这种效果之后,觉得还可以。我也就草草实现,没管细节了。结果过了几天告诉我,增加需求:要求可以显示距离报警时间还有多久以及报警之后还有多久报警解除。这个逻辑上是好加的,很简单。按照这样的思路:
第一如果dataPoint小于outlier且predictions数组里的值都小于outlier的时候显示“状态正常”;第二如果dataPoint小于outlier,但是predictions数组里的值有值大于等于outlier的时候,取第一个满足这个条件的值的索引x,显示“距离实际报警还有x分钟”;第三如果dataPoint大于等于outlier且predictions数组里的值都大于等于outlier的时候显示“已报警,未来持续一段时间”;第四如果dataPoint大于等于outlier,但是predictions数组里的值有值小于outlier的时候,取第一个满足这个条件的值的索引x,显示“已报警,距离警报解除还有x分钟”; dataPoint和predictions都是在updateData中定义变化的。
dataPoint表示实时值,outlier表示阈值,predictions表示实时值对应的预测数据数组。
实战显示,折线图真实情况和对应标题总是对不齐,说明这两条线有大问题。之前只是大概满足这个形式,这回需要精确到点,所以暴露问题了。
问题1:历史数据每秒增加1个点,预测数据每秒增加15个点(大于1个点)
问题2:根据echart的绘制机制,二者动起来的方式都是先到达设定的最大长度,且刚开始都是从最左边开始绘制,所以如果按照一般形式,二者根本对不齐。
这里开始尝试很多解决办法,首先是预测数据的存储方式。比如现在是
第一秒:历史数据是a,预测数据是1,2,3,那么预测队列为1,2,3;
第二秒:历史数据是b,预测数据是4,5,6,那么预测队列为1,4,5,6;
第三秒:历史数据是c,预测数据是7,8,9,那么预测队列为1,4,7,8,9;
以此类推,对应算法:
保留原预测数据的第一个值,加上新的预测数据if (currentState.currentResponse === null) {// 第一次接收数据currentState.currentResponse = response;allPredictedData.value[extractedName] = [...response];} else {// 将前一次的response第一个值添加到历史currentState.prevFirsts.push(currentState.currentResponse[0]);// 更新当前响应currentState.currentResponse = response;// 合并历史数据和新数据allPredictedData.value[extractedName] = [...currentState.prevFirsts,...currentState.currentResponse];}
由于数据预测的折线图是子组件,所以传值也是个问题。之前值都是存在子组件的,但是这个样子和父组件失去同步,因为父组件有个实时值显示,模拟真实情况传感器的数据。如果分开的话,父组件的值都走完了,点开数据预测界面发现从第一秒开始,那么就露馅了。由于父组件不是我写的,所以看起来也是脑袋大。
可算改完了,这回实时值和预测值都由父组件提供,子组件只负责数据的显示。蛋疼的是数据预测是向服务器请求的,所以其实实时值和对应的预测数据如果不加处理,无法直接对其,故将实时值的时间戳和对应的预测数据保存,子组件通过监测道名称和时间戳获取对应数据。
const currentTimestamp = item.data[currentIndex.value]?.timestamp;allPredictedData.value[extractedName].push({timestamp: currentTimestamp,prediction: response
});
这么尝试多次后(一两天把。。),发现问题仍然无法解决,一度怀疑是不是echart的问题,还是什么玄学。后面静下心来想想,在纸上debug了一下,理清逻辑之后发现问题所在。设计出来了最终版:
报警时间预测不准的原因是因为两个问题:1.二者起始没对齐 2.每秒增加的点数不同。
所以想到了使用填充的方式来使二者在开始对齐,右边是历史数据的方式。
第一步:获取监测道的全部数据(模拟传感器的实时数据,所以是有全部数据)
第二步:打开折线图的时候,获取当前点的dataPoint;
2.1 结合data,从dataPoint向前进行判断有多个点x;
2.2填充null,形式为[null,x,dataPoint]共20个点;这三步便生成了初始的20个点,后面更新的步骤为:
2.3获取新的dataPoint,加入初始数组,初始数组.shift()
左边则是预测数据:
第一步:初始化和历史数据相同,到2.2生成了一个[null,x,dataPoint]初始历史数据数组,此时加入当前点的预测数据生成新的数组:[初始历史数据数组,实时点对应的预测数据],这就是一个初始预测数据数组;
第二步:初始历史数组.shift(),追加上一个点对应预测数据的第一个点,生成第二个历史数据数组;
追加实时值的预测数据:[第二个历史数据数组,实时点对应的预测数据]。
下图为打开折线图的示意图: