解锁 ECharts 新技能:跨轴瀑布图实战指南

127 阅读7分钟

作者简介:大家好,我是文艺理科生Owen,某车企前端开发,负责前端业务需求和可视化项目 目前在卷的技术方向:数据可视化、工程化系列,主要偏向最佳实践,以及开发中填补的坑 希望对大家有所帮助,有兴趣可以在评论区交流互动,感谢支持~~~

最近接到一个瀑布图的改造需求,产品原定只实现正半轴数据,直接在echarts官方示例中找到原始代码, echarts.apache.org/examples/zh…

image.png

样式改造和数据集成后,搞定。

具体解释可参考这个帖子:blog.csdn.net/Snow_GX/art…

主要思路是使用3个series对象,分别对应 占空柱、增加值、减少值。

如下图对应3个series对象数据分别为:

// 占空柱数据
placeholder = [0, 1000, 2000]
// 正增加值数据
positive =  [1000, 2000, 0]
// 负增加值数据
negative = [0, 0, 1000]

结果产品说业务又要加负值逻辑,必须加。我:好的(为啥每次不早说呢)

然后开始分析,按照初始值、增量值、结果值分别为正负,一共有8种情况,如下图。然后按照常识(正+正=正,负+负=负)删掉两种异常组合,还剩6种组合。

接下来就依次尝试了。默认从接口中获取到的数据:

  • 当前值数组:endList
  • 当前值减去上一个值的变化值数组:addList(增加为正值,减少为负值)

然后尝试每一种情况,用 endList 和 addList 表示 占空柱、增加值、减少值 对应数组 。

下面是 echarts官方示例中可运行的代码,可以直接复制测试。

const addData = [1000, -2000]
const colors = ['#5470c6', '#91cc75']

option = {
  title: {
    text: 'Waterfall Chart'
  },
  tooltip: {
    trigger: 'axis',
    axisPointer: {
      type: 'shadow'
    },
    formatter: function (params) {
      var tar = params[1];
      return tar.name + '<br/>' + tar.seriesName + ' : ' + tar.value;
    }
  },
  grid: {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true
  },
  xAxis: {
    type: 'category',
    splitLine: { show: false },
    data: ['Total', 'Rent', 'Utilities', 'Transportation', 'Meals', 'Other']
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      name: 'Placeholder',
      type: 'bar',
      stack: 'Total',
      itemStyle: {
        borderColor: 'transparent',
        color: 'transparent'
      },
      emphasis: {
        itemStyle: {
          borderColor: 'transparent',
          color: 'transparent'
        }
      },
      data: [0, 0]
    },
    {
      name: 'Life Cost',
      type: 'bar',
      stack: 'Total',
      label: {
        show: true,
        position: 'top',
        formatter: function(params) {
          return addData[params.dataIndex]
        }
      },
      itemStyle: {
        color: function(params) {
          return addData[params.dataIndex] >= 0 ? colors[0] : colors[1]
        }
      },
      data: [1000, 1000]
    },
    {
      name: 'Life Cost1',
      type: 'bar',
      stack: 'Total',
      label: {
        show: false,
        position: 'top',

      },
      itemStyle: {
        color: function(params) {
          return addData[params.dataIndex] >= 0 ? colors[0] : colors[1]
        }
      },
      data: [0, -1000]
    }
  ]
};
  1. 初值值正 -> 增量值正 -> 结果值正

规律:

  • 占空比数组:endList[i-1]
  • 增加值数组: addList[i]
  • 减少值数组: 0
  1. 初值值负 -> 增量值负 -> 结果值负

规律:

  • 占空比数组:endList[i-1]
  • 增加值数组: addList[i]
  • 减少值数组: 0
  1. 初值值正 -> 增量值负 -> 结果值正

规律:

  • 占空比数组:endList[i]
  • 增加值数组: 0
  • 减少值数组: addList[i]
  1. 初值值负 -> 增量值正 -> 结果值负

规律:

  • 占空比数组:endList[i]
  • 增加值数组: -addList[i-1]
  • 减少值数组: 0
  1. 初值值正 -> 增量值负 -> 结果值负(跨轴)

规律:

  • 占空比数组:0
  • 增加值数组: endList[i-1]
  • 减少值数组: endList[i]
  1. 初值值负 -> 增量值正 -> 结果值正(跨轴)

规律:

  • 占空比数组:0
  • 增加值数组: endList[i]
  • 减少值数组: endList[i-1]

完整效果

源码

echarts演练场可运行代码

const endList = [1000,  2000, 1000, -1000, -2000, -1000, 1000]
const addList = [1000, 1000, -1000, -2000, -1000, 1000, 2000]
// 计算起始值数组
let startList = []
for(let i = 0; i < endList.length; i++) {
  startList[i] = endList[i] - addList[i]
}
console.log(startList, 'startList')
let placeholder = [], positive = [], negative = []

