【Echarts 实战录】常见问题 (二)

2,397 阅读8分钟

一、阴影指示器层级修改

如下代码, 开启了 X 坐标轴阴影指示器, 但是触发 指示器 时, 会发现阴影是覆盖在图表上面的, 因为很明显的可以看到图表的颜色会变浅很多

option = {
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    // 开启坐标指示器
    axisPointer: {
      show: true,
      type: 'shadow',
    }
  },
  yAxis: { type: 'value' },
  series: [
    {
      data: [120, 200, 150, 80, 70, 110, 130],
      type: 'bar',
      // 设置高亮时颜色保存一致
      emphasis: {
        itemStyle: {
          color: 'inherit'
        } 
      }
    }
  ]
};

上面代码效果图如下:

klx.pro.S2FwdHVyZSAyMDIzLTAzLTEwIGF0IDEyLjQwLjUyLmdpZjE2Nzg0MjMyODY1MzY=.gif

这里可以通过修改 阴影指示器z 属性配置, 来设置 指示器 的层级, 使 阴影指示器层级 低于图表即可, 修改代码如下:

option = {
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    // 开启坐标指示器
    axisPointer: {
+     z: -1,
      show: true,
      type: 'shadow',
+     label: { show: false }, // 将 label 隐藏了, 这里的 label 样式会有点问题
    }
  },
  yAxis: { type: 'value' },
  series: [
    {
      data: [120, 200, 150, 80, 70, 110, 130],
      type: 'bar',
      // 设置高亮时颜色保存一致
      emphasis: {
        itemStyle: {
          color: 'inherit'
        } 
      }
    }
  ]
};

调整后效果如下:

klx.pro.S2FwdHVyZSAyMDIzLTAzLTEwIGF0IDEyLjQzLjA0LmdpZjE2Nzg0MjM0MTIxMTE=.gif

二、如何让坐标轴指示器不触发图表的「高亮」「tooltip」?

下面代码中, 开启了 tooltip阴影指示器 同时设置了高亮颜色为 red, 那么当我们触发 阴影指示器 (鼠标没有 hover 图表上)的情况下, 依然会触发图表高亮、以及 tooltip

option = {
  tooltip: { trigger: 'item' },
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    // 开启坐标指示器
    axisPointer: {
      z: -1,
      show: true,
      type: 'shadow',
      label: { show: false }, // 将 label 隐藏了, 这里的 label 样式会有点问
    }
  },
  yAxis: { type: 'value' },
  series: [
    {
      data: [120, 200, 150, 80, 70, 110, 130],
      type: 'bar',
      emphasis: {
        itemStyle: {
          color: 'red'
        } 
      }
    }
  ]
};

上面代码效果图如下:

klx.pro.S2FwdHVyZSAyMDIzLTAzLTEwIGF0IDEyLjU5LjMxLmdpZjE2Nzg0MjQ0MDQ5OTA=.gif

上图可以发现, 我们鼠标 hover 在图表外面, 依然触发了 tooltip, 要知道配置中 tooltip.trigger 可是 item; 这种情况下我们可以通过设置 axisPointer.triggerTooltipfalse, 让 指示器 不触发 tooltip

option = {
  tooltip: { trigger: 'item' },
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    // 开启坐标指示器
    axisPointer: {
      z: -1,
      show: true,
      type: 'shadow',
+     triggerTooltip: false,
      label: { show: false }, // 将 label 隐藏了, 这里的 label 样式会有点问
    }
  },
  yAxis: { type: 'value' },
  series: [
    {
      data: [120, 200, 150, 80, 70, 110, 130],
      type: 'bar',
      emphasis: {
        itemStyle: {
          color: 'red'
        } 
      }
    }
  ]
};

上面代码效果图如下: 在图表之外触发 指示器 不会再显示 tooltip 了, 只有 hover 到图表上面才会触发 tooltip

klx.pro.S2FwdHVyZSAyMDIzLTAzLTEwIGF0IDEzLjE0LjI5LmdpZjE2Nzg0MjUzMTcxNzQ=.gif

