普通视图

发现新文章,点击刷新页面。
昨天 — 2025年7月2日首页

【拒绝平庸】Vue+ECharts图表美化--柱状图展示优化教程

作者 Easy_Y
2025年7月2日 20:01

优化后的饼图效果

6.gif

Scss样式部分

html,body{
  width: 100%;
  height: 100%;
  padding:0px; 
  box-sizing: border-box;
  overflow: hidden;
}
 
body{
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
}
 
.layout-demo-box{
  display: flex;
  flex-direction: column;
  width: 540px;
  height: 300px;
  background: linear-gradient(
    to bottom,
  #000e2a 0%,
  #000000 10%,
  #001134 100%
  );
  border: 1px solid #00436e;
  border-radius: 5px;
  *{
    box-sizing: border-box;
  }
  .title-box{
    display: flex;
    align-items: center;
    width: 100%; 
    height: 50px;
    flex-shrink: 0;
    padding: 20px 30px 0px 20px; 
    span{
      flex-shrink: 0;

      &:nth-child(1){
        width: 0px;
        flex-grow: 1;
      }
    }
    .btn-box{
      display: block;
      color:#6bf6fc;
      cursor: pointer;
    }
    h1{
      font-size: 14px; 
      line-height: 16px; 
      margin: 0px;
      background: linear-gradient(to top, #00d1fe, #fff);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;  
    }
    p{
      font-size: 12px;
      margin:2px 0px;
      color:#416387;
    }
  }
  .chart-box{
    width: 100%;
    height: 0px;
    flex:1;
  } 
}  

HTML页面部分

<div id="app">
  <!-- demo内容 start -->  
  <div class="layout-demo-box">
    <div class="title-box">
      <span>
        <h1>柱状图面板</h1>
        <p>统计日期(2025-07-02 12:00:00)</p> 
      </span>  
    </div> 
    <div class="chart-box" id="chartId"></div>
  </div>
  <!-- demo内容 end --> 
</div>

JS页面部分

  methods: {
    /**
     * 初始化并渲染 ECharts 图表
     * 功能说明:
     * 1. 创建 ECharts 实例并渲染图表
     * 2. 自动响应窗口大小变化
     * 3. 组件销毁时自动清理资源防止内存泄漏 
     */
    initEcharts() {
      // 1. 获取 DOM 元素 - 添加空检查
      const chartDom = document.getElementById('chartId'); 
      if (!chartDom) {
        console.warn(' 图表容器不存在');
        return;
      }
  
      // 2. 初始化图表实例
      this.myChart  = echarts.init(chartDom); 
      
      // 3. 设置图表配置 
      const option = {
        // option 配置 start ---------------------------------------
        
        // option 配置 end ---------------------------------------
      };
      
      // 4. 应用配置
      try {
        this.myChart.setOption(option); 
      } catch (error) {
        console.error(' 图表配置错误:', error);
      }
  
      // 5. 响应式处理 - 使用防抖优化性能
      this.handleResize  = debounce(() => {
        this.myChart  && this.myChart.resize(); 
      }, 300);
      
      window.addEventListener('resize',  this.handleResize); 
    },
    
    // 清理资源 
    destroyEcharts() {
      if (this.myChart)  {
        window.removeEventListener('resize',  this.handleResize); 
        this.myChart.dispose(); 
        this.myChart  = null;
      }
    }
  },
  
  // Vue生命周期钩子-组件挂载完成后调用
  mounted() {
    this.$nextTick(() => {
      this.initEcharts(); 
    });
  }, 

  // Vue生命周期钩子-组件销毁前调用
  beforeDestroy() {
    this.destroyEcharts(); 
  }

定义data数据

  // 数据
  chartData:{
    xAxisData: ['语文','数学','英语','科学','历史'],
    seriesData : [20, 80, 100, 40, 34, 90, 60]
  }, 

柱状图的option配置

tooltip:{
  trigger: 'axis',
  axisPointer: {
    type: 'shadow',
    shadowStyle: { // 鼠标经过背景色
      color: 'rgba(0, 67, 110, 0.1)',
    } 
  },
  formatter: function(params) { 
    return params[0].marker + params[0].name + "成绩:" + params[0].data
  }
}, 
animation: true,
grid: {
  top: "40",
  bottom: "40",
  left: "50", 
  right: "20"  
},
xAxis: {
  data: chartData.xAxisData,
  axisLine: {
    show: true, //隐藏X轴轴线
    lineStyle: {
      color: '#0A376C'
    }
  },
  axisTick: {
    show: false //隐藏X轴刻度
  },
  axisLabel: {
    show: true,
    margin: 14,
    fontSize: 12,
    textStyle: {
      color: "#A3C0DF" //X轴文字颜色
    }
  }  
},
yAxis: [
  {
    type: "value",
    gridIndex: 0,  
    splitLine: {
      show: true,
      lineStyle: {
        type: 'dashed', // 关键属性:虚线
        color: '#011731',
        width: 1
      },
    },
    axisTick: {
      show: false
    },
    axisLine: {
      show: false, 
    },
    axisLabel: {
      show: true,
      margin: 14,
      fontSize: 10,
      textStyle: {
        color: "#A3C0DF" //X轴文字颜色
      }
    }
  }
], 
series: [
  {
    name: "单科成绩",
    type: "bar",
    barWidth: 16,
    itemStyle: {
      normal: {
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
            offset: 0,
            color: "#07ecd9"
          },
          {
            offset: 1,
            color: "#034881"
          }
        ]), 
      }
    }, 
    data: chartData.seriesData,
    z: 10,
    zlevel: 2,
    label: {
      show: true,
      position: "top",
      distance: 5,
      fontSize:12,
      color: "#01fff4"
    }
  },   
  {
    // 分隔
    type: "pictorialBar",
    itemStyle: {
      normal:{
        color:"#0F375F"
      }
    },
    symbolRepeat: "fixed",
    symbolMargin: 2,
    symbol: "rect",
    symbolClip: true,
    symbolSize: [16, 2],
    symbolPosition: "start",
    symbolOffset: [0, -1], 
    data: chartData.seriesData, 
    z: 0,
    zlevel: 3,
  }
]      

