优化echarts大量数据且高频率更新数据的场景

832 阅读3分钟

业务场景

  1. 存在不确定数量的多个折线图、3D 折线图。
  2. 每个图中存在不确定数量的多条线。
  3. 每次刷新间隔大概在40ms左右,数据量递增。
  4. 更新数据的时候还需要频繁的交互。

思路

官网提供的优化配置可以说是基本没啥用,appendData也着实有些离谱每次都需要 resize 才能正常看到最新的数据,并且resize对性能的消耗也不低。折线图的降采样算法确实有效果,但是后期数据量上来后消耗的时间也会很长,无奈只能另辟蹊径。

一开始打算使用offscreenCanvas把echarts放到webworker中这样就不会影响主线程的性能,但是却失去了和图表的交互。 ----PASS

转入研究第二个方案iframe,在chrome67版本后chrome提供了一个站点隔离的安全特性(使用iframe时如果站点不同会会使用一个新的进程),经过尝试后发现由于每次更新渲染时间比较长会阻塞渲染进程卡顿的问题还是无法避免。----PASS

解决方案

1、去掉折线图的symbol,不绘制symbol的话渲染速度会快非常多。

// option
{
  tooltip: {
    trigger: 'axis' // 不绘制symbol的话默认的提示会失效,改为通过坐标轴触发。
  },
  series: [
   {
     name: 'Email',
     type: 'line',
     symbol: 'none', // 设置未none后不再绘制symbol
     data: [120, 132, 101, 134, 90, 230, 210]
   },
   ...
}

2、降采样算法

  • echarts内置的降采样算法性能提升有限(line3D不支持)而且只能在主线程中,所以打算自己实现折线图和3D 折线图的降采样算法并放到worker中降低主线程压力。

  • 2d折线图数据降采样 -github

function lttp (arr: number[][], num: number): number[][]{

  const seriesLength: number = arr.length

  if (num >= seriesLength || num === 0) {
    return arr
  }

  if (num === 1) {
    return [[arr[0][0], arr[0][1]]]
  }

  const downsampled: number[][] = []
  let sampledIndex = 0
  let bucket = (seriesLength - 2) / (num - 2)
  let point: number = 0
  let nextPoint: number = 0
  let area: number
  let maxAreaPoint: number[] = []
  let maxArea: number

  downsampled[sampledIndex++] = arr[point]

  for (let count = 0; count < num - 2; count++) {
    let averageX = 0
    let averageY = 0
    let averageRangeStart = Math.floor((count + 1) * bucket) + 1
    let averageRangeEnd = Math.floor((count + 2) * bucket) + 1

    if (averageRangeEnd > seriesLength) {
      averageRangeEnd = seriesLength
    }

    let averageRangeLength = averageRangeEnd - averageRangeStart

    if (averageRangeStart > averageRangeEnd) {
      averageRangeStart++
    }

    if (averageRangeStart < averageRangeEnd) {
      averageX = averageX + arr[averageRangeStart][0]
      averageY = averageY + arr[averageRangeStart][1]
    }

    averageX = averageX / averageRangeLength
    averageY = averageY / averageRangeLength

    let rangeOffs = Math.floor((count) * bucket) + 1
    let rangeTo = Math.floor((count + 1) * bucket) + 1
    let pointBucketX = arr[point][0]
    let pointBucketY = arr[point][1]

    maxArea = -1
    area = -1

    while (rangeOffs < rangeTo) {

      area = Math.abs((pointBucketX - averageX) * (arr[rangeOffs][1] - pointBucketY) - (pointBucketX - arr[rangeOffs][0]) * (averageY - pointBucketY)) * 0.5;

      if (area > maxArea) {
        maxArea = area
        maxAreaPoint = arr[rangeOffs]
        nextPoint = rangeOffs
      }
      rangeOffs++
    }

    downsampled[sampledIndex++] = maxAreaPoint
    point = nextPoint
  }

  downsampled[sampledIndex++] = arr[seriesLength - 1]

  return downsampled
}


// 示例用法
// 假设我们有一个二维数组arr,每个子数组表示一个点的坐标
const arr2d = Array.from({ length: 100 }, () => Array.from({ length: 2 }, () => Math.random())); // 100个三维点

const sampling2d = lttp(arr2d, 50);

console.log(sampling2d)


  • 3D折线图数据降采样 github
function farthestPointSampling (arr: number[][], num: number): number[][] {
  const arrLength: number = arr.length

  if (num >= arrLength || num === 0) {
    return arr
  }

  const nPoints: number = arr.length
  const nDim: number = arr[0].length // 假设所有点具有相同的维度

  const sampledIndices: number[] = [0]
  const minDistances: number[] = new Array(nPoints).fill(Infinity)
  for (let i = 1; i < num; i++) {
    const currentPoint: number[] = arr[sampledIndices[i - 1]]
    const distToCurrentPoint: number[] = new Array(nPoints)

    for (let j = 0; j < nPoints; j++) {
      const diff: number[] = []
      for (let k = 0; k < nDim; k++) {
        diff.push(arr[j][k] - currentPoint[k])
      }
      distToCurrentPoint[j] = Math.sqrt(diff.reduce((sum, val) => sum + val * val, 0))
    }

    for (let j = 0; j < nPoints; j++) {
      minDistances[j] = Math.min(minDistances[j], distToCurrentPoint[j])
    }

    const farthestPointIdx: number = minDistances.indexOf(Math.max(...minDistances))
    sampledIndices.push(farthestPointIdx)
  }

  return sampledIndices.sort((a, b) => a - b).map(i => arr[i])
}


// 示例用法
// 假设我们有一个二维数组arr,每个子数组表示一个点的坐标
const arr3d = Array.from({ length: 100 }, () => Array.from({ length: 3 }, () => Math.random())); // 100个三维点

const sampling3d = farthestPointSampling(arr3d, 50);

console.log(sampling3d)

3、再切片

  • 降采样算法虽然在worker中执行,但是数据量大的时候执行时间也会比较长,会导致数据更新的频率降低。 这里我的做法是每累计1000条数据进行一次降采样,每次降采样后和之前降采样的数据进行合并。