同样的, 如果在触发 指示器 时不想触发高亮, 我们又该怎么处理呢? 这里处理起来就相对麻烦点, 整体思路: 创建 2 套相同的坐标体系, 一个作为 series 的坐标轴, 一个则隐藏用于创建 坐标轴指示器, 这样 坐标轴指示器就 就不会触发 series 高亮了, 因为它们不是处于同一个坐标系

+ const xAxisBase = {
+   type: 'category',
+   data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+ }

option = {
  tooltip: { trigger: 'item' },
+ xAxis: [
+   {...xAxisBase},
+   // 隐藏坐标系, 用于设置坐标指示器
+   {
+     ...xAxisBase,
+     show: false,
+     axisPointer: {
+       z: -1,
+       show: true,
+       type: 'shadow',
+       label: { show: false }, // 将 label 隐藏了, 这里的 label 样式会有点问
+     }
+   },
+ ],
+ yAxis: [
+   { type: 'value' },
+   { type: 'value' },
+ ],
  series: [
    {
+     // 设置 serie 使用的是那个坐标系
+     xAxisIndex: 0,
+     yAxisIndex: 0,
      data: [120, 200, 150, 80, 70, 110, 130],
      type: 'bar',
      emphasis: {
        itemStyle: {
          color: 'red'
        } 
      }
    }
  ]
};

上面代码效果图如下: 通过设置 2 个坐标系, serie指示器 处于不同 坐标系, 它们之间就不会产生影响

klx.pro.S2FwdHVyZSAyMDIzLTAzLTEwIGF0IDEzLjM5LjAzLmdpZjE2Nzg0MjY3ODc4Nzk=.gif

三、圆环饼图鼠标悬停时显示文本

很常见的一个饼图交互方式, 官网也有完整的一个 DEMO, 思路就是: 默认情况下隐藏 饼图label 在鼠标 hover 时会触发 饼图 的高亮, 这时将 label 显示出来即可

option = {
  series: [
    {
      type: 'pie',
      radius: ['40%', '70%'],
      // 默认隐藏
      label: {
        show: false,
        position: 'center'
      },
      // 高亮则显示
      emphasis: {
        label: {
          show: true,
          fontSize: 40,
          fontWeight: 'bold'
        }
      },
      data: [
        { value: 735, name: 'Direct' },
        { value: 580, name: 'Email' },
        { value: 484, name: 'Union Ads' },
        { value: 300, name: 'Video Ads' }
      ]
    }
  ]
};

上面代码效果图如下:

klx.pro.S2FwdHVyZSAyMDIzLTAzLTEwIGF0IDEzLjU1LjI1LmdpZjE2Nzg0Mjc3NjY4NzY=.gif

四、 主动触发 Tooltip

如下代码, 通过 Echarts 实例中 dispatchAction 方法, 来触发 action, 其中 tyep 表示 action 类型, seriesIndex dataIndex 分别表示要触发哪个系列、哪条数据的 action, 代码中通过 setTimeout 实现了 tooltip 的轮播

// 轮播器: 轮播每条数据的 tooltip
const carousel = () => {
  // 1. 计算器
  carousel.total = (carousel.total || 0) + 1
  
  // 2. 手动显示 tooltip
  myChart.dispatchAction({
    type: 'showTip',
    seriesIndex: 0,
    dataIndex: carousel.total % 4,
  });
    
  // 3. 清除定时器
  clearTimeout(carousel.timer)
  
  // 4. 重新启动定时器
  carousel.timer = setTimeout(carousel, 1000);
}

// 执行轮播
carousel()

option = {
  tooltip: {
    trigger: 'item'
  },
  series: [
    {
      name: 'Access From',
      type: 'pie',
      radius: ['40%', '70%'],
      itemStyle: {
        borderRadius: 10,
        borderColor: '#fff',
        borderWidth: 2
      },
      data: [
        { value: 735, name: 'Direct' },
        { value: 580, name: 'Email' },
        { value: 484, name: 'Union Ads' },
        { value: 300, name: 'Video Ads' }
      ]
    }
  ]
};

