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

📘 一、背景介绍

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

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

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

📊 二、效果说明

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

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

⚙️ 三、核心逻辑分析

1️⃣ 图表初始化

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

js 复制代码
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轴设置一个可伸缩。

js 复制代码
{
  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️⃣ 自动分段构造

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

js 复制代码
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 为下一段的起始。

📌 举个例子:

js 复制代码
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项

最终返回的数据结构:

js 复制代码
[
  { gte: 0, lte: 2 },
  { gte: 3, lte: 5 },
  { gte: 6, lte: 7 },
  { gte: 8, lte: 9 }
]

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

js 复制代码
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 设备监控 在线/离线分段显示、设备心跳波动
车辆电池监控系统 显示充放电过程,支持按时间切片
医疗设备监控 重要参数监控断段可视化警示

📚 六、总结

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

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

源码

kotlin 复制代码
<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>
相关推荐
OEC小胖胖4 小时前
去中心化身份:2025年Web3身份验证系统开发实践
前端·web3·去中心化·区块链
Cacciatore->5 小时前
Electron 快速上手
javascript·arcgis·electron
vvilkim5 小时前
Electron 进程间通信(IPC)深度优化指南
前端·javascript·electron
某公司摸鱼前端6 小时前
ES13(ES2022)新特性整理
javascript·ecmascript·es13
ai小鬼头7 小时前
百度秒搭发布:无代码编程如何让普通人轻松打造AI应用?
前端·后端·github
漂流瓶jz7 小时前
清除浮动/避开margin折叠:前端CSS中BFC的特点与限制
前端·css·面试
前端 贾公子7 小时前
在移动端使用 Tailwind CSS (uniapp)
前端·uni-app
散步去海边7 小时前
Cursor 进阶使用教程
前端·ai编程·cursor
清幽竹客7 小时前
vue-30(理解 Nuxt.js 目录结构)
前端·javascript·vue.js
weiweiweb8887 小时前
cesium加载Draco几何压缩数据
前端·javascript·vue.js