在线Demo

下方可在线查看Demo完整代码

总结

通过以上步骤,我们成功地使用 Echarts 制作并优化了一个柱状图。在实际应用中,大家可以根据具体的数据和业务需求,进一步调整图表的样式和交互效果,让数据可视化更加美观和实用。希望这篇教程能对大家在前端数据可视化开发中有所帮助

昨天以前首页

🔋 Vue + ECharts 实现分段折线图教学实战:电池趋势图案例

作者 Kier
2025年6月24日 18:43

📘 一、背景介绍

在数据可视化中,有时候我们需要根据数据状态、时间断点等将一条数据曲线分段展示,如:

  • 电池电量在中断恢复后重新记录
  • IoT 设备掉线后重连产生的空白段
  • 按时间周期或状态对折线图拆段绘制

本篇文章以 Vue + ECharts 实现的一个电量趋势图为例,带你完整理解 “折线图分段绘制” 的实现方法。

📊 二、效果说明

微信截图_20250624170323.png

组件能够实现以下可视化特性:

功能点 描述
📈 折线分段展示 根据传入断点索引,自动将整条线拆成多段
🕒 时间坐标轴 X 轴为一天 24 小时的时间轴,按时间绘图
🔋 电量展示 Y 轴为电池电量(单位 %)
🔍 提示框 悬浮提示显示时间、电量
🎯 异常标记点 battery_state == 0 的点自动添加图标
🔍 缩放条 支持拖动缩放查看历史数据

⚙️ 三、核心逻辑分析

1️⃣ 图表初始化

使用 $echarts 初始化图表,并在窗口 resize 时自动适配。

lineFunction () {
  const myLine = this.$echarts.init(this.$refs.daychart)
  const option = this.getOption()
  myLine.setOption(option, true)
  window.addEventListener('resize', () => myLine.resize())
}

2️⃣ 图表配置

其中 X 轴采用 time 类型,限制显示当日时间段为查询时间的当天,0点-24点。y轴配置为value+%的形式,毕竟是电池电量,需要将电量值转换成百分比的形式来显示。考虑到电池上报频率以及后端数据的短时间连续,同时在x轴设置一个可伸缩。