上面代码效果图如下:

klx.pro.S2FwdHVyZSAyMDIzLTAzLTEwIGF0IDIxLjI3LjE1LmdpZjE2Nzg0NTQ4NjUxMjQ=.gif

同时我们也可以通过 dispatchAction 来取消 tooltip

// 取消所有高亮设置
echart.dispatchAction({
  type: 'downplay',
  seriesIndex: 0,
  seriesName: '告警处置占比',
});

关于更多 dispatchAction 的使用细节, 请参考 官网

五、为坐标轴绑定事件

在实际项目中, 我们可能需要点击 坐标轴label 跳转到新页面, 这时我们可能会为 Echarts 实例绑定 click 事件, 但事实上会发现点击 坐标轴label 是无法触发 click 事件的, 如下代码: 为 为 Echarts 实例绑定了 click 事件, 会发现点击事件并不能生效

myChart.on('click', params => {
  if(params.targetType === 'axisLabel'){
    alert(`点击 label: ${params.value}`)
  }
})

option = {
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value',
  },
  series: [
    {
      data: [150, 230, 224, 218, 135, 147, 260],
      type: 'line'
    }
  ]
};

那么我们是不是可以认为 Echarts 是无法为坐标轴绑定事件呢? 其实不是的, 坐标轴也是能够触发事件的, 只是默认是关闭的, 我们需要通过 yAxis.triggerEvent 配置来启用它

myChart.on('click', params => {
  if(params.targetType === 'axisLabel'){
    alert(`点击 label: ${params.value}`)
  }
})

option = {
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
+   triggerEvent: true,
    type: 'value',
  },
  series: [
    {
      data: [150, 230, 224, 218, 135, 147, 260],
      type: 'line'
    }
  ]
};

参考来源: eCharts xAxis labels click event

六、坐标轴 label 超出显示省略号「hover」 展示全部信息

这里我们需要完成两个功能: 第一个就是需要针对长字符串进行一个截取, 字符串截取方法如下: 方法的主要功能就是针对字符串、以及给定的长度, 对字符串进行处理, 如果字符串过长则会进行截取, 并显示 ...

/**
 * 字符串截取
 * @param {String} str 源字符串
 * @param {Number} len 截取长度, 双字节字符按 2 个长度进行计算
 * @param {String} [ellipsis='...'] 尾缀
 * @returns 截取后的字符串 + 尾缀
 */
const sliceStr = ({
  str,
  len,
  padStr,
  ellipsis = '...',
}) => {
  // 1. 空值处理
  if (!str || !len) { return '' }

  // 2. 计算字符串长度: 双字节按 2 个长度计算
  const strLen = str.replace(/[^\x00-\xff]/gi, 'aa').length

  // 3. 判断字符串是否超出指定的长度
  if (strLen <= len) {
    return padStr ? `${''.padStart(len - strLen, padStr)}${str}` : str
  }

  // 4. 循环每个字符进行算计
  let newStr = ''
  let num = ellipsis.length
  for (let i = 0, lens = str.length; i < lens; i++) {
    num += str.charCodeAt(i) > 255 ? 2 : 1

    if (num > len) {
      break
    } else {
      newStr = str.substring(0, i + 1)
    }
  }

  return `${newStr}${ellipsis}`
}

sliceStr({ str: '索非亚皮革制品', len: 7 }) // 索非...

Echarts 中使用 sliceStr 来截取 label 长度: 如下代码针对 y 坐标轴的 label 进行了截取

/**
 * 字符串截取
 * @param {String} str 源字符串
 * @param {Number} len 截取长度, 双字节字符按 2 个长度进行计算
 * @param {String} [ellipsis='...'] 尾缀
 * @returns 截取后的字符串 + 尾缀
 */
