📘 一、背景介绍
在数据可视化中,有时候我们需要根据数据状态、时间断点等将一条数据曲线分段展示,如:
- 电池电量在中断恢复后重新记录
- 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
(小于等于)字段表示索引范围,用于后续图表按段绘制。如果没有传入断点数组,表示整个数据不分段,直接返回一个完整区间。
🔄 循环行为解读:
-
循环
i
从0
到breaks.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>