{
  tooltip: { ... },
  xAxis: { type: 'time', min: '2025-06-23 00:00:00', max: '2025-06-23 24:00:00' },
  yAxis: { type: 'value', min: 5, max: 100, formatter: '{value} %' },
  series: this.my_series,
  dataZoom: [内置缩放条]
}

3️⃣ 自动分段构造

为什么要分段,因为电池可能停止上报或者故障,所以可能导致电池状态不连续。

piecesFun () {
  this.piecesList = this.buildSegments()
  this.my_series = []

  this.piecesList.forEach(segment => {
    const slice = this.history_info.slice(segment.gte, segment.lte + 1)
    const seriesData = slice.map(item => [item.in_time, item.battery_power])

    this.my_series.push({
      name: '',
      type: 'line',
      data: seriesData,
      smooth: true,
      itemStyle: {
        color: '#3A84FF',
        lineStyle: { color: '#1E84FF' }
      },
      showSymbol: true,
      symbolSize: (_, params) => (params.dataIndex === 0 ? 6 : 0),
      symbol: 'circle',
      markPoint: {
        symbol: 'image://' + require('../../../assets/images/bty_unactive.png'),
        symbolSize: 6,
        data: this.createdPointList(this.history_info, this.x_data)
      }
    })
  })
}

4️⃣ 分段算法封装 这段 buildSegments() 函数的作用是根据给定的断点列表(day_break_list)对完整数据(history_info)进行分段,生成一组表示每段范围的对象。这里根据 day_break_list 提供的索引断点,把数组 history_info 划分成若干段,每段以 gte(大于等于)和 lte(小于等于)字段表示索引范围,用于后续图表按段绘制。如果没有传入断点数组,表示整个数据不分段,直接返回一个完整区间。

🔄 循环行为解读:

  • 循环 i0breaks.length,总共 n+1 次(n 是断点个数),每次处理一段。

  • end

    • 如果是前 n 次,则取当前断点索引。
    • 最后一次(i == breaks.length),end 为数组尾部(len - 1),确保收尾部分被包含。
  • 每次将 [start, end] 作为一段(闭区间)加到 segments

  • 然后更新 start = end + 1 为下一段的起始。

📌 举个例子:

this.history_info.length = 10
this.day_break_list = [2, 5, 7]

则划分逻辑为:

段号 gte lte 描述
1 0 2 第0~2项
2 3 5 第3~5项
3 6 7 第6~7项
4 8 9 剩余尾部第8~9项

最终返回的数据结构:

[
  { gte: 0, lte: 2 },
  { gte: 3, lte: 5 },
  { gte: 6, lte: 7 },
  { gte: 8, lte: 9 }
]

5️⃣ 异常点标记 将电池状态为 0 的数据点标记出来,支持图标自定义。

createdPointList (history_info, xdata) {
  return history_info
    .map((itm, idx) => itm.battery_state == 0 ? {
      coord: [xdata[idx], itm.battery_power]
    } : null)
    .filter(Boolean)
}

🧪 五、使用案例场景

行业/场景 应用示例
智能制造 SMT 电量追踪、断电恢复显示
IoT 设备监控 在线/离线分段显示、设备心跳波动
车辆电池监控系统 显示充放电过程,支持按时间切片
医疗设备监控 重要参数监控断段可视化警示

📚 六、总结

微信截图_20250624183550.png

本文详细介绍了一个基于 Vue + ECharts 的可视化折线图组件,其核心在于:

  • 利用 series 构建多个段落折线
  • 自动化拆段逻辑封装为 buildSegments()
  • 灵活支持时间、数据点提示、缩放等功能
  • 可扩展性良好,适用于多种趋势监控场景

源码

<template>
  <div id="dayline" ref="daychart" style="width: 100%; height: 200px"></div>