const sliceStr = ({
  str,
  len,
  padStr,
  ellipsis = '...',
}) => {
  // 1. 空值处理
  if (!str || !len) { return '' }

  // 2. 计算字符串长度: 双字节按 2 个长度计算
  const strLen = str.replace(/[^\x00-\xff]/gi, 'aa').length

  // 3. 判断字符串是否超出指定的长度
  if (strLen <= len) {
    return padStr ? `${''.padStart(len - strLen, padStr)}${str}` : str
  }

  // 4. 循环每个字符进行算计
  let newStr = ''
  let num = ellipsis.length
  for (let i = 0, lens = str.length; i < lens; i++) {
    num += str.charCodeAt(i) > 255 ? 2 : 1

    if (num > len) {
      break
    } else {
      newStr = str.substring(0, i + 1)
    }
  }

  return `${newStr}${ellipsis}`
}


+ option = {
+   xAxis: {
+     type: 'value',
+     boundaryGap: [0, 0.01]
+   },
+   yAxis: {
+     type: 'category',
+     data: ['前端算法', '即时通讯技术', '前端可视化'],
+     // 关键代码: 针对 y 坐标轴 label 进行格式化
+     axisLabel: {
+       formatter: (value) => sliceStr({ str: value, len: 7 })
+     }
+   },
+   series: [
+     {
+       name: '2012',
+       type: 'bar',
+       data: [19325, 23438, 31000]
+     }
+   ]
+ };

上面代码效果图如下:

klx.pro.tinify.aW1hZ2UucG5nMTY3ODQ5OTU5NjU0MQ==.png

接下来我们要做的就是实现: 鼠标 hover 展示, 完整信息, 这里思路其实也很简单:

  1. Echarts 绑定 鼠标移入、移出事件
  2. 鼠标 hover 到坐标轴 label 上时, 判断展示文本和实际值是否相等, 不等则展示完整信息
/**
 * 字符串截取
 * @param {String} str 源字符串
 * @param {Number} len 截取长度, 双字节字符按 2 个长度进行计算
 * @param {String} [ellipsis='...'] 尾缀
 * @returns 截取后的字符串 + 尾缀
 */
const sliceStr = ({ str, len, padStr, ellipsis = '...' }) => {
  // 1. 空值处理
  if (!str || !len) {
    return '';
  }

  // 2. 计算字符串长度: 双字节按 2 个长度计算
  const strLen = str.replace(/[^\x00-\xff]/gi, 'aa').length;

  // 3. 判断字符串是否超出指定的长度
  if (strLen <= len) {
    return padStr ? `${''.padStart(len - strLen, padStr)}${str}` : str;
  }

  // 4. 循环每个字符进行算计
  let newStr = '';
  let num = ellipsis.length;
  for (let i = 0, lens = str.length; i < lens; i++) {
    num += str.charCodeAt(i) > 255 ? 2 : 1;

    if (num > len) {
      break;
    } else {
      newStr = str.substring(0, i + 1);
    }
  }

  return `${newStr}${ellipsis}`;
};

+ // 添加 dom 用于放置 tip 信息
+ const tip = document.createElement('div')
+ document.body.appendChild(tip)
+ 
+ // 鼠标移入事件
+ myChart.on('mouseover', ({ event, value, componentType, targetType }) => {
+   const {
+       target,
+       event: { clientX, clientY }
+     } = event;
+ 
+   // 判断条件: 在 y 轴触发 && 触发类型为坐标轴 label && 展示内容和 value 不一致
+   if (componentType === 'yAxis' && targetType === 'axisLabel' && target?.parent?.style?.text !== value) {
+     // 显示 tip
+     tip.innerHTML = `
+       <div style="
+         position: fixed; 
+         top: ${clientY}px; 
+         left: ${clientX + 40}px; 
+         padding: 5px;
+         border-radius: 4px;
+         background: #f5f5f5; 
+       ">
+         ${value}
+       </div>
+     `
+   }
+ });
+ 
+ // 鼠标移出事件
+ myChart.on('mouseout', ({ event, value, componentType, targetType }) => {
+   // 判断条件: 在 y 轴触发 && 触发类型为坐标轴 label
+   if (componentType === 'yAxis' && targetType === 'axisLabel') {
+     tip.innerHTML = '' // 移除 tip
+   }
+ });