const handleData = () => {
  endList.forEach((_, i) => {
    // 正+正=正,负-负=负
    // ● 占空比数组:endList[i-1]
    // ● 增加值数组: addList[i]
    // ● 减少值数组: 0
    if((startList[i] >= 0 && addList[i] > 0 && endList[i] > 0) || (startList[i] <=0 && addList[i] < 0 && endList[i] < 0)) {
      placeholder[i] = endList[i-1] || startList[i]
      positive[i] = addList[i]
      negative[i] = 0
    }
    // 正+负=正
    // ● 占空比数组:endList[i]
    // ● 增加值数组: -addList[i]
    // ● 减少值数组: 0
    else if((startList[i] >=0 && addList[i] < 0 && endList[i] > 0)) {
      placeholder[i] = endList[i]
      positive[i] = -addList[i]
      negative[i] = 0
    }
    // 负+正=负
    // ● 占空比数组:endList[i]
    // ● 增加值数组: -addList[i-1]
    // ● 减少值数组: 0
    else if((startList[i] <= 0 && addList[i] > 0 && endList[i] < 0)) {
      placeholder[i] = endList[i]
      positive[i] = addList[i-1]
      negative[i] = 0
    }
    // 正+负=负
    // ● 占空比数组:0
    // ● 增加值数组: endList[i-1]
    // ● 减少值数组: endList[i]
    else if((startList[i] >=0 && addList[i] < 0 && endList[i] < 0)) {
      placeholder[i] = 0
      positive[i] = endList[i-1]
      negative[i] = endList[i]
    }
    // 负+正=正
    // ● 占空比数组:0
    // ● 增加值数组: endList[i]
    // ● 减少值数组: endList[i-1]
    else if((startList[i] <=0 && addList[i] > 0 && endList[i] > 0)) {
      placeholder[i] = 0
      positive[i] = endList[i]
      negative[i] = endList[i-1]
    } else {
      console.log(111)
    }
  })
}
handleData()

console.log(placeholder, 'placeholder')
console.log(positive, 'positive')
console.log(negative, 'negative')

const colors = ['green', 'red']

option = {
  title: {
    text: 'Waterfall Chart'
  },
  tooltip: {
    trigger: 'axis',
    axisPointer: {
      type: 'shadow'
    },
    formatter: function (params) {
      var tar = params[1];
      return tar.name + '<br/>' + tar.seriesName + ' : ' + tar.value;
    }
  },
  grid: {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true
  },
  xAxis: {
    type: 'category',
    splitLine: { show: false },
    data: ['Total', 'Rent', 'Utilities', 'Transportation', 'Meals', 'Other']
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      name: 'Placeholder',
      type: 'bar',
      stack: 'Total',
      itemStyle: {
        borderColor: 'transparent',
        color: 'transparent'
      },
      emphasis: {
        itemStyle: {
          borderColor: 'transparent',
          color: 'transparent'
        }
      },
      data: placeholder
    },
    {
      name: 'Life Cost',
      type: 'bar',
      stack: 'Total',
      label: {
        show: true,
        position: 'insideTop',
        formatter: function(params) {
          return addList[params.dataIndex]
        }
      },
      itemStyle: {
        color: function(params) {
          return addList[params.dataIndex] >= 0 ? colors[0] : colors[1]
        }
      },
      data: positive
    },
    {
      name: 'Life Cost1',
      type: 'bar',
      stack: 'Total',
      label: {
        show: false,
        position: 'top',

      },
      itemStyle: {
        color: function(params) {
          return addList[params.dataIndex] >= 0 ? colors[0] : colors[1]
        }
      },
      data: negative
    }
  ]
};

