「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
需求降临
客户:我想要在这图表加一个功能 -- 点击的时候加一条红线。
我(内心):这加条红线不就是配置里markLine一加就收工了嘛,轻轻松松。
我(内心-产品思维):事情肯定没那么简单!你知道这条线是横的还是竖的吗?画这条线的目的是什么吗?哔哩吧啦~~
需求沟通
因为我们是做数据分析的图表展示,因此我给了以下两种方案:
-
方案1:画横向线(y轴),只画一次,一般用于设置预警线和平均值,方便客户查看数据在该线上的波动情况。
-
方案2:画竖向线(x轴),每次点击更新一次,一般用于展示当前多个线段的数据。
-
方案3:已有的tooltips功能能否满足效果
在经过与客户沟通后,决定是做方案2
最终实现效果
不吊胃口,先放最终实现效果图,效果是类似固定的tooltips,有兴趣的可以往下看~(以下为模拟的数据)
第一步:画标线
Tag:markLine
原理:使用了echarts的markLine配置项,markLine是在series配置下的
series: [
{
name: '邮件营销',
type: 'line',
stack: '总量',
data: [120, 132, 101, 134, 90, 230, 210],
markLine: { // 标线
symbol: 'none', // 标线两端的标记类型
animation: false, // 是否开启动画
silent: true, // 图形是否不响应和触发鼠标事件,默认为 false,即响应和触发鼠标事件。
data: [
{
lineStyle:{ // 标线样式
type:"solid",
color:"rgb(203 65 65)",
width: 2
},
xAxis: '周二' // 标线的x值
}
]
}
}
]
以上配置的效果图如下:
第二步:点击事件处理
Tag:click、getZr
因为需求是需要动态的更新标线,因此需要绑定点击事件,更新配置中的值。
-
坑点:Echarts的API中click事件作用于对线上的点,点击其他空白区域没反应。
-
解决:经过一番搜索后,使用 getZr() 方法可解决该问题(注:此方法官网的API并未记录,但源码中有)
该方法用于获取点击时的像素点。然后通过containPixel方法,把像素点转化为坐标系数据,因此可以实现点击任意地方(包括阴影部分)。
获取坐标轴索引代码如下:
chart.getZr().on('click', async (params) => {
const pointInPixel = [params.offsetX, params.offsetY]
if (chart.containPixel('grid', pointInPixel)) {
let xindex = chart.convertFromPixel({ seriesIndex: 0 }, pointInPixel)[0]
// xindex是数据index
// todo...
}
})
完整更新标线代码
const xData = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
chart.getZr().on('click', async (params) => {
const pointInPixel = [params.offsetX, params.offsetY]
if (chart.containPixel('grid', pointInPixel)) {
const xindex = chart.convertFromPixel({ seriesIndex: 0 }, pointInPixel)[0]
const chartOption = chart.getOption(); // 获取图表当前所有配置
const setSelectSeries = chartOption.series[0]; // 获取图表series配置
let markLineArr = setSelectSeries.markLine.data ? setSelectSeries.markLine.data : []
if (markLineArr.length > 0) {
// 根据markType删除之前的线
markLineArr = markLineArr.filter((item) => item.markType !== 'select')
}
markLineArr.push({
markType: 'select', // 使用自定义markType标记
silent:false,
lineStyle:{
type:"solid",
color:"rgb(203 65 65)",
width: 2
},
xAxis: xData[xindex].toString() // 这里要用字符串,数字类型的话是索引
})
setSelectSeries.markLine.data = markLineArr
chart.setOption({ series: chartOption.series })
}
})
以上效果如下:
第三步:画数据层
Tag:label
原理:每一个markLine都有一个label,我们利用label属性进行个性化配置
我们的目标是制作一个居中的数据层,因此label肯定要在中间的
- 坑点1:但根据官网的文档,只有
position: middle
时能使label居中,但文字却是侧边显示, 而且文档markLine的配置里也没有其他属性可以使label调整成正位置。
- 解决1:在经过一番查找,发现有人使用了
rotate
属性,我已看,这不是刚好符合需求,于是在文档里找,发现是markPoint(设置标记点)的属性,因此,测试用该配置,确实能实现效果,这也给我提供了思路:之后配置可以在功能类似的配置项里去查找
文字改了正的方向后,需要的是偏移,在markPoint配置里找到了偏移的属性,因此,雏形大概出来了
第四步:数据层样式处理
Tag: rich
原理:可以使用label的各种属性对样式进行调整,其中rich的使用把展示的值
修改为{a|展示的值}
,然后在rich中配置a
的样式即可,rich中具体能改动的样式参考这里。
配置如下
label: {
position:'middle', // 设置label的位置
formatter: labelStr, // 内容
rotate: 0, // 内容的旋转角度
offset: [10, 0], // 内容偏移
backgroundColor: 'rgba(71, 71, 71, 0.8)', // 背景颜色
align: 'left', // 内容对其方式
color: 'rgba(255, 255, 255, 1)',
padding: 10,
borderRadius: 4,
borderWidth: 1,
rich: { // 富文本样式调整
a: {
fontSize: 14,
padding: [10, 0, 0, 0]
},
b: {
fontSize: 14,
}
}
}
效果如下:
第五步:数据层位置处理
Tag: offset
offset: [10, 0] : 其中数组的第0位是x方向的偏移,数组的第1位是y方向的偏移
- 目的:数据层实现居中的效果
- 问题1:居中 -- 数据的高度是由多少类线段(series数组的长度)决定
- 问题2:点击时如果数据层宽度超过整个容器的宽度(部分会被遮挡),需靠左边显示
问题效果如下图所示:
问题1解决思路: 根据series数组的长度动态计算数据层的偏移值。
const seriesLen = chartOption.series.length;
const countOffset = [10, seriesLen * 16] // 16为文字的高度
问题2解决思路 根据文字的最大的长度计算出整个数据层的宽度,然后设置对应的负的偏移。
let strLen = 0; // 数据最大长度
chartOption.series.forEach((item, idx) => {
let valueLen = `${item.name}:${item.data[xindex]}`.length;
if (valueLen > strLen) {
strLen = valueLen
}
})
const seriesLen = chartOption.series.length // series数组长度
const chartWidth = chart.getWidth() // 当前图表宽度
const labelWidth = strLen * 12 // label宽度,12为文字宽度
const isLeft = (chartWidth - params.offsetX) < labelWidth ? true : false // 根据label宽度判断数据层是否在左边
const countOffset = [isLeft ? -labelWidth - 30 : 10, seriesLen * 16] // 30为间隙值,该值可自行尝试
关键配置项
label: {
width: labelWidth,
offset: countOffset
}
效果如下所示
最终配置项及代码
<script>
option = {
title: {
text: '折线图堆叠'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
name: '邮件营销',
type: 'line',
stack: '总量',
data: [120, 132, 101, 134, 90, 230, 210],
markLine: {
symbol: 'none', // 标线两端的标记类型
animation: false, // 是否开启动画
silent: true, // 图形是否不响应和触发鼠标事件,默认为 false,即响应和触发鼠标事件。
}
},
{
name: '联盟广告',
type: 'line',
stack: '总量',
data: [220, 182, 191, 234, 290, 330, 310]
},
{
name: '视频广告',
type: 'line',
stack: '总量',
data: [150, 232, 201, 154, 190, 330, 410]
},
{
name: '直接访问',
type: 'line',
stack: '总量',
data: [320, 332, 301, 334, 390, 330, 320]
},
{
name: '搜索引擎',
type: 'line',
stack: '总量',
data: [820, 932, 901, 934, 1290, 1330, 1320]
}
]
};
let chart = echarts.init(document.getElementById('chart'))
chart.setOption(option)
const xData = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
chart.getZr().on('click', async (params) => {
const pointInPixel = [params.offsetX, params.offsetY]
if (chart.containPixel('grid', pointInPixel)) {
const xindex = chart.convertFromPixel({ seriesIndex: 0 }, pointInPixel)[0]
const chartOption = chart.getOption(); // 获取图表当前所有配置
const setSelectSeries = chartOption.series[0]; // 获取图表series配置
let markLineArr = setSelectSeries.markLine.data ? setSelectSeries.markLine.data : []
if (markLineArr.length > 0) {
// 根据markType删除之前的线
markLineArr = markLineArr.filter((item) => item.markType !== 'select')
}
const xVal = xData[xindex].toString()
let labelStr = `{b|${xVal}}\n`
let strLen = 0;
chartOption.series.forEach((item, idx) => {
let valueLen = `${item.name}:${item.data[xindex]}`.length;
if (valueLen > strLen) {
strLen = valueLen
}
labelStr += `{a|${item.name}:${item.data[xindex]}${idx + 1 === chartOption.series.length ? '' : '\n'}}`
})
const seriesLen = chartOption.series.length // series数组长度
const chartWidth = chart.getWidth() // 当前图表宽度
const labelWidth = strLen * 12 // label宽度
const isLeft = (chartWidth - params.offsetX) < labelWidth ? true : false // 根据label宽度判断数据层是否在左边
const countOffset = [isLeft ? -labelWidth - 30 : 10, seriesLen * 16] // 30为间隙值,该值可自行尝试
markLineArr.push({
markType: 'select', // 使用自定义markType标记
silent:false,
lineStyle:{
type:"solid",
color:"rgb(203 65 65)",
width: 2
},
label: {
width: labelWidth,
position:'middle',
formatter: labelStr,
rotate: 0,
offset: countOffset,
backgroundColor: 'rgba(71, 71, 71, 0.8)',
align: 'left',
color: 'rgba(255, 255, 255, 1)',
padding: 10,
borderRadius: 4,
borderWidth: 1,
rich: { // 调整样式
a: {
fontSize: 14,
padding: [10, 0, 0, 0]
},
b: {
fontSize: 14,
}
},
},
xAxis: xVal // 这里要用字符串,数字类型的话是索引
})
setSelectSeries.markLine.data = markLineArr
chart.setOption({ series: chartOption.series })
}
})
</script>
结语
本篇文章技术点比较少,主要还是想分享前端工程师处理需求和解决Echarts问题的思路:
对于一个新的需求:
- 自己先根据公司/部门业务进行分析,心里大概有实现后的样子
- 查看现有功能是否符合需求
- 与客户进行沟通!!!
Echarts问题
- 主要还是查看官方文档的配置项为主
- 遇到文档里没有的配置可查看类似的配置项
下次遇到需求,不要盲目接到需求就开始做哦~
感谢你的观看,文章中所涉及的代码,有更好的实现方式各位大佬也可在评论区一起探讨~