Echarts 动态加载折线图数据

1,334 阅读6分钟

动态加载

基本定义

向图表中添加数据,完成数据更新 是可视化方案中关键一环。

Echarts 图表(本文特指 折线图)中,动态追加或更新数据,也正是本文主要阐述的 动态加载数据 能力。

本文重点介绍如何设计动态加载策略,通过 队列(先入先出) 的方式,实现 暴力吞入、定期吐出 式数据更新!

在开始之前,我们不妨脑暴一下 实现难点,以便您在阅读下文过程中,有的放矢,拿捏脉路:

  1. 动态加载数据时,如何兼顾 Echarts 性能
  2. 高频 or 低频 setOption 数据
  3. 如何设计可复用的 动态加载方案
  4. 在数据加载过程中,如何 更新图表配置

主要特点

本文封装的动态加载策略,将具备以下特点:

  1. 队列结构,数据先入先出
  2. 暴力吞入,定期清空吐出
  3. 定时吐出,等待超时销毁
  4. 非法输入,控制不入队列
  5. 数据更新,也能更新配置

触发时机

  1. 业务接口响应数据时,动态更新图表数据
  2. 组件或页面销毁之前,销毁动态加载实例
  3. 清空图表时(若有),销毁动态加载实例

效果演示

👉👉👉 在线演示 👈👈👈

chrome_0f71wQyKhc.gif

认识折线

特殊优势

正如 Echarts 官网所描述的:↓↓↓

折线图是用 折线 将各个数据点 标志 连接起来的图表,用于展现数据的变化趋势。

image.png

我们选用 折线图,作为动态加载数据的目标对象,主要因其具备以下优势:

  1. 它是业务场景中最常见的图表类型
  2. 它能最直观表现动态数据更新趋势
  3. 它的属性配置简单,上手成本极低

setOption

setOptionEcharts 更新图表配置(包括图表数据)的一个且万能的 API

在本文前面,我们脑暴了一个难点:

高频 or 低频 setOption 数据 ???

我们已经知道,每次 setOptionEcharts 内部将做以下事情:

  1. 合并前后参数及数据
  2. 刷新图表
  3. 适当监听和触发事件
  4. 若开启动画,对比数据前后差异,再通过合适动画表现数据变化

完成这些事情,将消耗资源,牺牲性能。

那么,高频还是低频的问题,就有了最直接的答案:

尽可能降低 setOption 的频率

需要注意的是,如果仅仅是更新加载数据,就折线图而言,

是不是 appendData 的方式更佳 ? -- 是的!

然鹅,本文就很贱兮兮,是既要还要的那种尿性~

既要更新数据,还要更配置。选择 setOption 就要 优于 appendData 了。

加载策略

设计思路

  1. 通用工具: 数据吞吐的队列工具类(先入先出)
// 队列工具类:先入先出 @/shared/queue.js
export default class Queue {
  constructor() {
    this.items = []
  }

  // 向队列添加一个元素
  enqueue(item) {
    this.items.push(item) // 队列尾部添加元素
  }

  // 从队列中移除一个元素
  dequeue() {
    return this.items.shift() // 队列头部移除元素
  }

  // 返回队列中第一个元素
  front() {
    return this.items[0]
  }

  // 返回队列中的最后一个元素
  tail() {
    return this.items[this.items.length - 1]
  }

  // 队列是否为空
  isEmpty() {
    return this.size() === 0
  }

  // 队列长度
  size() {
    return this.items.length
  }

  // 清空队列
  clear() {
    this.items = []
  }

  // 获取队列
  queue() {
    return this.items
  }
}
  1. 设计方法: 清空队列,向图表中添加数据
import MyQueue from '@/shared/queue.js'
import debounce from 'lodash/debounce'

let SeriesList = [] // 暂存所有曲线全量数据
let myQueue = new MyQueue() // 使用队列处理追加的数据
let myIntervalID = null // setInterval ID
let myEmptyCount = 0 // 队列为空次数
let myNotEmptyCount = 0 // 队列非空次数

// 清空队列
function _clearQueue(echarts, cleared = () => {}) {
  if (!echarts) return

  if (MyQueue.isEmpty()) {
    myEmptyCount += 1 // 空次数累加
    return
  }

  myNotEmptyCount += 1 // 非空次数累加

  let emptyCounter = 0 // 队列中的非法数据元素个数
  const queue = MyQueue.queue() // 获取队列所有数据
  MyQueue.clear() // 清空队列

  const { xAxis, yAxis, series } = echarts.getOption() || {}
  SeriesList = series || []
  
  queue.forEach((data) => {
    emptyCounter += 1
  })

  if (emptyCounter === queue.length) return // 不处理空数据

  _appendToEcharts(echarts)
  typeof cleared === 'function' && cleared() // 回调函数:已清空有效的队列
}
  1. 设计方法:Echarts 中追加数据
