最近接到個任務,需要用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(),追加上一個點對應預測數據的第一個點,生成第二個歷史數據數組;
追加實時值的預測數據:[第二個歷史數據數組,實時點對應的預測數據]。
下圖為打開折線圖的示意圖: