Echarts系列之客户让我画标线

2,120 阅读6分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

需求降临

客户:我想要在这图表加一个功能 -- 点击的时候加一条红线。

我(内心):这加条红线不就是配置里markLine一加就收工了嘛,轻轻松松。

我(内心-产品思维):事情肯定没那么简单!你知道这条线是横的还是竖的吗?画这条线的目的是什么吗?哔哩吧啦~~

需求沟通

因为我们是做数据分析的图表展示,因此我给了以下两种方案:

  • 方案1:画横向线(y轴),只画一次,一般用于设置预警线和平均值,方便客户查看数据在该线上的波动情况。

  • 方案2:画竖向线(x轴),每次点击更新一次,一般用于展示当前多个线段的数据。

  • 方案3:已有的tooltips功能能否满足效果

在经过与客户沟通后,决定是做方案2

最终实现效果

不吊胃口,先放最终实现效果图,效果是类似固定的tooltips,有兴趣的可以往下看~(以下为模拟的数据)

1.gif

第一步:画标线

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值
          }
        ]
      }
  }
]

以上配置的效果图如下:

image.png

第二步:点击事件处理

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

以上效果如下:

2.gif

第三步:画数据层

Tag:label

原理:每一个markLine都有一个label,我们利用label属性进行个性化配置

我们的目标是制作一个居中的数据层,因此label肯定要在中间的

  • 坑点1:但根据官网的文档,只有position: middle时能使label居中,但文字却是侧边显示, 而且文档markLine的配置里也没有其他属性可以使label调整成正位置。

image.png

  • 解决1:在经过一番查找,发现有人使用了rotate属性,我已看,这不是刚好符合需求,于是在文档里找,发现是markPoint(设置标记点)的属性,因此,测试用该配置,确实能实现效果,这也给我提供了思路:之后配置可以在功能类似的配置项里去查找

文字改了正的方向后,需要的是偏移,在markPoint配置里找到了偏移的属性,因此,雏形大概出来了

image.png

第四步:数据层样式处理

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

效果如下:

image.png

第五步:数据层位置处理

Tag: offset

offset: [10, 0] : 其中数组的第0位是x方向的偏移,数组的第1位是y方向的偏移

  • 目的:数据层实现居中的效果
  • 问题1:居中 -- 数据的高度是由多少类线段(series数组的长度)决定
  • 问题2:点击时如果数据层宽度超过整个容器的宽度(部分会被遮挡),需靠左边显示

问题效果如下图所示: image.png

问题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
}

效果如下所示

image.png

最终配置项及代码

<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问题的思路:

对于一个新的需求:

  1. 自己先根据公司/部门业务进行分析,心里大概有实现后的样子
  2. 查看现有功能是否符合需求
  3. 与客户进行沟通!!!

Echarts问题

  1. 主要还是查看官方文档的配置项为主
  2. 遇到文档里没有的配置可查看类似的配置项

下次遇到需求,不要盲目接到需求就开始做哦~

感谢你的观看,文章中所涉及的代码,有更好的实现方式各位大佬也可在评论区一起探讨~