可运行html源码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ECharts 柱形图示例</title>
    <script src="https://echarts.apache.org/zh/js/vendors/echarts/dist/echarts.min.js"></script>
  </head>
  <body>
    <div id="main" style="width: 600px; height: 400px"></div>
    <script>
      // 初始化 echarts 实例
      var myChart = echarts.init(document.getElementById("main"));

      // 指定图表的配置项和数据
      const endList = [1000, 2000, 1000, -1000, -2000, -1000, 1000];
      const addList = [1000, 1000, -1000, -2000, -1000, 1000, 2000];
      // 计算起始值数组
      let startList = [];
      for (let i = 0; i < endList.length; i++) {
        startList[i] = endList[i] - addList[i];
      }
      console.log(startList, "startList");
      let placeholder = [],
        positive = [],
        negative = [];

      const handleData = () => {
        endList.forEach((_, i) => {
          // 正+正=正,负-负=负
          // ● 占空比数组:endList[i-1]
          // ● 增加值数组: addList[i]
          // ● 减少值数组: 0
          if (
            (startList[i] >= 0 && addList[i] > 0 && endList[i] > 0) ||
            (startList[i] <= 0 && addList[i] < 0 && endList[i] < 0)
          ) {
            placeholder[i] = endList[i - 1] || startList[i];
            positive[i] = addList[i];
            negative[i] = 0;
          }
            // 正+负=正
            // ● 占空比数组:endList[i]
            // ● 增加值数组: -addList[i]
            // ● 减少值数组: 0
          else if (startList[i] >= 0 && addList[i] < 0 && endList[i] > 0) {
            placeholder[i] = endList[i];
            positive[i] = -addList[i];
            negative[i] = 0;
          }
            // 负+正=负
            // ● 占空比数组:endList[i]
            // ● 增加值数组: -addList[i-1]
            // ● 减少值数组: 0
          else if (startList[i] <= 0 && addList[i] > 0 && endList[i] < 0) {
            placeholder[i] = endList[i];
            positive[i] = addList[i - 1];
            negative[i] = 0;
          }
            // 正+负=负
            // ● 占空比数组:0
            // ● 增加值数组: endList[i-1]
            // ● 减少值数组: endList[i]
          else if (startList[i] >= 0 && addList[i] < 0 && endList[i] < 0) {
            placeholder[i] = 0;
            positive[i] = endList[i - 1];
            negative[i] = endList[i];
          }
            // 负+正=正
            // ● 占空比数组:0
            // ● 增加值数组: endList[i]
            // ● 减少值数组: endList[i-1]
          else if (startList[i] <= 0 && addList[i] > 0 && endList[i] > 0) {
            placeholder[i] = 0;
            positive[i] = endList[i];
            negative[i] = endList[i - 1];
          } else {
            console.log(111);
          }
        });
      };
      handleData();

      console.log(placeholder, "placeholder");
      console.log(positive, "positive");
      console.log(negative, "negative");

      const colors = ["green", "red"];

      option = {
        title: {
          text: "Waterfall Chart",
        },
        tooltip: {
          trigger: "axis",
            axisPointer: {
            type: "shadow",
          },
          formatter: function (params) {
            var tar = params[1];
            return tar.name + "<br/>" + tar.seriesName + " : " + tar.value;
          },
        },
        grid: {
          left: "3%",
          right: "4%",
          bottom: "3%",
          containLabel: true,
        },
        xAxis: {
          type: "category",
          splitLine: { show: false },
          data: [
            "Total",
            "Rent",
            "Utilities",
            "Transportation",
            "Meals",
            "Other",
          ],
        },
        yAxis: {
          type: "value",
        },
        series: [
          {
            name: "Placeholder",
            type: "bar",
            stack: "Total",
            itemStyle: {
              borderColor: "transparent",
              color: "transparent",
            },
            emphasis: {
              itemStyle: {
                borderColor: "transparent",
                color: "transparent",
              },
            },
            data: placeholder,
          },
          {
            name: "Life Cost",
            type: "bar",
            stack: "Total",
            label: {
              show: true,
              position: "insideTop",
              formatter: function (params) {
                return addList[params.dataIndex];
              },
            },
            itemStyle: {
              color: function (params) {
                return addList[params.dataIndex] >= 0 ? colors[0] : colors[1];
              },
            },
            data: positive,
          },
          {
            name: "Life Cost1",
            type: "bar",
            stack: "Total",
            label: {
              show: false,
              position: "top",
            },
            itemStyle: {
              color: function (params) {
                return addList[params.dataIndex] >= 0 ? colors[0] : colors[1];
              },
            },
            data: negative,
          },
        ],
      };

      // 使用指定的配置项和数据显示图表
      myChart.setOption(option);
    </script>
  </body>
</html>

关键修改点

1、柱子颜色

在增加值、减少值对应的series对象中,通过itemStyle的color函数,根据变化值动态展示颜色

itemStyle: {
  color: function(params) {
    return addList[params.dataIndex] >= 0 ? colors[0] : colors[1]
  }
},

2、数据标签

在增加值的series对象中,通过label的formatter函数,关联params索引,获取对象变化值。索引取值 在echarts中常用的技巧。

label: {
  show: true,
  position: 'insideTop',
  formatter: function(params) {
    return addList[params.dataIndex]
  }
},

3、跨轴处理

echarts中默认的瀑布图示例仅支持y轴的正半轴,对负半轴处理无法兼容,需要分情况逐一处理。比如上一个值是正值,当前值为负值,应该先减去正值(对应增加值的series),然后再减去负值(对应减少值的series)。

总结:本文通过实际项目中的echarts瀑布图为例,逐一分析解决了官方示例中未覆盖的y负半轴问题,提到了echarts中常用的索引取值的使用技巧,并给出了demo源码,供大家享用。

日拱一卒,功不唐捐。