最近,产品提了个需求,需要echarts像表格分页一样去展示数据,而且数据还分不同的粒度,需要缩放的时候切换不同粒度的数据,最重要的就是, 数据量巨大,根本不能一次性全部放在网页里展示,只能动态的请求一部分数据,但是效果上要连贯起来。。。我看了一下echarts的文档,好像还真能实现
需求理解
这不就是根据滚动条的变化不停切换数据嘛
- 滚动条向前拖到顶的时候,加载上一页数据
- 滚动条向后拖到底的时候,加载下一页数据
- 滚动条放大到100%的时候,切换更上一级的数据
- 滚动条缩小到0%的时候,切换更下一级的数据
初始化滚动条
ehchart options里需要有2条zoom滚动条,inside是支持图形通过鼠标滚轮来控制数据范围,slider是在图形的下方有一条可以拖动的滚动条来控制数据的范围 这2条滚动条是联动的,start、end的范围是 0-100, start代表起始访问, end代表终止范围。初始化时,我们随意给一个范围就行。
js
// 0|------start---end----|100
dataZoom: [
{ type: 'inside', start: 10, end: 30 },
{ type: 'slider', start: 10, end: 30 },
]
监听滚动条事件
任何滚动、拖动的事件都会被这个方法监听到,如果是滚动鼠标滚轮进行缩放,数据的变化就在params.batch上,如果是拖动底部的滚动条,数据的变化就在params上
js
$echarts.value.chart.on('dataZoom', onDataZoom)
function onDataZoom(params) {
const option = $echarts.value.chart.getOption()
const insideZoom: any = option.dataZoom.find((e: any) => e.type === 'inside')
// 视图中可见的数据个数
const visibleNum = insideZoom.endValue - insideZoom.startValue
let curZoomInfo = {
start: params.start,
end: params.end,
}
if (params.batch) {
curZoomInfo = {
start: params.batch[0].start,
end: params.batch[0].end,
}
}
}
通过echarts实例的getOption方法,可以获取实时变化的option,这里的dataZoom上有个比较重要的属性,除了我们刚刚初始化设置的start、end, 还有startValue、endValue, 代表当前图形中数据的索引,如果整个data.length=10, start=0, end=100, 代表百分百展示图形,那么startValue=0、 endValue=9, start,end代表百分百,startValue,endValue代表索引, endValue-startValue代表当前视图中可见数据的个数
区分放大缩小,还是拖动
鼠标滚动的时候是放大缩小,这个会造成滚动条信息的变化;来回拖动滚动条的时候也会造成信息的变化,不同的是,拖动的时候,end-start是不变的,只是start,end的位置不停变化。 放大缩小的时候,end-start要么变大要么变小,代表要么在放大,要么在缩小。
向前请求
当拖动的时候,并且start=0的时候,说明用户把滚动条拖动最前面了,需要向前请求数据
向后请求
当拖动的时候,并且end=100的时候,说明用户把滚动条拖动最后面了,需要向前请求数据
请求上一级数据
当缩小的时候,如果end-start大于一个值,例如99,说明用户已经把图放到最大了,可以请求上一级的数据
请求下一级数据
当放大的时候,如果end-start小于一个值,例如 100/data.length,代表视图上只有一个点了,可以请求下一级的数据
数据的拼接
为了提高用户体验,保持数据的连贯性,我们在向前向后请求数据的时候,可以选择不完全替换成上一页或者下一页的数据,而是选择保留一部分之前的数据。 比如,视图上原来有50个数据,我们向前请求40个数据,留下之前的10个数据,再拼成新的50个数据。 数据拼接完成以后,还要调整一下滚动条的位置,让滚动条移动到新数据对应的位置。 例如,向前请求的时候,start=0,等数据拼接替换完成,start=40 * (100/data.length), start应该给前面新请求的40个点留出位置,不再呆在0这个位置, 这是视图里出现所有点的情况,考虑到视图当中可能出现的点只是数据的一部分, 应该start=visibleNum > 40 ? 0 : (40 - visibleNum) * step
上下级数据的关联
当视图上只剩一个点的时候,用户继续放大图,就需要请求该点关联的下一级的数据,这个点的信息可以通过 data[startValue | endValue]来获取,具体还要结合滚动条的位置, start > 50 的时候就是 data[endValue], 反之,就是data[startValue]。 缩小没有这个问题,当视图上的点全部出现,用户还继续滚动缩小的话,请求所有data相关的上一级的数据即可。
代码
js
<template>
<div class="about"></div>
<div style="height: 400px">
<v-chart ref="$echarts" :option="option" />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import _ from 'lodash'
import 'echarts'
function mockData(name: string, num: number, direction: string, curIndex: number) {
const arr = []
for (let i = 1; i <= num; i++) {
if (direction === 'before') {
arr.push({
id: curIndex - i,
label: `${name}-${curIndex - i}`,
value: Math.random() * 10,
})
} else {
arr.push({
id: curIndex + i,
label: `${name}-${curIndex + i}`,
value: Math.random() * 10,
})
}
}
if (direction === 'before') {
return arr.reverse()
} else {
return arr
}
}
const $echarts = ref<any>()
onMounted(() => {
if ($echarts.value) {
$echarts.value.chart.on('dataZoom', onDataZoom)
}
})
const option = ref({
xAxis: {
type: 'category',
data: [],
},
yAxis: {
type: 'value',
},
dataZoom: [
{ type: 'inside', start: 10, end: 30 },
{ type: 'slider', start: 10, end: 30 },
],
series: [
{
data: [],
type: 'line',
},
],
})
let preZoomInfo: any = { start: 10, end: 30 }
// 当前数据粒度
let curLevel = 'ss'
// 一张图最多展示的点
let totalNum = 50
// 每次请求获取的点
let fetchNum = 40
// 切换到下一页的时候,滚动条稍微偏移1个点,能看到上一次滚动的位置,更加连贯
let offNum = 0
// 当前数据
let curData: any[] = mockData(curLevel, totalNum, 'after', 0)
updateOptions(curData, { start: 10, end: 30 })
/**
* 更新echarts
*/
function updateOptions(data: any, zoomInfo: any) {
option.value.xAxis.data = data.map((e: any) => e.label)
option.value.series[0].data = data.map((e: any) => e.value)
option.value.dataZoom = [
{ type: 'inside', ...zoomInfo },
{ type: 'slider', ...zoomInfo },
]
}
/**
* 获取前面的数据
*/
function getPreData() {
const index = curData[0].id
return mockData(curLevel, fetchNum, 'before', index)
}
/**
* 获取后面的数据
*/
function getAfterData() {
const index = _.last(curData).id
return mockData(curLevel, fetchNum, 'after', index)
}
/**
* 切换粗粒度数据
*/
function getBigLevelData() {
if (curLevel === 'mm') {
return
}
if (curLevel === 'ss') {
curLevel = 'mm'
}
return mockData(curLevel, totalNum, 'after', 0)
}
/**
* 获取curData中第index个相关的更小一级数据
* @param index 视图中最后可见的那个数据索引
*/
function getSmallLevelData(index: number) {
if (curLevel === 'ss') {
return
}
if (curLevel === 'mm') {
curLevel = 'ss'
}
// 上一级相关联的点
const item = curData[index]
console.log(item)
return mockData(curLevel, totalNum, 'after', 0)
}
/**
* 滚动条时间
* @param params
*/
function onDataZoom(params: any) {
let step = 100 / totalNum
const option = $echarts.value.chart.getOption()
const insideZoom: any = option.dataZoom.find((e: any) => e.type === 'inside')
// 视图中可见的数据个数
const visibleNum = insideZoom.endValue - insideZoom.startValue
let curZoomInfo = {
start: params.start,
end: params.end,
}
if (params.batch) {
curZoomInfo = {
start: params.batch[0].start,
end: params.batch[0].end,
}
}
const round = (num: number) => {
return Number(num.toFixed(1))
}
// 拖动进度条
const isDrag =
round(preZoomInfo.end - preZoomInfo.start) === round(curZoomInfo.end - curZoomInfo.start)
// 进度条变大
const isBiger =
round(preZoomInfo.end - preZoomInfo.start) < round(curZoomInfo.end - curZoomInfo.start)
// 进度条变小
const isSmaller =
round(preZoomInfo.end - preZoomInfo.start) > round(curZoomInfo.end - curZoomInfo.start)
preZoomInfo = curZoomInfo
// 请求上一级图形
if (isBiger && curZoomInfo.end - curZoomInfo.start > step * (totalNum - 1)) {
const data = getBigLevelData()
if (data) {
curData = data
updateOptions(curData, { start: 10, end: 30 })
}
return
}
// 请求下一级图形
if (isSmaller && curZoomInfo.end - curZoomInfo.start < step) {
const data = getSmallLevelData(
curZoomInfo.end > 50 ? insideZoom.endValue : insideZoom.startValue
)
if (data) {
curData = data
updateOptions(curData, { start: 10, end: 30 })
}
return
}
// 向前请求
if (isDrag && curZoomInfo.start === 0) {
const preData = getPreData()
curData = [...preData, ...curData].slice(0, totalNum)
let start = visibleNum > fetchNum + offNum ? 0 : (fetchNum + offNum - visibleNum) * step
let end = (fetchNum + offNum) * step
updateOptions(curData, { start, end })
return
}
// 向后请求
if (isDrag && curZoomInfo.end === 100) {
const afterData = getAfterData()
curData = [...curData, ...afterData].slice(fetchNum, fetchNum + totalNum)
let start = (totalNum - fetchNum - offNum) * step
let end = start + visibleNum * step
updateOptions(curData, { start, end })
return
}
}
</script>