vue3中使用echarts实现双X轴折线图和面积图堆叠效果

问题背景

想实现的效果如下:
在这里插入图片描述
在这里插入图片描述

我是先拿到这个图的,需求是按照这个图来实现效果,仔细看图可以得知,两条红绿折线图是在一个X轴上,浅蓝色面积图是在另一个X轴上。 知道需要两个X轴来实现上图效果是解决这个问题的关键; 然后就可以去echarts官方文档,查找我们需要的配置项资料。

问题拆解

1)两条x轴长度不一致,导致面积图比折线图长。

在这里插入图片描述

  • 由图上可以看出第2个x轴数据貌似比第1个x轴多一个数据的,所以第2个x轴(蓝色面积图)使用了 state.xAxis2DataList,是一个重新组装的数据,由 分位区间 + X的第一个最小数据 = 11个数据
  • 第一个x轴,折线图(中位数和当前值)分别都是10个数据
2) 浅色面积图跟左侧y轴之间没有留白,折线图是有一定的留白。

通过设置 xAxis.boundaryGap 属性,想让哪个x轴有留白就设置哪个。

在这里插入图片描述

xAxis: [{
      type: 'category',
      data: state.unit,
      axisLine: { lineStyle: { color: '#d9d9d9', width: 2 } },
      axisTick: {
        alignWithLabel: true,
        ineStyle: { width: 2 }
      },
      axisLabel: {
        color: '#999999'
      }
    }, {
      type: 'category',
      data: [...state.unit, '0分位'],
      boundaryGap: true, // 坐标轴两边留白
      axisLine: { show: false }, // x轴轴线
      axisTick: { show: false }, // x轴刻度线
      axisLabel: { show: false }// x轴标签相关
    }],
3)浅蓝色面积图上有纵向的白色虚线,它的实现方式是:
{
    type: 'line',
    data: state.xAxis2DataList,
    xAxisIndex: 1, // 使用第2个x轴
    label: { show: true },
    lineStyle: { opacity: 0 },
    areaStyle: { color: 'rgba(51,153,255,0.5)' },
    markLine: {
      silent: true,
      symbol: 'none',
      label: { show: false },
      lineStyle: { color: '#fff', width: 1 },
      data: state.xAxis2DataList.map((value, index) => ({ // 这里是重点!
        xAxis: index
      }))
    },
    tooltip: { show: false } // 提示框组件
  }

可参考文档实例:
折线图区域高亮
markLine-图表标线

4)两条x轴都显示的样子

现在将第二条x轴相关代码进行注释掉,就可以真正看到两条x轴了。

在这里插入图片描述

最终实现代码

首先接口返回的数据结构如下所示:

const resData = {
  '单位(百万美元)': ['1分位', '2分位', '3分位', '4分位', '5分位', '6分位', '7分位', '8分位', '9分位', '10分位'],
  X: ['4.96~8.35', '8.35~9.12', '9.12~9.8', '9.8~10.13', '10.13~10.67', '10.67~11.41', '11.41~12.06', '12.06~13.11', '13.11~14.88', '14.88~20.04'],
  '中位数(median)': [10.67, 10.67, 10.67, 10.67, 10.67, 10.67, 10.67, 10.67, 10.67, 10.67],
  分位区间: [8.35, 9.12, 9.8, 10.13, 10.67, 11.41, 12.06, 13.11, 14.88, 20.04],
  '当前值(current)': [15.39, 15.39, 15.39, 15.35, 15.29, 15.29, 15.29, 15.28, 15.3, 15.3],
  '指数(分位值)': [8.0463, 8.7243, 9.4594, 10.2565, 11.1207, 12.0578, 13.0738, 14.1754, 15.3699, 16.665]
}
<template>
  <div>
    <el-empty v-if="state.error" description="请求失败......"/>
    <!-- 这是自己封装的echarts组件 -->
    <div v-else v-loading="state.loading">
      <BaEchart height="360px" :option="option" ref="baEchartRef"/>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted, shallowRef } from 'vue'
import type { EchartsOption } from 'echarts'

const baEchartRef = ref()
const option = shallowRef<EchartsOption | null>()

