ECharts海量資料渲染解決卡頓的4種方式

南风晚来晚相识發表於2024-04-24

場景

週五進行需求評審的時候;
出現了一個圖表,本身一個圖表本沒有什麼稀奇的;
可是產品經理在圖表的上的備註,讓我覺得這個事情並不簡單;
那個圖表的時間跨度可以是月,年,而且時間間隔很短;
這讓我意識到事情並不是想的那樣簡單;
然後經過簡單的詢問:如果選擇的範圍是年;資料可能會上萬;
我們都知道;出現上萬的資料;
在渲染的時候肯定會出現白屏,操作的時候卡頓;
今天週末,沒有事幹;來研究研究 Echarts 渲染海量資料

file模組用來寫檔案

我們首先使用node來生成10萬條資料;
藉助node的fs模組就行;
如果不會的小夥伴;也不要擔心;超級簡單
// 引入模組
let fs = require('fs');
// 資料內容
let fileCont='我是檔案內容'
/**
 * 第一個引數是檔名
 * 第二個引數是檔案內容,這個檔案的內容必須是字串哈(特別注意)
 * 第三個引數是回撥函式, 回撥函式中有兩個引數,
 * 第一個引數是錯誤資訊,
 * 第二個引數是寫入成功後的返回值
 * */ 
fs.writeFile('./demodata.txt',fileCont, (error, data) => {
  if (!error) {
    console.log('寫入成功了',data)
  } else {
    console.log('寫入失敗了',error)
  }
})

現在我們需要建立一個指定型別的資料格式

我們等會從2022.1.1開始;每條資料間隔5分鐘;產生10萬條資料。
time的值是時間戳,我們可以透過 new Date().getTime() 來獲取
value的值是溫度,我們透過Math.random() * 50+10來獲取
time的時間間隔是每隔5分鐘
資料格式如下
 [
  {"time":1640966400000,"value":36.57},
  {"time":1640966700000,"value":31.68},
]
// 引入模組
let fs = require('fs');
// 生成100000條符合要求的資料格式
function timeFn(total){
  // 獲取2022年1月1日的時間戳
  let dateTimeStamp = new Date(2022, 0, 1).getTime(); // 2022年1月1日 
  // 5分鐘的時間戳是多少
  let oneHourStamp = 1000 * 60*5;
  let newArr = []
  for(let i= 0;i<total; i++){
    // 構造我們需要的資料格式
    newArr.push(
      { time: dateTimeStamp + oneHourStamp* i,
        value:Math.random() * 50+10 
      }
    )
  }
  return newArr
}
let needData = timeFn(100000)

fs.writeFile('./demodata.js',JSON.stringify(needData), (error, data) => {
  // JSON.stringify(needData) 將陣列轉為字串
  if (!error) {
    console.log('寫入成功了',data)
  } else {
    console.log('寫入失敗了',error)
  }
})

10萬條資料渲染耗時10秒,且頁面非常卡頓

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="../allechart/echarts.js"></script>
  <script src="./demodata.js"></script>
</head>

<body>
  <div style="width: 598px;height: 400px;" id="box"></div>
