前言
❇️ 本篇文章主要整理了在日常使用 Echarts 图表库的过程中遇到的一些问题及常用技巧,希望对大家有所帮助,相关内容会持续更新。
以下接正文 🎉
1、按需引入 Echarts
众所周知,Echarts 的包体积很大,项目中往往只用到部分功能,如果不按需引入,打包后体积将很大。我们可以用下面的方法实现按需引入。
网上有如下方法,但最终发现无效,还是全量引入的。
// 引入基本模板
let Echarts = require("echarts/lib/echarts");
// 按需引入需要的组件模块
require("echarts/lib/chart/line");
require("echarts/lib/component/title");
require("echarts/lib/component/legend");
require("echarts/lib/component/tooltip");
需要用以下方法:
1、安装 babel-plugin-equire
npm i babel-plugin-equire -D
2、在项目的 .babelrc 文件中添加该插件。
{
"plugins": [
// other plugins
"equire"
]
}
3、新建一个文件 echarts.js。
const echarts = equire([
'bar',
'line',
'grid',
'legend',
'tooltip',
'title'
])
export default echarts
4、组件中直接引入使用即可。(4.8.0之后的版本,控制台可能会提示 “import echarts from ‘echarts/lib/echarts’” is not supported anymore. Use “import * as echarts from ‘echarts/lib/echarts’” instead;),不用理会,生产环境是不会有这个错误提示的,如果介意,降级到4.8.0即可)
import echarts from '@/assets/echarts'
2、动态更新 Echarts 配置
// 初始创建
var myChart = echarts.init(document.getElementById('main'));
var option = {
........
}
myChart.setOption(option);
function refreshData(data) {
// 刷新数据
var option = myChart.getOption();
option.series[0].data = data;
myChart.setOption(option);
}
refreshData(data); //自定义刷新的时候调用
3、动态更新 Echarts 配置
1、 tooltip有一个confine属性,设置为true就可以
tooltip: {
trigger: 'axis',
confine: true, // tooltip限制在图表内
axisPointer: {
type: 'shadow',
crossStyle: {
color: '#999'
}
},
backgroundColor: 'rgba(255,255,255,0.9)',
formatter: function (params) {
if (!params.length) return defaultTpl
let ret = ''
params.forEach((itm, idx) => {
ret +=
`<p class="line${itm.seriesIndex}">${MapField[itm.seriesIndex]}:<span>${itm.value}</span></p>`
})
return `<div class="tooltip-chart">${ret}</div>`
}
}
2、手动指定位置
tooltip: {
position: function (point, params, dom, rect, size) {
dom.style.transform = "translateZ(0)";
}
}
4、多页面情况下,图 表的resize事件污染导致图表消失
一般出现在使用了多标签页面,或者开启了类似keep-alive这种缓存组件情况下,只需要注意根据页面来区分即可。
拿vue项目来说,在开启keep-alive情况下,多个页面同时引入了同一个图表组件,在resize事件里做判断即可,可以配合activated生命周期来同步resize状态,确保图表正常展示,如下:
<chart-trend ref="chartTrend" slot="content" page="/overviewBoard" />
renderChart(config) {
this.chart && this.chart.dispose()
const el = document.getElementById(config.id)
this.chart = echarts.init(el)
this.chart.setOption(config.option)
// 监听resize事件
window.addEventListener('resize', this.resizeFn)
this.$on('hook:beforeDestroy', () => {
window.removeEventListener('resize', this.resizeFn)
})
},
resizeFn() {
// console.log(this.$route.path, this.page)
// 注意:在使用keep-alive时,处于其他页面时需要屏蔽,只在当前页面执行 resize,不然会导致图表消失
this.$route.path === this.page && this.chart.resize()
},
activated() {
this.chart && this.chart.resize()// 页面切回来重新计算图表,避免在其他页面resize之后,这边图表还是之前的尺寸
}
5、X轴文字超长处理、X轴文字点击事件绑定
1、首先对formatter显示的文字长度做个截断展示。
axisLabel: {
margin: 18,
rotate: !that.isEmpty ? 45 : 0,
// width: 60,
// overflow: 'truncate',
// ellipsis: '...',
formatter: function (value) {
return (value.length > 5 ? (value.slice(0, 5) + '...') : value)
},
color: '#666'
}
2、然后监听图表的mouseover事件,在鼠标悬浮到文字上时展示完整文字,点击事件同理。
// 扩展echart功能,[X轴文字点击、超长显示...、图表自适应]
extChart(mychart) {
const that = this
// 判断是否创建过div框
const el = document.getElementById('xaxis-hover')
if (!el) {
const el = document.createElement('div')
el.id = 'xaxis-hover'
document.body.appendChild(el)
}
// 监听mouse移入移出事件
mychart.on('mouseover', function (params) {
if (params.componentType === 'xAxis' && params.value !== '-') {
const target = document.querySelector('#xaxis-hover')
target.textContent = params.value
target.style.display = 'inline-block'
document.querySelector('html').onmousemove = function (event) {
var xx = event.pageX - 30
var yy = event.pageY + 20
target.style.left = xx + 'px'
target.style.top = yy + 'px'
}
}
})
mychart.on('mouseout', function (params) {
document.querySelector('#xaxis-hover').style.display = 'none'
})
// 监听click事件
mychart.on('click', function (params) {
document.querySelector('#xaxis-hover').style.display = 'none'
if (params.targetType === 'axisLabel' && params.value !== '-') {
const picker = that.$parent.$parent.$refs.datePicker
that.$store.commit('app/SET_DATA', {
key: 'queryParam',
value: {
customerName: params.value,
picker: {
typeIdx: picker.typeIdx,
curYear: picker.curYear,
// checkQuarterly: 0,
checkMonth: picker.month,
checkWeek: picker.week,
checkDate: picker.date
}
}
})
that.$router.push('/customerSituation')
}
})
// 监听resize事件
window.addEventListener('resize', this.resizeFn)
this.$on('hook:beforeDestroy', () => {
window.removeEventListener('resize', this.resizeFn)
})
}
6、X轴文字hover变色
监听图表实例的 mouseover 和 mouseout 事件,然后动态的修改对应轴里的data配置(通过打印图表配置,可以发现data里是可以支持对象形式的),y 轴同理。
// X轴文字hover高亮
chartHoverXlabel(value, color, isMouseout) {
var option = JSON.parse(JSON.stringify(this.chart.getOption()))
// 深拷贝一份原有配置
const newItem = {
value: value
}
if (!isMouseout) {
newItem.textStyle = {
color
}
}
const index = option.xAxis[0].data.findIndex(item => item === value || item.value === value)
option.xAxis[0].data.splice(index, 1, newItem)
this.chart.setOption(option)
},
// 扩展echart功能,[X轴文字hover颜色]
extChart(mychart) {
const that = this
// 监听mouse移入移出事件
mychart.on('mouseover', function (params) {
if (params.targetType === 'axisLabel' && params.value !== '-') {
that.chartHoverXlabel(params.value, '#1863FB')
}
})
mychart.on('mouseout', function (params) {
if (params.targetType === 'axisLabel') {
that.chartHoverXlabel(params.value, '#666666', true)
}
})
}
7、X轴文字过长时,有的会被隐藏
设置如下 axisLabel 的 interval 属性为 0 即可,注意配合 width 属性。
axisLabel: {
margin: 18,
interval: 0,
width: 40,
fontSize: 10,
color: '#666',
formatter: function (value) {
return value.length > 5 ? (value.slice(0, 5) + '...') : value
}
},
相关参考:
Echart X轴文字旋转最全处理方案
Echart 折线图 鼠标悬浮 拐点增加圆圈强调
8、Y轴数值间距等分问题
Eacharts 中Y轴默认的分段为5,但在渲染的时候会根据数值动态计算,有时候不一定是我们想要的,所以需要我们自己去计算,主要配合的属性为 yAxis 下的 min、max、interval 这几个属性。(主要需求中有多个图表并列展示,数据范围相差比较大,为了统一样式必须统一分段)
- 首先通过 Y 轴的数值获取最小/最大值(注意负数的情况);
- 然后通过分段数计算出Y轴对应的实际最大值边界,这里统一通过函数 getRangeData 处理;
- 最后设置 yAxis 中对应的属性即可。
tips:如果有些特殊情况,需要单独处理,比如Y轴为百分比数值的情况。
如下代码:
// 获取数据范围相关数据,[最小值,最大值,间隔值,范围数组]
getRangeData(data, gap = 5) {
if (data.length) {
if (this.config.title.includes("时效达成率")) {
// 时效达成率相关分类为百分比单位,不需要计算,保证最大值为100%即可
return {
min: 0,
max: 100,
interval: null,
range: null
};
}
const dataMin = Math.min(...data); // 获取数据中最小值
const dataMax = Math.max(...data); // 获取数据中最大值
const min = dataMin > 0 ? 0 : Math.floor(dataMin); // 这里主要是为了处理0.0的问题
// 有如下情况要处理
// dataMin = 0 && dataMax = 0
// dataMin >= 0 && dataMax > 0
// dataMin < 0 && dataMax < 0
// dataMin <= 0 && dataMax >= 0
let interval = null; // 默认间隔
let range = [0]; // 默认初始值为0
if (dataMin === 0 && dataMax === 0) {
return {
min: 0,
max: 100,
interval: null,
range: null
};
}
if (dataMin >= 0 && dataMax > 0) {
interval = dataMax >= 10 ? Math.ceil(dataMax / gap) : dataMax / gap; // 最大值小于10情况下允许小数
}
if (dataMin <= 0 && dataMax >= 0) {
range = [min]; // 最小值为负数,则初始值为向下取整后的负数
const allLen = Math.abs(dataMin) + dataMax;
// interval = allLen >= 10 ? Math.ceil(allLen / gap) : allLen / gap;
interval = Math.ceil(allLen / gap);
// 为了保证范围包含数值,这里使用 Math.ceil 向上取整
}
// 根据最大值和分段等距划分数值
for (let i = 0; i < gap; i++) {
range.push(range[i] + interval);
}
const max = range[range.length - 1];
return {
min,
max,
interval,
range
};
}
return {
min: 0,
max: null,
interval: null,
range: null
};
}
计算相关数据
const { min: minValue, max: maxValue, interval: curInterval } = this.getRangeData(that.config.data);
设置 yAxis 对应属性
yAxis: {
// name: that.config.title, // 这里title位置会跟着Y轴数值移动,已放弃
type: 'value',
min: minValue, // 自己算的最小值
max: maxValue, // 自己根据数据算的上限,设为'dataMax'将以y轴最大值为上限
interval: curInterval, // 自己根据上限和分段数算的间隔值
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
🚀附上完整文件
chart-bar.vue
最终图表效果:
9、X轴左右留白
有时候需要在X轴两侧留有空白,那么设置 xAxis 的 boundaryGap 属性为 true 即可。
xAxis: {
type: 'category',
boundaryGap: true,
offset: 10,
axisLine: {
lineStyle: {
color: '#ccc'
}
},
axisLabel: {
rotate: 45,
fontSize: 12,
color: '#666'
},
data: axisXData
}
如图:
10、多份数据共用Y轴时,动态设置Y轴刻度范围(X轴同理)
-
【背景】:有时项目中会在同一个图表中使用多个数据,比如多个折线图,并且他们是共用X轴和Y轴的,在这种情况下,如果都是正数是没问题的,切换 legend(图例) 也可以。
-
【问题】:但是当我们的数据中有一部分包含负数的时候,图表为了考虑其他数据的展示,会把 0 刻度线往上推,导致含负数的部分显示在X轴以上,含0值的没有贴在X轴上,和其他图表对比起来看就有问题。
-
【分析】:经过查阅 api,发现可以给 yAxis 的配置里添加 min 和 max字段, 手动指定 Y轴的刻度范围,于是我们就可以根据数据提前计算好最大、最小值,然后加入配置即可;还有就是顶部的图例是可以切换的,由于我们给 Y轴配置了固定的大小区间,这里需要监听
legendselectchanged
事件,然后根据选中的数据重新计算最大、最小值,并渲染图表。修改完后如下图:
-
【核心代码】
// 获取Y轴最小/最大值
const { min: baseMinValue, max: baseMaxValue } = this.getBaseVal(seriesData);
// 数值转单位(万)
toWan(data) {
if (data !== null && data !== "") {
if (data >= 10000) {
return (data / 10000).toFixed(1);
}
if (Number(data) !== 0) {
const wanVal = data / 10000;
const strVal = wanVal.toString();
let idx = 0;
if (/\d?\.\d+/.test(strVal)) {
idx = [...strVal.split(".")[1]].findIndex((c) => c > 0);
}
return wanVal.toFixed(idx + 1);
}
return "0.0";
}
return "-";
}
// @获取Y轴最小/最大值
getBaseVal(data) {
const min = Math.min(...data.map(item => {
return Math.min(...item.data)
}))
const max = Math.max(...data.map(item => {
return Math.max(...item.data)
}))
return {
min,
max
}
}
// 监听图例切换事件
that.chart.on('legendselectchanged', (param) => {
const selected = param.selected
const selectedLegend = []
const selectedData = []
const options = that.chart.getOption()
// 1.获取当前选择的数据
for (const key in selected) {
if (selected[key] === true) {
selectedLegend.push(key)
}
}
that.seriesData.forEach(item => {
selectedLegend.forEach(itm => {
if (item.name === itm) {
selectedData.push(item)
}
})
})
// 2.更新最大最小值配置
const {
min,
max
} = that.getBaseVal(selectedData)
options.yAxis.forEach(item => {
item.min = min
item.max = max
})
that.chart.setOption(options)
})
- 【最后】:为了美观,我去掉了Y轴刻度线,附上完整代码:chart-trend.vue
😄 要是每组数据都用单独的Y轴,那简单一些,直接计算出每组数据的最小值和最小值所在索引,然后放入各自的Y轴配置即可。
// @获取数据组中最小值及索引
findMinData(data) {
const innerMinArr = data.map(item => {
return Math.min(...item.data)
})
const min = Math.min(...innerMinArr)
const minIndex = innerMinArr.findIndex(c => c === min)
return {
min,
minIndex
}
}
完整代码附上:chart-trend.vue
11、图表默认勾选特定的图例(legend)
有时候图表默认进来只需要勾选指定的图例,这个我们可以直接通过配置 legend
字段中的 selected
属性实现这个效果,如图:
// >图例数据
const LegendData = ["降水量", "温度", "湿度"];
// >生成默认勾选的图例
const SelectedLegend = {"降水量": false, "温度": false};
// >配置 legend
legend: {
data: LegendData,
selected: SelectedLegend
}
12、Echart 柱状图中正负数值显示不同颜色
有时候,我们需要控制柱状图的条形的颜色,比如正数显示蓝色,负数显示红色。只需要在 series
中把 color
配置为动态的即可。
series: [
{
data: seriesData,
type: "bar",
barMaxWidth: 30,
itemStyle: {
color: fucntion(params) {
return params.value >= 0 ? "#1863FB" : "#ef4b4b";
},
},
},
]
13、取消 Echarts 饼图的动画效果
有时候,我们不需要饼图悬浮时的动画效果。只需要在 series
中把 hoverAnimation
配置为动态的即可。
series: [
{
data: seriesData,
type: "bar",
hoverAnimation: false, // 关闭动画效果
},
]
14、Echarts 饼图设置默认选中项并在中间显示文字
很多时候,我们需要 Echart 饼图中间显示指定的文字,只需要配置 series
下的的 formatter
即可,但是饼图初始状态下并不会展示中间的文字,需要我们悬浮到相关饼图上才会显示,这时需要我们手动通过 setOption
方法给它指定一个默认值。
var myChart = echarts.init(document.getElementById('pie-chart'));
var index = 0;
var option = {
grid: { //设置图表撑满整个画布
top: "12px",
left: "12px",
right: "16px",
bottom: "12px",
containLabel: true
},
series: [
{
name: '访问来源',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
label: {
normal: {
show: false,
position: 'center',
formatter: function (data) { // 设置圆饼图中间文字排版
return data.value + "\n" + "用户数"
}
},
emphasis: {
show: true, //文字至于中间时,这里需为true
textStyle: { //设置文字样式
fontSize: '12',
color: "#333"
}
}
},
labelLine: {
normal: {
show: false
}
},
data: [
{
value: 335,
name: '优秀',
itemStyle: {
color: "#3de16b"
}
},
{
value: 310,
name: '良好',
itemStyle: {
color: "#27baff"
}
},
{
value: 234,
name: '一般',
itemStyle: {
color: "#5865e5"
}
},
{
value: 135,
name: '较差',
itemStyle: {
color: "#fea51a"
}
},
{
value: 1548,
name: '糟糕',
itemStyle: {
color: "#ef5e31"
}
}
]
}
]
};
myChart.setOption(option);
//设置默认选中高亮部分
myChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: 0
});
// 当鼠标移入时,如果不是第一项,则把当前项置为选中,如果是第一项,则设置第一项为当前项
myChart.on('mouseover', function (e) {
myChart.dispatchAction({
type: 'downplay',
seriesIndex: 0,
dataIndex: 0
});
if (e.dataIndex != index) {
myChart.dispatchAction({
type: 'downplay',
seriesIndex: 0,
dataIndex: index
});
}
if (e.dataIndex == 0) {
myChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: e.dataIndex
});
}
});
//当鼠标离开时,把当前项置为选中
myChart.on('mouseout', function (e) {
index = e.dataIndex;
myChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: e.dataIndex
});
});
15、取消/禁用 Echarts 图例的点击事件
有时候,我们需要禁用图例的点击切换功能。只需要在 legend
中把 selectedMode
配置为 false
的即可。
legend: {
selectedMode: false, // 是否允许点击
}
16、设置饼状图 labelLine 的平滑度
在饼图和玫瑰图的场景下,如果想设置图形指示线条的拐角(就是
labelLine
),可以通过smooth
这个属性来配置,这个属性控制线段的圆滑值,取值范围为0-1
,或者直接填布尔值true/false
。
labelLine: {
length: 10,
length2: 20,
smooth: 0.3,
lineStyle: {
width: 2
}
}
具体效果如下图:
对应配置如下:
const series = [
{
name: title,
type: 'pie',
radius: [50, 100],
center: ['50%', '50%'],
roseType: 'radius',
data: data.map((item: any, index: number) => {
return { ...item, label: { color: colorInRoseList[index].color } }
}),
showEmptyCircle: true,
avoidLabelOverlap: true,
minAngle: 10, // 设置每块扇形的最小占比
itemStyle: {
borderRadius: 4
},
label: {
show: true,
position: 'outside',
formatter: function (val) {
let labelText =
totalInPieCenter.value.value > 0
? ((val.data.value / totalInPieCenter.value.value) * 100).toFixed(
2
) + '% '
: '0%'
let unit =
type?.value === EnumPageType.ALIGN_RATE_DETAIL ? '条' : '人'
return labelText + ', ' + val.data.value + unit
}
},
labelLine: {
length: 10,
length2: 20,
smooth: 0.3,
lineStyle: {
width: 2
}
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
},
itemStyle: {
borderRadius: 8,
opcaity: 1
}
}
}
]
17、Echart finished 事件不触发
图表的数据源大都来自后端接口,属于动态数据,如果数据量比较大,我们有必要添加一个
loading
状态,这时候就需要通过Echart
提供的事件钩子onfinished
来实现,加载状态可以通过Echart
实例上的showLoading
和hideLoading
方法来支持。详细文档请参见:
当图表渲染完毕后(包括动画效果),该钩子会被触发。那么我们就可以在这个回调中处理加载状态了。但是在我实际使用过程中,发现当图表数据为 0
或 空
的情况时,由于没有相关图表绘制动画,这个 finished
钩子并不会被执行,导致加载状态一直存在。
经过反复调试无解,回到官方文档,发现了一行小字(稻草),如下
注意:建议在调用
setOption
前注册相关事件,否则在动画被禁用时,注册的事件回调可能因时序问题而不被执行。
通过调整代码顺序,终于解决了这个问题,太坑了,还是得细看文档 🧐。
// 已省略相关代码
loading = true
myChart = echarts.init(chartDom)
// 等待图表渲染完毕(一定要放在setOption之前)
myChart.on("finished", () => {
loading = false
})
myChart.setOption(chartOption, true)
18、Echart 点击事件触发多次
为了实现点击跳转的交互,我们需要给图表绑定
click
事件,但发现点击事件有时会触发多次,导致逻辑错乱。其主要原因是 图表实例所绑定的事件没有及时取消导致的,我们可以和jquery
一样在绑定之前先使用off
来取消事件绑定。
myChart = echarts.init(chartDom)
myChart.on("finished", () => {
myChart.off('click').on('click', function (event) {}
})
19、如何实现动态适配容器宽高
很多时候,我们的图表容器并不是固定宽高的,需要处理自适应的问题。通常我们会通过监听
window.onresize
事件做处理,但是有时候,页面中的内容区域区域并不是跟随外层窗口的变化而变化的,这样我们就只能去监听内容区域的resize
事件了。
但是只有window
对象才有resize
事件,普通标签是无法通过监听resize
事件来处理的。
基于此问题,我们了解到两个可用的 API
,分别是 MutationObserver 和 ResizeObserver,它们都可以监听元素的变化,而且用法相似。如果我们只想监听某个元素的尺寸变化,那就直接用 ResizeObserver
,大家可以参考 MDN 上的具体介绍。(当然也要考虑兼容性)
如果是 vue3 的项目,大家可以直接使用 vueuse
这个工具集中提供的 useElementSize 钩子(相关钩子还有很多),具体使用如下:
<template>
<div ref="el">
Height: {{ height }}
Width: {{ width }}
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useElementSize } from '@vueuse/core'
const el = ref(null)
const { width, height } = useElementSize(el)
watchEffect(() => {
if (width.value && height.value) {
// 更新图表
emits('resize', { width: width.value, height: height.value })
chartRef.value?.resize();
}
})
</script>
因为可以得到元素的 {width, height}
,对于一些依赖绝对尺寸的场景,也可以从容更新 option
,保证 resize
后尺寸不会出现问题。当视窗大小变化后,相关的图表也会重新根据新视窗的大小调整尺寸,相关组件使用示例:
<template>
<BaseECharts @resize="size => onResize(size)" ref="chartRef"></BaseECharts>
</template>
<script setup>
import BaseECharts from '@/components/BaseECharts'
const chartRef = ref(null)
const genOption = ({ width, height }) => {
const minSideLength = Math.min(width, height)
return {
series: [
// ...其他选项省略
{
radius: [0.3 * minSideLength, 0.4 * minSideLength],
center: [minSideLength / 2, minSideLength / 2],
type: 'pie',
}
]
}
}
const onResize = (size) => {
const option = genOption(size)
chartRef.value.setOption(option)
}
</script>
评论区