const state = reactive({
  loading: false,
  error: false,
  unit: [] as any[],  // x坐标轴类目名字
  median: [] as any[], // 中位数(median)数据
  curValue: [] as any[], // 当前值(current)数据
  xAxis2DataList: [] as any[] // 第2条x轴数据
})
const setOption = () => {
  option.value = {
    color: ['#ff666c', '#4ecc9a', '#999999'],
    grid: {
      left: '2%',
      right: '2%',
      bottom: 0,
      containLabel: true
    },
    legend: {
      itemWidth: 20,
      itemHeight: 12
    },
    tooltip: {
      trigger: 'axis',
      formatter: function(params) {
        const median = `<div>${params[0].marker}${params[0].seriesName}<span style="font-weight: bold;">${params[0].value}</span></div>`
        const current = `<div>${params[1].marker}${params[1].seriesName}<span style="font-weight: bold;">${params[1].value}</span></div>`
        return `${median}${current}`
      }
    },
    xAxis: [{
      type: 'category',
      data: state.unit,
      axisLine: { lineStyle: { color: '#d9d9d9', width: 2 } },
      axisTick: {
        alignWithLabel: true,
        ineStyle: { width: 2 }
      },
      axisLabel: {
        color: '#999999'
      }
    }, {
      type: 'category',
      data: [...state.unit, '0分位'],
      boundaryGap: true, // 坐标轴两边留白
      axisLine: { show: false }, // x轴轴线
      axisTick: { show: false }, // x轴刻度线
      axisLabel: { show: false }// x轴标签相关
    }],
    yAxis: [{
      type: 'value',
      position: 'left',
      splitLine: { lineStyle: { type: [2, 2], dashOffset: 0 } },
      axisLine: { lineStyle: { color: '#d9d9d9' } },
      axisTick: { show: true },
      axisLabel: { color: '#999999' }

    }],
    series: [
      {
        type: 'line',
        name: '中位数(median)',
        data: state.median,
        xAxisIndex: 0 // 使用第1个x轴
      }, {
        type: 'line',
        name: '当前值(curValue)',
        data: state.curValue,
        xAxisIndex: 0 // 使用第1个x轴
      }, {
        type: 'line',
        data: state.xAxis2DataList,
        xAxisIndex: 1, // 使用第2个x轴
        label: { show: true },
        lineStyle: { opacity: 0 },
        areaStyle: { color: 'rgba(51,153,255,0.5)' },
        markLine: {
          silent: true,
          symbol: 'none',
          label: { show: false },
          lineStyle: { color: '#fff', width: 1 },
          data: state.xAxis2DataList.map((value, index) => ({
            xAxis: index
          }))
        },
        tooltip: { show: false } // 提示框组件
      }]
  }
}
function reverseArr(arr:any[]) {
  return arr?.slice().reverse() ?? []
}

onMounted(() => {
  getData()
})
const getData = async() => {
  state.loading = true
  state.error = false
  // 模拟请求接口
  xxxApi.getBoatsInfo().then(({ data }) => {
    state.unit = reverseArr(data['单位(百万美元)']) // x轴坐标名称
    state.median = reverseArr(data['中位数(median)'])
    state.curValue = reverseArr(data['当前值(current)'])
    const xAxis2DataList = reverseArr(data['分位区间'])
    const X = data.X
    state.xAxis2DataList = [...xAxis2DataList, Number(X[0].split('~')[0])] // 取X数组第一个(最小的一个值)
    setOption()
  }).catch(() => {
    state.error = true
  }).finally(() => {
    state.loading = true
  })
}
</script>

<style lang="less" scoped></style>

<template>
  <!-- 封装好的公共echarts -->
  <div class="chart-warpper" ref="chartRef" :style="{ height: height }"></div>
</template>

<script setup lang="ts">
  import {
    ref,
    unref,
    reactive,
    onMounted,
    shallowRef,
    defineOptions,
    withDefaults,
    defineExpose,
    defineProps,
    nextTick,
    onActivated,
    onBeforeUnmount,
    watch
  } from 'vue'
  import { useConfigStore } from '/@/stores/config'
  import { useEventListener } from '@vueuse/core'
  import { useNavTabsStore } from '/@/stores/navTabs'

  import * as echarts from 'echarts'
  import type { EChartsOption } from 'echarts'
  defineOptions({
    name: 'BaEchart'
  })
  interface Props {
    height?: string
    option: EChartsOption
  }
  const props = withDefaults(defineProps<Props>(), {
    height: '300px'
  })

  const useNavTabs = useNavTabsStore()
  const config = useConfigStore()
  const chartRef = shallowRef()
  const myChart = shallowRef()

  // 基于准备好的dom,初始化echarts实例
  const initChart = () => {
    myChart.value = echarts.init(chartRef.value as HTMLElement)
  }
  const echartsResize = () => {
    nextTick(() => {
      myChart.value.resize()
    })
  }

  onActivated(() => {
    echartsResize()
  })
  onMounted(() => {
    initChart()
    useEventListener(window, 'resize', echartsResize)
  })
  onBeforeUnmount(() => {
    myChart.value.dispose()
  })

  watch(
    () => props.option,
    () => {
      myChart.value.setOption(props.option)
    }
  )
  watch([
    () => useNavTabs.state.tabFullScreen,
    () => config.layout.menuCollapse,
    () => {
      setTimeout(() => {
        echartsResize()
      }, 300)
    }
  ])
  // 暴露方法给其他页面使用
  defineExpose({
    showLoading: () =>
      myChart.value.showLoading('default', {
        text: '',
        zleve: '10000',
        spinnerRadius: 15,
        lineWidth: 2,
        showSpinner: true,
        color: '#e52523'
      }),
    hideLoading: () => myChart.value.hideLoading(),
    registerMap: (mapName: string, rawDef: any, rawSpecialAreas?: any) =>
      echarts.registerMap(mapName, rawDef, rawSpecialAreas),
    echartsResize: () => echartsResize()
  })
</script>

<style lang="less" scoped></style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铁锤妹妹@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值