</body>
<script>
  function backTime(value){
    let date = new Date(value);  
    // 獲取年份、月份和日期  
    let year = date.getFullYear();  
    // 月份從 0 開始,需要加 1  
    let month = date.getMonth() + 1; 
    let day = date.getDate();  
    let hours = date.getHours();  
    let minutes = date.getMinutes();  
    let seconds = date.getSeconds(); 
    // 格式化月份和日期為兩位數(不足兩位時補零)  
    month = month < 10 ? '0' + month : month;  
    day = day < 10 ? '0' + day : day;  
    hours = hours < 10 ? '0' + hours : hours;  
    minutes = minutes < 10 ? '0' + minutes : minutes;  
    seconds = seconds < 10 ? '0' + seconds : seconds;  
    // 返回格式化後的字串  
    return year + '-' + month + '-' + day +  '\n' + hours + ':' + minutes + ':' + seconds;
  }
 
  console.log(backData)
  let myChart = echarts.init(document.querySelector('#box'))
  let option = {
    legend: {
      data: ['某城市']
    },
    tooltip: {
      trigger: 'axis',
      triggerOn: 'mousemove',
      confine: true,
      extraCssText: 'white-space: pre-wrap'
    },
    xAxis: {
      type: 'category',
      // 返回時間
      data: backData.map(v=> backTime(v.time)),
      splitLine: { show: false },
      lineStyle: {
        width: 2
      },
      axisTick: {
        show: false
      },
      axisLabel:{
        // 更改x軸文字顏色的配置
        textStyle: {
          color: '#717782'
        },
        showMinLabel: true,
        showMaxLabel: true // 固定顯示X軸的最後一條資料
      },
    },
    yAxis: {
      type: 'value',
      axisLine: {
        show: false
      },
      axisTick: {
        show: false
      },
      splitLine: {
        lineStyle: {
          color: '#D2DBE6'
        }
      },
      axisLabel: {
        formatter: '{value}',
        color: '#717782'
      }
    },
    grid: {
      left: '30',
      right: '35',
      bottom: '10',
      top: '20',
      containLabel: true
    },
  
    series: [
      {
        data:backData.map(v=>v.value),
        type: 'line',
        smooth: true
      }
    ]
  };
  myChart.setOption(option);
</script>

</html>

發現的問題

我們發現渲染時間非常久(需要10多秒),而且頁面卡頓;
有沒有好的辦法來解決這個問題呢;
是有的,最好的使用echarts的dataZoom用於區域縮放;
透過滑塊看指定區域的資料,我們來嘗試一下

dataZoom的常見屬性介紹

type: "slider" || "inside",
  slider:這種型別會在圖表的一側新增一個滑動條,
  使用者可以透過拖動滑動條來改變資料視窗的範圍,從而實現資料的縮放。
  inside:這種型別縮放元件是內建於座標系中的,
  使用者可以透過滑鼠滾輪、觸屏手指滑動等方式來運算元據的縮放。
  簡單點說:slider會產生一個捲軸,inside不會

xAxisIndex: 可以是一個數字,表示特定的X軸索引;
  也可以是一個陣列,表示同時控制多個X軸。

xAxisIndex: 0, 控制第1條資料開始

start: 0, 資料視窗範圍的起始百分比。範圍是:0 ~ 100。表示 0% ~ 100%。

end: 1, 資料視窗範圍的結束百分比。範圍是:0 ~ 100。

minSpan: 0, 用於限制視窗大小的最小值(百分比值),取值範圍是 0 ~ 100。

maxSpan: 10, 用於限制視窗大小的最大值(百分比值),取值範圍是 0 ~ 100。

特別提醒:start: 設定為0;說明是從第1條資料開始的;
那麼xAxisIndex就必須是0;
因為xAxisIndex不是0,他們就互相矛盾了;
minSpan 和 maxSpan一般配合使用;主要是用於只展示某一個區間;
無論使用者怎麼縮放;都會在這個區間

我們使用 dataZoom 來處理海量的資料

... 其他配置項
dataZoom: [
  {
    type: "slider", // 滑塊型別 值有slider和inside
    xAxisIndex: [0],
    start: 0,
    end: 1,
    minSpan: 0, // 用於限制視窗大小的最小值(百分比值),取值範圍是 0 ~ 100。
    maxSpan: 10,
  },
],
series: [
  {
    data:backData.map(v=>v.value),
    type: 'line',
    smooth: true
  }
]

配置之後,我們發現渲染非常流暢

透過配置前和配置後的圖的對比
我們發現配置之後;頁面渲染速度非常快;
開啟頁面就渲染完成,壓根想不到是10萬條資料;
說明透過 dataZoom 是非常有效的

dataZoom處理海量資料的優缺點

優點:配置簡單;
缺點:只能看指定區域性的資料;無法看整體資料

其他辦法 sampling 降取樣策略 sampling: 'average'

series: [
  {
    data:backData.map(v=>v.value),
    type: 'line',
    smooth: true,
    sampling: 'average',//' 表示採用平均值取樣策略
  }
]

sampling的幾個值

'lttb': 採用 Largest-Triangle-Three-Bucket 演算法,
      可以最大程度保證取樣後線條的趨勢,形狀和極值。
      不過有可能會造成頁面渲染時間長
