动态加载
基本定义
向图表中添加数据,完成数据更新 是可视化方案中关键一环。
向 Echarts 图表(本文特指 折线图)中,动态追加或更新数据,也正是本文主要阐述的 动态加载数据 能力。
本文重点介绍如何设计动态加载策略,通过 队列(先入先出) 的方式,实现 暴力吞入、定期吐出 式数据更新!
在开始之前,我们不妨脑暴一下 实现难点,以便您在阅读下文过程中,有的放矢,拿捏脉路:
- 动态加载数据时,如何兼顾 Echarts 性能
- 高频 or 低频 setOption 数据
- 如何设计可复用的 动态加载方案
- 在数据加载过程中,如何 更新图表配置
主要特点
本文封装的动态加载策略,将具备以下特点:
- 队列结构,数据先入先出
- 暴力吞入,定期清空吐出
- 定时吐出,等待超时销毁
- 非法输入,控制不入队列
- 数据更新,也能更新配置
触发时机
- 业务接口响应数据时,动态更新图表数据
- 组件或页面销毁之前,销毁动态加载实例
- 清空图表时(若有),销毁动态加载实例
效果演示
👉👉👉 在线演示 👈👈👈
认识折线
特殊优势
正如 Echarts 官网所描述的:↓↓↓
折线图是用 折线 将各个数据点 标志 连接起来的图表,用于展现数据的变化趋势。
我们选用 折线图,作为动态加载数据的目标对象,主要因其具备以下优势:
- 它是业务场景中最常见的图表类型
- 它能最直观表现动态数据更新趋势
- 它的属性配置简单,上手成本极低
setOption
setOption 是 Echarts 更新图表配置(包括图表数据)的一个且万能的 API
在本文前面,我们脑暴了一个难点:
高频 or 低频 setOption 数据 ???
我们已经知道,每次 setOption,Echarts 内部将做以下事情:
- 合并前后参数及数据
- 刷新图表
- 适当监听和触发事件
- 若开启动画,对比数据前后差异,再通过合适动画表现数据变化
完成这些事情,将消耗资源,牺牲性能。
那么,高频还是低频的问题,就有了最直接的答案:
尽可能降低 setOption 的频率
需要注意的是,如果仅仅是更新加载数据,就折线图而言,
是不是 appendData 的方式更佳 ? -- 是的!
然鹅,本文就很贱兮兮,是既要还要的那种尿性~
既要更新数据,还要更配置。选择 setOption 就要 优于 appendData 了。
加载策略
设计思路
- 通用工具: 数据吞吐的队列工具类(先入先出)
// 队列工具类:先入先出 @/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
}
}
- 设计方法: 清空队列,向图表中添加数据
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() // 回调函数:已清空有效的队列
}
- 设计方法: 向 Echarts 中追加数据
// 向 Echarts 中追加数据
function _appendToEcharts(echarts) {
const echartsOption = echarts.getOption()
echarts.setOption({
...echartsOption,
series: SeriesList
}, true)
}
- 公共常量: 定时器时间
// 定时器时间
export const INTERVAL_TIME = 800
5. 设计方法: 清空定时器
// 清空定时器
function _clearMyInterval() {
clearInterval(myIntervalID)
myIntervalID = null
myEmptyCount = 0
myNotEmptyCount = 0
}
- 暴露方法: 更新已缓存的配置数据
/** 业务配置数据 */
let LegendSetting = {} // 图例 Setting 信息
let SeriesEditInitConfig = [] // 暂存曲线初始化配置
let SeriesEditSettingConfig = [] // 暂存曲线Setting配置
// 更新已缓存的配置数据
export function updateConfig(legendSetting, seriesInitConfig, seriesSettingConfig) {
LegendSetting = legendSetting
SeriesEditInitConfig = seriesInitConfig
SeriesEditSettingConfig = seriesSettingConfig
}
- 暴露方法: 向缓存队列追加数据
// 向缓存队列追加数据
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,清空一次队列
}
}
- 暴露方法: 加载数据
export function loadData(data = [], echarts, loaded = () => {}) {
updateConfig(LegendSetting, SeriesEditInitConfig, SeriesEditSettingConfig) // 更新所需配置
append(data, echarts, () => {
clearedChart = false // 更新图表清空标记
loaded('loading') // 回调函数:数据加载中
}, () => {
loaded('finished') // 回调函数:数据加载完成
}) // 向缓存队列追加数据
if (!LoadDataCounter) { // 第一次加载数据后,通过 resize 方式,更新并使项目字体文件生效
LoadDataCounter = 1
}
}
- 暴露方法: 清空暂存、定时器、停止追加等
// 清空暂存、定时器、停止追加等
export function clearAppend() {
SeriesList = []
myQueue.clear()
_clearMyInterval()
}
清空图表
清空图表将做以下动作:
- 清空曲线
- 清空图例
- 清空坐标轴
- 保留标题及设置功能
- 保留图表及设置功能
- 禁止单项设置坐标轴
- 禁止单项设置曲线
- 禁止单项设置图例
- 销毁动态加载时,相关变量及定时器
- 暴露方法: 清空图表数据
// 清空图表数据
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
// 清空选中框
// 清空选中曲线
}
性能提升
- 折线图配置简化
- 关闭标记 symbol
- 降采样策略 sampling 开启为 lttb
- 关闭动画
- 定时器超时销毁
- 内部定时器,会在队列连续出现 N 次空状态情况下,被主动销毁,进而提升性能(N 值为 2)