likes
comments
collection
share

echarts根据滚动条变化动态加载数据

作者站长头像
站长
· 阅读数 3

最近,产品提了个需求,需要echarts像表格分页一样去展示数据,而且数据还分不同的粒度,需要缩放的时候切换不同粒度的数据,最重要的就是, 数据量巨大,根本不能一次性全部放在网页里展示,只能动态的请求一部分数据,但是效果上要连贯起来。。。我看了一下echarts的文档,好像还真能实现

需求理解

这不就是根据滚动条的变化不停切换数据嘛

  • 滚动条向前拖到顶的时候,加载上一页数据
  • 滚动条向后拖到底的时候,加载下一页数据
  • 滚动条放大到100%的时候,切换更上一级的数据
  • 滚动条缩小到0%的时候,切换更下一级的数据

初始化滚动条

ehchart options里需要有2条zoom滚动条,inside是支持图形通过鼠标滚轮来控制数据范围,slider是在图形的下方有一条可以拖动的滚动条来控制数据的范围 这2条滚动条是联动的,start、end的范围是 0-100, start代表起始访问, end代表终止范围。初始化时,我们随意给一个范围就行。

// 0|------start---end----|100
dataZoom: [
  { type: 'inside', start: 10, end: 30 },
  { type: 'slider', start: 10, end: 30 },
]

监听滚动条事件

任何滚动、拖动的事件都会被这个方法监听到,如果是滚动鼠标滚轮进行缩放,数据的变化就在params.batch上,如果是拖动底部的滚动条,数据的变化就在params上

$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相关的上一级的数据即可。

代码

<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>