[Vue]Echarts堆叠柱状图的label显示数字问题

image

如上图,堆叠的时候,柱子上方显示堆叠的总数。那不堆叠的时候,如下图,就显示自己的数据

image

那么怎么实现这个功能呢(完整组件代码在文末)

首先要解决的问题1:

你怎么知道用户通过图例(legend)点击事件来切换系列的可见性?

initChart() {
      this.chartInstance = echarts.init(this.$refs.chart);
      this.updateChart();

      // 添加图例点击事件监听
      this.chartInstance.on("legendselectchanged", params => {
        console.log("图例点击变化:", params);
        console.log("当前选中图例:", params.selected);
        // 更新堆叠图的标签统计值
        this.updateStackedLabels(params.selected);
      });
    },

ok,那么就是要计算堆叠的时候,总额是多少了

// 计算堆叠组总和(只计算可见系列)
    calculateStackTotal(series, stackName, dataIndex, selectedState) {
      return series
        .filter(s =>
          s.stack === stackName &&
          s.type === "bar" &&
          selectedState[s.name] !== false
        )
        .reduce((sum, s) => sum + (s.data[dataIndex] || 0), 0);
    },

计算好了显示上去呗

// 更新堆叠图标签统计值(只在最顶部柱子显示)
    updateStackedLabels(selectedState) {
      if (!this.chartInstance) return;
      const option = this.chartInstance.getOption();
      const series = option.series;
      // 记录每个堆叠组的最顶部可见系列
      const topVisibleSeries = {};
      // 第一次遍历:找出每个堆叠组的最顶部可见系列
      series.forEach(serie => {
        if (serie.stack && selectedState[serie.name] !== false) {
          if (!topVisibleSeries[serie.stack] ||
              series.indexOf(serie) > series.indexOf(topVisibleSeries[serie.stack])) {
            topVisibleSeries[serie.stack] = serie;
          }
        }
      });
      // 第二次遍历:设置标签显示
      series.forEach(serie => {
        if (serie.stack) {
          const isVisible = selectedState[serie.name] !== false;
          const isTopVisible = topVisibleSeries[serie.stack] === serie;

          serie.label = {
            show: this.showLabel && isVisible && isTopVisible,
            position: "top",
            color: "#000", // 黑色字体
            formatter: params => {
              if (!isVisible || !isTopVisible) return "";
              const stackTotal = this.calculateStackTotal(
                series,
                serie.stack,
                params.dataIndex,
                selectedState
              );
              return this.$options.filters.ThousandSeparate(stackTotal, 2);
            }
          };
        }
      });

      this.chartInstance.setOption({ series });
    },

ok,我们的目标实现了:堆叠的时候显示统计值,不堆叠的时候显示自己的值

那么问题是,刷新页面,堆叠,显示的是各自的值,要点一下图例触发变动,才生效

那么就不太好,所以需要想办法触发一下。

已知

computed: {
   // 生成图例数据
    computedLegendData() {
      return [
        ...this.seriesData.map(item => item.name),
        ...this.lineData.map(item => item.name)
      ].filter(Boolean);
    }
  }
legend: {
          show: this.showLegend,
          data: this.computedLegendData,
          type: "scroll",
          bottom: 0
        },

于是,等他渲染完,要触发一下label的更新

updateChart() {
      if (!this.chartInstance) return;
      const option = {
        ...this.getBaseOption(),
        series: this.getSeriesConfig()
      };
      this.chartInstance.setOption(option, true);
      // 触发更新
      const initialSelectedState = this.computedLegendData.reduce((acc, name) => {
        acc[name] = true;
        return acc;
      }, {});
      this.updateStackedLabels(initialSelectedState);
    },

如此就大功告成了,最后附上这个组件的完整代码

这个组件我肯定还会继续优化,但是后面优化后的代码不会更新到本文了,有兴趣可以关注之后我发的博文。

点击查看代码
<!--
/**
 * @file EchartsBarLine.vue
 * @description 通用的 ECharts 柱状图与折线图组合组件。
 *
 * ### 功能说明:
 * 该组件封装了基于 ECharts 的柱状图和折线图,支持多柱子、堆叠柱子、动态效果、背景网格线显示控制等功能。
 *
 * ### 封装原因:
 * 为了提高代码复用性,简化复杂图表的开发流程,统一管理图表配置,减少重复代码。
 *
 * ### 优点:
 * - 高度可定制:支持自定义横纵坐标、堆叠柱子、折线图等。
 * - 灵活性强:通过 props 控制图表的各种属性(如动画、背景线、图注等)。
 * - 易于维护:集中管理图表逻辑,便于后续扩展和修改。
 *
 * ### 使用场景:
 * - 数据可视化展示:适用于需要同时展示柱状图和折线图的场景。
 * - 多维度数据分析:适合对比多个数据系列的趋势或分布。
 * - 动态数据更新:实时更新图表数据,展示动态变化趋势。
 */