</template>
<script>
export default {
  props: {
    x_data: {
      type: Array,
      required: true
    },
    y_data: {
      type: Array,
      required: true
    },
    day_break_list: {
      type: Array,
      required: true
    },
    history_info: {
      type: Array,
      required: true
    },
    show_date: {
      type: String,
      required: true
    }
  },
  data () {
    return {
      chart: null, // 图表实例对象
      piecesList: [],
      my_series: []
    }
  },
  watch: {
    y_data: {
      handler (value) {
        this.piecesFun()
        this.lineFunction()
      },
      deep: true
    },
    x_data: {
      handler (value) {
        this.piecesFun()
        this.lineFunction()
      },
      deep: true
    }
  },

  mounted () {
    this.piecesFun()
    this.lineFunction()
    // this.line()
  },
  methods: {
    lineFunction () {
      var myLine = this.$echarts.init(this.$refs.daychart)
      var option = this.getOption()
      myLine.setOption(option, true)
      window.addEventListener('resize', function () {
        myLine.resize()
      })
      // this.chart = myLine;
    },
    getOption () {
      return {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'cross'
          },
          formatter: params => {
            let _this = this
            var res =
              `<div style="text-align:left"><span>` +
              _this.$t('battery.time') +
              `:</span>` +
              params[0].data[0] +
              `</div>` +
              `<div style="text-align:left"><span>` +
              _this.$t('battery.power') +
              `:</span>` +
              params[0].data[1] +
              `%` +
              `</div>`
            return res
          }
        },
        legend: {
          orient: 'vertical',
          left: 15,
          top: 0
        },
        grid: {
          top: 20,
          left: '3%',
          right: '4%',
          bottom: 50,
          containLabel: true
        },
        xAxis: {
          type: 'time',
          splitNumber: 24,
          min: this.show_date + ' 00:00:00',
          max: this.show_date + ' 24:00:00',
          boundaryGap: false,
          axisLine: {
            show: false //不显示坐标轴线
          },
          axisTick: {
            show: false //不显示坐标轴刻度线
          },
          // data: this.x_data,
          splitLine: {
            show: true,
            lineStyle: {
              type: 'solid',
              color: '#E3ECFA'
            }
          }
        },
        yAxis: {
          type: 'value',
          min: '5',
          max: '100',
          axisLine: {
            show: false //不显示坐标轴线
          },
          axisTick: {
            show: false //不显示坐标轴刻度线
          },
          axisLabel: {
            //这种做法就是在y轴的数据的值旁边拼接单位
            formatter: '{value} %'
          },
          splitLine: {
            show: true,
            lineStyle: {
              type: 'solid',
              color: '#E3ECFA'
            }
          }
        },
        series: this.my_series,
        dataZoom: [
          {
            type: 'inside',
            height: 8,
            start: 0,
            end: 100
          },
          {
            start: 100,
            end: 0,
            height: 8
          }
        ]
      }
    },
    // 生成标记点
    createdPointList (history_info, xdata) {
      history_info = history_info || []
      xdata = xdata || []
      var res = []
      history_info.forEach((itm, idx) => {
        // 未提交的坐标提取
        if (itm.battery_state == 0) {
          res.push({
            coord: [xdata[idx], itm.battery_power]
          })
        }
      })
      return res
    },
    // 计算分段数据
    piecesFun () {
      this.piecesList = []
      this.my_series = []

      const segments = this.buildSegments()
      this.piecesList = segments

      segments.forEach(segment => {
        const slice = this.history_info.slice(segment.gte, segment.lte + 1)
        const seriesData = slice.map(item => [item.in_time, item.battery_power])

        this.my_series.push({
          name: '',
          type: 'line',
          data: seriesData,
          smooth: true,
          itemStyle: {
            color: '#3A84FF',
            lineStyle: {
              color: '#1E84FF'
            }
          },
          showSymbol: true,
          symbolSize (value, params) {
            return params.dataIndex === 0 ? 6 : 0
          },
          symbol: 'circle',
          label: {
            normal: {
              show: false,
              position: 'top',
              distance: 10
            }
          },
          markPoint: {
            symbol:
              'image://' + require('../../../assets/images/bty_unactive.png'),
            symbolSize: 6,
            data: this.createdPointList(this.history_info, this.x_data)
          }
        })
      })
    },
    buildSegments () {
      const len = this.history_info.length
      const breaks = this.day_break_list
      const segments = []

      if (!breaks || breaks.length === 0) {
        return [{ gte: 0, lte: len - 1, color: '#3A84FF' }]
      }

      let start = 0
      for (let i = 0; i <= breaks.length; i++) {
        const end = breaks[i] !== undefined ? breaks[i] : len - 1
        if (start <= end) {
          segments.push({
            gte: start,
            lte: end,
            color: '#3A84FF'
          })
        }
        start = end + 1
      }

      return segments
    }
  }
}
</script>


❌
❌