'average': 取過濾點的平均值
'min': 取過濾點的最小值
'max': 取過濾點的最大值
'minmax': 取過濾點絕對值的最大極值 (從 v5.5.0 開始支援)
'sum': 取過濾點的和

sampling 降取樣策略 sampling: 'lttb'

series: [
  {
    data:backData.map(v=>v.value),
    type: 'line',
    smooth: true,
    // 採用 Largest-Triangle-Three-Bucket 演算法,
    // 可以最大程度保證取樣後線條的趨勢,形狀和極值。
    // 不過有可能會造成頁面渲染時間長
    sampling: 'lttb'
  }
]

降取樣策略 sampling 的優缺點

優點:可以看見整體的趨勢;
缺點:部分資料丟失;tooltip功能可能實現不了

使用 large 屬性

series: [
  {
    data:backData.map(v=>v.value),
    type: 'line',
    smooth: true,
    //開啟大資料量最佳化,在資料特別多而出現圖形卡頓時候開啟
    large:true, 
  }
]

發現使用large屬性沒有效果

為什麼我們使用large沒有效果呢?
我們去官網看看怎麼說的;
https://echarts.apache.org/zh/option.html#series-bar.type
在上面這個文件中心,我發現折線圖[type: 'line']沒有 large 屬性
large支援的圖表:柱狀圖(bar), K 線圖 (candlestick), 路徑圖(lines),散點圖(scatter)
其他型別的圖表暫時不支援;
所以我們可以把折線圖更改為柱狀圖看下是否可以解決卡頓問題;
series: [
  {
    data:backData.map(v=>v.value),
    type:'bar',
    //開啟大資料量最佳化,在資料特別多而出現圖形卡頓時候開啟
    large:true,
  }
]

large 屬性的介紹以及優缺點

large:是否開啟大資料量最佳化;
在【資料圖形】特別多而出現卡頓時候可以開啟。
開啟後配合 largeThreshold 在資料量大於指定閾值的時候對繪製進行最佳化。
優點:解決圖形卡資料量大而產生的卡頓問題。
缺點:最佳化後不能自定義設定單個資料項的樣式;
【特別提醒】: 
large支援的圖表:柱狀圖(bar), K 線圖 (candlestick), 路徑圖(lines),散點圖(scatter)

"輔助"large屬性最佳配角 largeThreshold

有些時候;資料量並不是一開始就是大量;
而是經過時間的變化;資料才變成了海量資料;
如果我們一開始就使用large開啟最佳化;這顯然是不適合的;
這個時候;我們的最佳配角 largeThreshold 就閃亮登場了;
largeThreshold:開啟繪製最佳化的閾值。
當資料量(即data長度)大於這個閥值的時候,會開啟高效能模式。
低於這個閥值;不會開啟高效能模式;
比如我們希望:資料量(即data長度)大於1萬時開啟高效能模式,你可以這樣設定:
series: [
  {
    data:backData.map(v=>v.value),
    type:'bar',
    //開啟大資料量最佳化,在資料特別多而出現圖形卡頓時候開啟
    large:true,
    // 資料量大於1萬時開啟高效能模式,如果沒有大於1萬;不會開啟
    // 此時我們的資料是10萬;會開啟;渲染非常快
    largeThreshold: 10000,
  }
]

appendData 屬性的簡單介紹

根據官網的介紹;
appendData屬性會分片載入資料和增量渲染;
在大資料量的場景下(例如地理數的打點);
此時,資料量將會非常的大;
在網際網路環境下,往往需要分片載入;
appendData 介面提供了分片載入後增量渲染的能力;
渲染新加入的資料塊時,不會清除原有已經渲染的部分資料。
但是並不是所有圖表型別都支援這個屬性;
目前不支援series系列的;如柱狀圖,折線圖,餅狀圖這些都不支援;
目前支援的圖有: 散點圖(scatter),線圖(lines)。
ECharts GL的散點圖(scatterGL)、線圖(linesGL) 和 視覺化建築群(polygons3D)。
上面這段參考:https://echarts.apache.org/zh/api.html#echartsInstance.appendData

series系列的圖表

尾聲

最近股票虧麻了;哎;好難受;
如果覺得我的文章寫的還不錯;給我點個推薦;
感謝了

相關文章