-->
<template>
  <div class="echarts-wrapper">
    <a-spin v-if="isLoding" />
    <div
      ref="chart"
      class="echarts-chart"
      :style="{ width: width, height: height, display: isLoding ? 'none' : 'block' }"
    />
  </div>
</template>

<script>
import * as echarts from "echarts";
import "../css/echarts-theme.less";

export default {
  name: "EchartsBarLine",
  props: {
    // 图表宽度
    width: {
      type: String,
      default: "100%"
    },
    // 图表高度
    height: {
      type: String,
      default: "400px"
    },
    // 横坐标数据
    xAxisData: {
      type: Array,
      required: true
    },
    // 纵坐标数据(多柱子、堆叠柱子)
    seriesData: {
      type: Array,
      required: true
    },
    // 折线图数据
    lineData: {
      type: Array,
      default: () => []
    },
    // 是否显示背景横线
    showGridLines: {
      type: Boolean,
      default: true
    },
    // 图注
    legendData: {
      type: Array,
      default: () => []
    },
    // 动画效果开关
    animation: {
      type: Boolean,
      default: true
    },
    // 自定义颜色数组
    colors: {
      type: Array,
      default: null // 如果未传入,则使用默认主题色
    },
    // 是否启用平滑曲线
    smooth: {
      type: Boolean,
      default: true
    },
    // 柱状图宽度
    barWidth: {
      type: [String, Number],
      default: "30%"
    },
    // 是否显示图例
    showLegend: {
      type: Boolean,
      default: true
    },
    // 是否显示数据标签
    showLabel: {
      type: Boolean,
      default: false
    },
    // 图表标题
    title: {
      type: String,
      default: ""
    },
    // 图表副标题
    subTitle: {
      type: String,
      default: ""
    },
    isLoding: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      chartInstance: null,
      resizeObserver: null
    };
  },
  computed: {
    finalColors() {
      // 如果用户传入了自定义颜色数组,则使用传入的颜色
      if (this.colors && this.colors.length > 0) {
        return this.colors;
      }

      // 否则,从 CSS 变量中加载默认主题色
      const rootStyles = window.getComputedStyle(document.documentElement);
      const seriesColors = [];

      for (let i = 1; i <= 16; i++) {
        const color = rootStyles.getPropertyValue(`--series-color-${i}`).trim();
        if (color) {
          seriesColors.push(color);
        }
      }

      return seriesColors;
    },
    // 生成图例数据
    computedLegendData() {
      return [
        ...this.seriesData.map(item => item.name),
        ...this.lineData.map(item => item.name)
      ].filter(Boolean);
    }
  },
  watch: {
    xAxisData: {
      handler() {
        this.updateChart();
      },
      deep: true
    },
    seriesData: {
      handler() {
        this.updateChart();
      },
      deep: true
    },
    lineData: {
      handler() {
        this.updateChart();
      },
      deep: true
    },
    showGridLines() {
      this.updateChart();
    },
    legendData() {
      this.updateChart();
    },
    colors() {
      this.updateChart();
    },
    smooth() {
      this.updateChart();
    },
    barWidth() {
      this.updateChart();
    },
    showLegend() {
      this.updateChart();
    },
    showLabel() {
      this.updateChart();
    },
    title() {
      this.updateChart();
    },
    subTitle() {
      this.updateChart();
    }
  },
  mounted() {
    this.initChart();
    this.initResizeObserver();
  },

  beforeDestroy() {
    this.disposeChart();
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
    }
  },

  methods: {
    initChart() {
      this.chartInstance = echarts.init(this.$refs.chart);
      this.updateChart();
      // 添加图例点击事件监听
      this.chartInstance.on("legendselectchanged", params => {
        // 更新堆叠图的标签统计值
        this.updateStackedLabels(params.selected);
      });
    },
    // 计算堆叠组总和(只计算可见系列)
    calculateStackTotal(series, stackName, dataIndex, selectedState) {
      return series
        .filter(s =>
          s.stack === stackName &&
          s.type === "bar" &&
          selectedState[s.name] !== false
        )
        .reduce((sum, s) => sum + (s.data[dataIndex] || 0), 0);
    },
    // 更新堆叠图标签统计值(只在最顶部柱子显示)
    updateStackedLabels(selectedState) {
      if (!this.chartInstance) return;
      const option = this.chartInstance.getOption();

      const series = option.series;
      // 记录每个堆叠组的最顶部可见系列
      const topVisibleSeries = {};
      // 第一次遍历:找出每个堆叠组的最顶部可见系列
      series.forEach(serie => {
        if (serie.stack && selectedState[serie.name] !== false) {
          if (!topVisibleSeries[serie.stack] ||
              series.indexOf(serie) > series.indexOf(topVisibleSeries[serie.stack])) {
            topVisibleSeries[serie.stack] = serie;
          }
        }
      });
      // 第二次遍历:设置标签显示
      series.forEach(serie => {
        if (serie.stack) {
          const isVisible = selectedState[serie.name] !== false;
          const isTopVisible = topVisibleSeries[serie.stack] === serie;

          serie.label = {
            show: this.showLabel && isVisible && isTopVisible,
            position: "top",
            color: "#000", // 黑色字体
            formatter: params => {
              if (!isVisible || !isTopVisible) return "";
              const stackTotal = this.calculateStackTotal(
                series,
                serie.stack,
                params.dataIndex,
                selectedState
              );
              return this.$options.filters.ThousandSeparate(stackTotal, 2);
            }
          };
        }
      });

      this.chartInstance.setOption({ series });
    },
    initResizeObserver() {
      if (typeof ResizeObserver !== "undefined") {
        this.resizeObserver = new ResizeObserver(() => {
          this.handleResize();
        });
        this.resizeObserver.observe(this.$el);
      } else {
        window.addEventListener("resize", this.handleResize);
      }
    },
    handleResize() {
      if (this.chartInstance) {
        this.chartInstance.resize();
      }
    },
    getBaseOption() {
      return {
        backgroundColor: "transparent",
        title: {
          text: this.title,
          subtext: this.subTitle,
          left: "left",
          textStyle: {
            fontSize: 18,
            fontWeight: "bold"
          },
          subtextStyle: {
            fontSize: 14,
            color: "#999"
          }
        },
        tooltip: {
          trigger: "axis",
          axisPointer: {
            type: "shadow"
          },
          confine: true,
          appendToBody: true,
          // 自定义 tooltip 格式化函数
          formatter: params => {
            let tooltipContent = "";

            // 遍历每个数据项
            params.forEach((item, index) => {
              const value = item.value; // 获取当前数据值
              const formattedValue = this.$options.filters.ThousandSeparate(value, 2); // 格式化数字
              tooltipContent += `
            <div>
              <span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${item.color};"></span>
              ${item.seriesName}: ${formattedValue}
            </div>
          `;
            });

            // 添加 x 轴的名称
            tooltipContent = `<strong>${params[0].name}</strong><br>` + tooltipContent;

            return tooltipContent;
          }
        },
        legend: {
          show: this.showLegend,
          data: this.computedLegendData,
          type: "scroll",
          bottom: 0
        },
        grid: {
          left: "3%",
          right: "3%",
          bottom: this.showLegend ? "15%" : "3%",
          top: "15%",
          containLabel: true
        },
        xAxis: {
          type: "category",
          data: this.xAxisData,
          axisTick: {
            alignWithLabel: true
          },
          axisLabel: {
            interval: 0,
            rotate: 30
          }
        },
        yAxis: {
          type: "value",
          splitLine: {
            show: this.showGridLines,
            lineStyle: {
              type: "dashed"
            }
          }
        }
      };
    },
    getSeriesConfig() {
      const series = [];

      // 柱状图配置
      this.seriesData.forEach((item, index) => {
        series.push({
          name: item.name,
          type: "bar",
          stack: item.stack || null,
          data: item.data,
          barWidth: this.barWidth,
          animation: this.animation,
          itemStyle: {
            color: this.finalColors[index % this.finalColors.length],
            borderRadius: [2, 2, 0, 0]
          },
          label: {
            show: this.showLabel,
            position: "top"
          },
          emphasis: {
            itemStyle: {
              shadowBlur: 10,
              shadowColor: "rgba(0, 0, 0, 0.5)"
            }
          }
        });
      });
      // 折线图配置
      this.lineData.forEach((item, index) => {
        series.push({
          name: item.name,
          type: "line",
          data: item.data,
          smooth: this.smooth,
          animation: this.animation,
          symbol: "circle",
          symbolSize: 8,
          itemStyle: {
            color:
              this.finalColors[
                (index + this.seriesData.length) % this.finalColors.length
              ]
          },
          lineStyle: {
            width: 3
          },
          label: {
            show: this.showLabel,
            position: "top"
          }
        });
      });

      return series;
    },
    updateChart() {
      if (!this.chartInstance) return;
      const option = {
        ...this.getBaseOption(),
        series: this.getSeriesConfig()
      };
      this.chartInstance.setOption(option, true);
      // 触发更新
      const initialSelectedState = this.computedLegendData.reduce((acc, name) => {
        acc[name] = true;
        return acc;
      }, {});
      this.updateStackedLabels(initialSelectedState);
    },
    disposeChart() {
      if (this.chartInstance) {
        this.chartInstance.dispose();
        this.chartInstance = null;
      }
    }
  }
};
</script>

<style scoped>
.echarts-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
  min-height: 300px;
}

.echarts-chart {
  width: 100%;
  height: 100%;
}

/* 响应式调整 */
@media (max-width: 768px) {
  .echarts-wrapper {
    padding: 8px;
  }

  /* 调整X轴标签 */
  :deep(.echarts-axis-x .echarts-axis-label) {
    font-size: 10px;
    transform: rotate(-45deg);
    margin-top: 5px;
  }

  /* 调整图例位置 */
  :deep(.echarts-legend) {
    bottom: 5px !important;
  }
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萌狼蓝天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值