option = {
  xAxis: {
    type: 'value',
    boundaryGap: [0, 0.01]
  },
  yAxis: {
    type: 'category',
    triggerEvent: true,
    data: ['前端算法', '即时通讯技术', '前端可视化'],
    // 关键代码: 针对 y 坐标轴 label 进行格式化
    axisLabel: {
      formatter: (value) => sliceStr({ str: value, len: 7 })
    }
  },
  series: [
    {
      name: '2012',
      type: 'bar',
      data: [19325, 23438, 31000]
    }
  ]
};

上面代码效果图如下:

klx.pro.S2FwdHVyZSAyMDIzLTAzLTExIGF0IDEwLjU5LjM1LmdpZjE2Nzg1MDM2MDA4NDE=.gif

七、修改鼠标手势 ✋🏻

在实现各种交互过程中, 我们可能需要修改鼠标的 手势, 这里可以直接通过 Echarts 实例方法 getDom() 找到图表的容器, 然后通过操作 DOM 方式修改 canvas 的样式

const [canvasDom] = myChart.getDom().getElementsByTagName('canvas')

// 设置鼠标手势
if (canvasDom) {
  canvasDom.style.cursor = 'default'
}

八、显示 tooltip 时, 不触发高亮状态

下面代码 tooltip 触发模式为 axis, 这种情况下, 触发 Tooltip 时会同时触图表的高亮

option = {
  tooltip: {
    trigger: 'axis',
  },
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      data: [120, 200, 150, 80, 70, 110, 130],
      type: 'bar',
      emphasis: {
        itemStyle: {
          color: 'red'
        } 
      }
    }
  ]
};

上面代码效果图如下:

klx.pro.S2FwdHVyZSAyMDIzLTAzLTExIGF0IDExLjU2LjUzLmdpZjE2Nzg1MDcwNDM1ODY=.gif

那么, 如果想要在触发 tooltip 时不触发 高亮, 在 hover 到图表上再触发 高亮, 我们又该怎么处理呢? 实现思路有:

  1. toltip.formatter 中手动取消掉图表的高亮
  2. Echarts 绑定 mousemove 事件, 当鼠标 hover 到图表上, 手动触发高亮
+ // 单独触发高亮
+ myChart.on('mousemove', ({ dataIndex, seriesIndex }) => {
+   myChart.dispatchAction({
+     dataIndex,
+     seriesIndex,
+     type: 'highlight',
+   })
+ })

option = {
  tooltip: {
    trigger: 'axis',
+   formatter: (args) => {
+     myChart.dispatchAction({
+       type: 'downplay',
+     })
+     return '1111' 
+   },
  },
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      data: [120, 200, 150, 80, 70, 110, 130],
      type: 'bar',
      emphasis: {
        itemStyle: {
          color: 'red'
        } 
      }
    }
  ]
};

上面代码效果图如下:

klx.pro.S2FwdHVyZSAyMDIzLTAzLTExIGF0IDEyLjE2LjQxLmdpZjE2Nzg1MDgyMzkwMzg=.gif

九、简单地图绘制

这里主要是介绍下 Echarts 5.3 版本之后, 如何注册地图组件, 这里之所以要单独进行讲解, 主要是因为 5.3 版本之后为了减少打包的体积, 官方在核心模块中移除了地图数据管理的方法 getMapregisterMap, 而目前网上找的案例大多是旧版本的实现方式, 导致很多 DEMO 在新版本都跑不起来, 下面是在 React 上的一个精简的演示代码, 更多细节可以翻看 官方文档

import { MapChart } from 'echarts/charts'
import * as echarts from 'echarts/core'

// 1. 注册地图组件
echarts.use([MapChart])


