使用echarts实现阶梯柱状图,展示高度变化趋势

客户要求实现以下需求:

以柱状图的形式展示用电趋势,同时将X轴上每个月份的用电变化直观表示出来,想到可以用阶梯柱状图实现,并标记百分比和上升下降趋势,设计图与实现效果如下:

实现原理如下:

  • 柱状图:x 轴使用 bar 系列生成正常柱状图,柱子顶部显示数值。
  • 阶梯虚线连接线(横竖折线) :新增一条line系列,开启step: 'middle'阶梯模式,线条设为绿色虚线,数据点对应每根柱子顶部数值,自动生成「横 - 竖 - 横」折线,完美匹配图中样式。
  • 上升下降百分比 + 箭头标注 :使用markPoint标记点,放在阶梯线中间位置,自定义箭头图标 + 调整高度绿色文字;通过计算两个数值的环比变化自动生成百分比。变化率计算:(今日-昨日)/昨日 *100,负数代表下降;markPoint.symbol: 'arrow' 内置箭头图形;symbolRotate: 180 将箭头旋转 180° 朝下,匹配图中绿色向下箭头;

代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>阶梯柱状图-标记点跟随右侧柱子高度</title>
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
<style>
html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: #0f1723;
}
#chart {
  width: 100%;
  height: 100%;
  min-height: 600px;
}
</style>
</head>
<body>
<div id="chart"></div>
<script>
const myChart = echarts.init(document.getElementById('chart'));

// 业务数据(5根柱子)
const categoryList = ['时段1', '时段2', '时段3', '时段4', '时段5'];
const valueList = [143.6, 192.2, 151.8, 129.5, 185.6];
const barColorList = ['#99a0b0', '#64748b', '#475569', '#3b82f6', '#2563eb'];

// 统一上浮距离,每组距离阶梯拐点留白完全一致
const gapOffset = 30;
const markPointData = [];

for (let i = 0; i < valueList.length - 1; i++) {
  const leftVal = valueList[i];
  const rightVal = valueList[i + 1]; // 后面/右侧柱子高度
  const changeRate = ((rightVal - leftVal) / leftVal * 100).toFixed(1);
  const absRate = Math.abs(changeRate);
  const isDrop = changeRate < 0;

  // 按要求:直接使用【后面右侧柱子】高度 + 统一上浮,不取双柱最大值
  const markPosY = rightVal + gapOffset;

  markPointData.push({
    coord: [i + 0.5, markPosY],
    value: `${absRate}%`,
    symbol: 'arrow',
    symbolSize: 18,
    //symbolSize: 0, // 文字自带升降箭头,隐藏自带箭头
    symbolRotate: isDrop ? 180 : 0,
    itemStyle: {
      color: isDrop ? '#22e066' : '#ff4d4f'
    },
    label: {
      show: true,
      color: isDrop ? '#22e066' : '#ff4d4f',
      fontSize: 18,
      fontWeight: 'bold',
      //formatter: isDrop ? '↓{c}' : '↑{c}', // 文字自带升降箭头,隐藏自带箭头
      formatter: '{c}',
      // 文字固定在箭头正下方,层级:箭头在上,百分比在下
      offset: [0, 28],
      backgroundColor: 'transparent'
    }
  })
}

// 自动计算y轴最大边界,防止顶部文字被截断
const allMarkY = markPointData.map(item => item.coord[1]);
const yAxisMax = Math.max(...allMarkY) + 40;

const option = {
  backgroundColor: '#0f1723',
  tooltip: {
    trigger: 'axis',
    axisPointer: { type: 'shadow' }
  },
  xAxis: {
    type: 'category',
    data: categoryList,
    axisLine: { lineStyle: { color: '#333' } },
    axisLabel: { color: '#ccc', fontSize: 14 },
    axisTick: { show: false }
  },
  yAxis: {
    type: 'value',
    name: 'kWh',
    nameTextStyle: { color: '#ccc', fontSize: 16 },
    splitLine: { lineStyle: { color: '#222' } },
    axisLine: { show: false },
    axisTick: { show: false },
    axisLabel: { color: '#ccc', fontSize: 16 },
    max: yAxisMax
  },
  series: [
    // 柱状图
    {
      name: '用电量',
      type: 'bar',
      barWidth: 60,
      data: valueList.map((val, idx) => ({
        value: val,
        itemStyle: {
          color: barColorList[idx],
          borderRadius: [6, 6, 0, 0]
        }
      })),
      label: {
        show: true,
        position: 'top',
        color: '#fff',
        fontSize: 24
      }
    },
    // 绿色虚线阶梯连线 step:middle 横竖折线
    {
      name: '趋势连线',
      type: 'line',
      data: valueList,
      step: 'middle',
      lineStyle: {
        color: '#22e066',
        type: 'dashed',
        width: 2
      },
      symbol: 'none',
      markPoint: {
        symbol: 'arrow',
        symbolSize: 22,
        data: markPointData
      }
    }
  ],
  legend: { show: false }
};

myChart.setOption(option);
window.addEventListener('resize', () => myChart.resize());
</script>
</body>
</html>