// 向 Echarts 中追加数据
function _appendToEcharts(echarts) {
  const echartsOption = echarts.getOption()

  echarts.setOption({
    ...echartsOption,
    series: SeriesList
  }, true)
}
  1. 公共常量: 定时器时间
// 定时器时间
export const INTERVAL_TIME = 800

5. 设计方法: 清空定时器

// 清空定时器
function _clearMyInterval() {
  clearInterval(myIntervalID)
  myIntervalID = null
  myEmptyCount = 0
  myNotEmptyCount = 0
}
  1. 暴露方法: 更新已缓存的配置数据
/** 业务配置数据 */
let LegendSetting = {} // 图例 Setting 信息
let SeriesEditInitConfig = [] // 暂存曲线初始化配置
let SeriesEditSettingConfig = [] // 暂存曲线Setting配置

// 更新已缓存的配置数据
export function updateConfig(legendSetting, seriesInitConfig, seriesSettingConfig) {
  LegendSetting = legendSetting
  SeriesEditInitConfig = seriesInitConfig
  SeriesEditSettingConfig = seriesSettingConfig
}
  1. 暴露方法: 向缓存队列追加数据
// 向缓存队列追加数据
export function append(data = [], echarts, addCb = () => {}, finished = () => {}) {
  myQueue.enqueue(data) // 将动态数据加入队列

  if (myIntervalID === null) {
    _clearQueue(echarts, () => addCb()) // 先清空一次后,再定时循环

    myIntervalID = setInterval(() => {
      _clearQueue(echarts, () => addCb()) // 回调函数:每次清空队列后,执行回调 addCb

      if (myEmptyCount - myNotEmptyCount >= INTERVAL_EMPTY_NUM) {
        _clearMyInterval() // 空次数比非空次数多于指定次数时,清空定时器
        typeof finished === 'function' && finished() // 回调函数:彻底完成数据加载
      }
    }, INTERVAL_TIME) // 每隔 INTERVAL_TIME ms,清空一次队列
  }
}
  1. 暴露方法: 加载数据
export function loadData(data = [], echarts, loaded = () => {}) {
  updateConfig(LegendSetting, SeriesEditInitConfig, SeriesEditSettingConfig) // 更新所需配置
  append(data, echarts, () => {
    clearedChart = false // 更新图表清空标记
    loaded('loading') // 回调函数:数据加载中
  }, () => {
    loaded('finished') // 回调函数:数据加载完成
  }) // 向缓存队列追加数据

  if (!LoadDataCounter) { // 第一次加载数据后,通过 resize 方式,更新并使项目字体文件生效
    LoadDataCounter = 1
  }
}
  1. 暴露方法: 清空暂存、定时器、停止追加等
// 清空暂存、定时器、停止追加等
export function clearAppend() {
  SeriesList = []
  myQueue.clear()
  _clearMyInterval()
}

清空图表

清空图表将做以下动作:

  1. 清空曲线
  2. 清空图例
  3. 清空坐标轴
  4. 保留标题及设置功能
  5. 保留图表及设置功能
  6. 禁止单项设置坐标轴
  7. 禁止单项设置曲线
  8. 禁止单项设置图例
  9. 销毁动态加载时,相关变量及定时器
  • 暴露方法: 清空图表数据
// 清空图表数据
export function clearChartData(echarts, webChannel) {
  if (!echarts) return

  const option = echarts.getOption()

  echarts.setOption({
    ...option,
    xAxis: [], // 清空 X 轴
    yAxis: [], // 清空 Y 轴
    series: [], // 清空曲线
  }, true)

  /** 重置相关状态 */
  clearedChart = true // 更新已清空标识
  clearAppend() // 清空追加逻辑相关数据
  clearSelectedLegend() // 清空图例选中状态
  fixClearByResize(echarts) // 处理大数据集下,清空操作后仍有曲线残留问题

  /** 重置相关行为 */
  // 重置 dataZoom
  // 清空选中框
  // 清空选中曲线
}

性能提升

  1. 折线图配置简化
  • 关闭标记 symbol
  • 降采样策略 sampling 开启为 lttb
  • 关闭动画
  1. 定时器超时销毁
  • 内部定时器,会在队列连续出现 N 次空状态情况下,被主动销毁,进而提升性能N 值为 2