const render = useCallback(async () => {
  // 2. 读取地图数据
  const geo = await request({
    url: '地图数据',
  })

  // 3. 注册地图数据: HK 地图名(自定义)、geo 地理数据
  echarts.registerMap('HK', geo)
  
  // 4. 设置 options
  echarts.setOption({
    visualMap: {
      min: 800,
      max: 50000,
      text: ['High', 'Low'],
      realtime: false,
      calculable: true,
      inRange: {
        color: ['lightskyblue', 'yellow', 'orangered']
      }
    },
    series: [
      {
        name: '香港18区人口密度',
        type: 'map',
        map: 'HK',
        label: {
          show: true
        },
        data: [
          { name: '中西区', value: 20057.34 },
          { name: '湾仔', value: 15477.48 },
        ],
      }
    ]
  })
}, [])
  1. 官方文档: registerMap 和 getMap 方法需要在引入地图组件后才能使用
  2. 地图数据获取: DataV.GeoAtlas地理小工具系列

十、自定义导出图片

Echarts 内置有 导出图片, 数据视图, 动态类型切换数据区域缩放重置 等工具, 可通过 toolbox 进行配置; 但是有个限制就是, 工具箱是集成在图表上的, 而且样式定制起来相对也比较麻烦, 同时实际开发中更多的可能需要我们进行手动导出、批量导出

自定义导出图片其实也简单 Ecahrts 实例中提供了方法 getDataURL(), 可获取到图片的 DataUrl

// 获取图表 DataUrl
const imgDataUrl = .getDataURL({
  pixelRatio: 2,
  backgroundColor: '#fff',
})

最后可以使用第三方库 file-saver 来导出图片

+ import { saveAs } from 'file-saver'

// 获取图表 DataUrl
const imgDataUrl = .getDataURL({
  pixelRatio: 2,
  backgroundColor: '#fff',
})

+ // 导出
+ saveAs(imgData, 'export.png')

如果您不想使用第三方库, 可参考下面代码进行导出

/**
 * 手动下载
 * @param {Object} echarts Echarts 实例
 * @param {String} name 下载的图片名称(不带后缀)
 * @param {String} [type = png] 下载文件类型 png, jpeg
 * @param {Number} [pixelRatio = 2] 导出的图片分辨率比例
 * @param {String} [backgroundColor = #fff] 导出图片背景色
 * @returns
 */
export const download = (echarts) => ({
  name,
  type = 'png',
  pixelRatio = 2,
  backgroundColor = '#fff',
}) => {
  // 获取画布图表地址信息
  const imgUrl = echarts.getDataURL({
    type,
    pixelRatio,
    backgroundColor,
  })

  // 如果浏览器支持 msSaveOrOpenBlob 方法(也就是使用IE浏览器的时候)那么调用该方法去下载图片
  if (window.navigator.msSaveOrOpenBlob) {
    // 1. 截取 base64 的数据内容 (掉前面的描述信息, 类似这样的一段: data:image/png;base64) 并解码为2进制数据
    const bstr = atob(imgUrl.split(',')[1])

    // 2. 获取解码后的二进制数据的长度, 用于后面创建二进制数据容器
    let bstrLength = bstr.length

    // 3. 创建一个 Uint8Array 类型的数组以存放二进制数据
    const u8arr = new Uint8Array(bstrLength)

    // 4. 将二进制数据存入 Uint8Array 类型的数组中
    while (bstrLength--) {
      u8arr[bstrLength] = bstr.charCodeAt(bstrLength)
    }

    // 5. 创建 blob 对象
    const blob = new Blob([u8arr])

    // 6. 调用浏览器的方法, 调起 IE 的下载流程
    window.navigator.msSaveOrOpenBlob(blob, `${name}.${type}`)
  } else {
    // 类 chrome 浏览器创建一个 a 标签并使用 a 标签的 href 属性下载
    const aElement = document.createElement('a')
    aElement.setAttribute('href', imgUrl)
    aElement.setAttribute('download', `${name}.${type}`)
    aElement.dispatchEvent(new MouseEvent('click'))
  }
}

大家好, 我是墨渊君, 如果您喜欢我的文章可以: