一、项目介绍
最近做项目,甲方要求在柱状图里加上水波图的效果,真是太有趣了,太有创意啦。马上动手实现。

思路
echart提供了一个renderItem方法,可以帮助我们自定义每个series的配置。我们分析柱状图跟水波图,就是由一个矩形跟一个波浪路径组成,因此我们可以返回一个图形数组来表现当前项。
二、了解renderItem
renderItem 函数是自定义系列的核心,它负责将数据项(dataItem)转换为可视化的图形元素。ECharts 会为 series.data 中的每个数据项调用一次 renderItem 函数。主要优势在于:
- 可以自由绘制各种图形元素
- ECharts 会自动管理图形的创建、删除、动画等细节
- 可以与其他组件(如 dataZoom、visualMap)无缝联动
参数
renderItem 函数接收两个参数:params 和 api
//params 包含当前数据信息和坐标系信息:
{
    context: {},       // 可供开发者暂存东西的对象
    seriesId: string,  // 本系列 ID
    seriesName: string, // 本系列名称
    seriesIndex: number, // 本系列索引
    dataIndex: number,  // 数据项在原始数据中的索引
    dataIndexInside: number, // 数据项在当前可见数据窗口中的索引
    dataInsideLength: number, // 当前可见数据长度
    coordSys: {        // 坐标系信息,类型不同结构也不同
        type: 'cartesian2d' | 'polar' | 'geo' | 'calendar' | 'singleAxis',
        // 不同坐标系下的具体属性...
    }
}
api 参数提供了一系列方法:
- api.value(index)- 获取数据项中指定维度的值
- api.coord(valueArray)- 将数据值转换为坐标系上的点
- api.size(valueArray)- 获取坐标系上一段数值范围对应的像素长度
- api.style(styleOverrides)- 获取或覆盖默认样式
返回值
renderItem 函数需要返回一个图形元素定义对象
{
    type: string,       // 图形类型,如'rect','circle','sector','polygon'等
    shape: object,      // 图形形状定义
    style: object,      // 图形样式
    extra: object,      // 额外信息,可在事件处理器中访问
    children: array,    // 子图形(当type为'group'时)
    // 其他可选属性...
}
三、实现
首先要实现基本配置,柱子的高度为数据的总数,水波的位置为已处理数据,提示窗展示名称、已处理、未处理、总数等数据。
// 数据配置 - 包含已处理和未处理的数据
const chartData = [
  { name: "车辆只进不出", processed: 60, unprocessed: 62, total: 122 },
  { name: "预警模型2", processed: 72, unprocessed: 82, total: 154 },
  { name: "预警模型3", processed: 71, unprocessed: 91, total: 162 }
];
// 创建水波柱状图配置
const createOption = () => {
  return {
    backgroundColor: 'transparent', //echart背景为透明
    animation: false,
    tooltip: { //提示窗配置
      trigger: 'axis',
      axisPointer: {
        type: 'shadow'
      },
      formatter: function(params) {
        const data = chartData[params[0].dataIndex];
        return `${params[0].name}<br/>已处理: ${data.processed}<br/>未处理: ${data.unprocessed}<br/>总计:${data.total}`;
      }
    },
    grid: { //图表位置占比配置,尽量居中
      left: '3%',
      right: '4%',
      bottom: '12%',
      top:'5%',
      containLabel: true
    },
    xAxis: {  //配置x轴
      type: 'category',
      data: chartData.map(item => item.name),
      axisLabel: {
        color: '#fff',
        fontSize: 12
      },
      axisLine: {
        lineStyle: {
          color: '#fff'
        }
      }
    },
    yAxis: { //配置y轴
      type: 'value',
      axisLabel: {
        color: '#fff',
        fontSize: 12,
      },
      axisLine: {
        lineStyle: {
          color: '#fff'
        }
      },
      splitLine: {
        lineStyle: {
          color: 'rgba(255, 255, 255, 0.1)'
        }
      }
    },
    series:series //配置数据项
  };
};
自定义数据项,通过rederItem方法返回一个矩形跟一个波浪路径
// 水波动画时间
let animationTime = 0;
let series = [
{
    name: '水波柱状图',
    type: 'custom',
    renderItem: (params, api) => {
      const categoryIndex = api.value(0); //当前项索引
      const totalValue = api.value(1); //当前项的值
      const processedValue = chartData[categoryIndex].processed; //已完成的值
      const start = api.coord([api.value(0), 0]); //开始的坐标位置,返回[x,y]坐标
      const end = api.coord([api.value(0), totalValue]); //结束的坐位位置
      const height = end[1] - start[1]; //高度
      const width = 40; //宽度
      const rectShape = {  //定义矩形的形状
        x: start[0] - width / 2, 
        y: start[1],
        width: width,
        height: height
      };
      // 计算水波位置 - 基于已处理数量占总数的比例
      const waterLevel = processedValue / totalValue;
      const wavePath = createWavePath(rectShape, waterLevel, animationTime);
      return { //返回值
        type: 'group',
        children: [
          {
            type: 'rect',  //矩形
            shape: rectShape,
            style: {
              fill: {
                  type: 'linear',
                  x: 0,
                  y: 0,
                  x2: 0,
                  y2: 1,
                  colorStops: [
                    { offset: 0, color: 'rgba(24, 144, 255, 0.8)' },
                    { offset: 0.5, color: 'rgba(64, 169, 255, 0.6)' },
                    { offset: 1, color: 'rgba(9, 109, 217, 0.4)' }
                  ]
                },
              stroke: 'rgba(24, 144, 255, 0.3)',
              lineWidth: 1
            }
          },
          {
            type: 'path',  //水波路径
            shape: {
              pathData: wavePath
            },
            style: {
              fill: {
                type: 'linear',
                x: 0,
                y: 0,
                x2: 0,
                y2: 1,
                colorStops: [
                  { offset: 0, color: 'rgba(24, 144, 255, 0.8)' },
                  { offset: 0.5, color: 'rgba(64, 169, 255, 0.6)' },
                  { offset: 0.5, color: 'rgba(64, 169, 255, 0.6)' },
                  { offset: 1, color: 'rgba(9, 109, 217, 0.4)' }
                ]
              }
            },
            z: 10
          }
        ]
      };
    },
    data: chartData.map(item => item.total),
    z: 10
  }
]
实现水波的方法,生成一个svg的路径。SVG 路径字符串是描述矢量图形的重要方式,下面我将详细介绍如何生成 SVG 路径字符串
基本 SVG 路径命令
| 命令 | 含义 | 示例 | 
|---|---|---|
| M | 移动到 (MoveTo) | M 10,20 | 
| L | 直线到 (LineTo) | L 30,40 | 
| C | 三次贝塞尔曲线 (Cubic Bezier) | C x1,y1 x2,y2 x,y | 
| Q | 二次贝塞尔曲线 (Quadratic Bezier) | Q x1,y1 x,y | 
| Z | 闭合路径 (ClosePath) | Z | 
// 创建水波路径
const createWavePath = (rect, waterLevel, time) => {
  const { x, y, width, height } = rect;
  const waterHeight = height * waterLevel; //水波的高度=柱子高度*百分比
  const waterY = y + height - waterHeight; //水波的y轴位置
  
  const waveLength = width;
  const waveHeight = 3;
  const frequency = 1;
  
  // 从底部开始绘制路径
  let path = `M ${x} ${y + height}`;
  
  // 绘制左侧边线到水波位置
  path += ` L ${x} ${waterY}`;
  
  // 绘制水波顶部
  for (let i = 0; i <= width; i += 2) {
    const waveX = x + i;
    // 使用正弦函数计算Y坐标
    const waveY = waterY + Math.sin((i / waveLength) * Math.PI * frequency + time) * waveHeight;
    path += ` L ${waveX} ${waveY}`;
  }
  
  // 绘制右侧边线回到底部
  path += ` L ${x + width} ${y + height}`;
  
  // 闭合路径
  path += ` Z`;
  
  return path;
};
创建echart图标,并实现水波的动画效果
// 创建图表
const createChart = () => {
  const container = chartRef.value;
  chartInstance = echarts.init(container);
  
  chartInstance.setOption(createOption());
  
  // 启动水波动画
  const animate = () => {
    animationTime += 0.1;
    if (chartInstance) {
      chartInstance.setOption({series:series});
    }
    
    requestAnimationFrame(animate);
  };
  
  animate();
};
onMounted(() => {
  createChart();
});
onUnmounted(() => {
  if (chartInstance) {
    chartInstance.dispose();
    chartInstance = null;
  }
});
最终效果: 


 400 186 1886
400 186 1886









