问题背景
想实现的效果如下:
我是先拿到这个图的,需求是按照这个图来实现效果,仔细看图可以得知,两条红绿折线图是在一个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>