🔋 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>
相关推荐
mldong5 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排6 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60616 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角7 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!7 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰8 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi9 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen10 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马11 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu11 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss