上周项目接到一个新的需求,实现一个每3秒动态更新数据 的折线图,刚开始有点懵逼了,echarts如何能做成动态的,最终实现的效果如下:
- 图表不断向左平滑滚动
- xAxis 始终保持 180 个点的滑动窗口
- 第一次加载一批历史数据,之后每 3 秒追加新数据
- 新旧数据无缝衔接

实现思路:
- 初始化阶段:创建 ECharts 实例
- 第一次数据加载(loadData):填满 180 点窗口
- 定时更新(smoothAdvance):每 3 秒滑动窗口
- 图表渲染(updateChart)
- 图表自适应(ResizeObserver)
遇到的问题:
-
第一次初始化数据的最后一个点和后续第一个点之间断裂(不连线)
-
第一次初始化数据的最后的连接点跟3s后更新数据的第一个点的连接线Tooltip显示undifiend
代码实现如下:
初始化阶段:创建 ECharts 实例
yaml
const initChart = () => {
if (!chartRef.current) return;
chartInstance.current = echarts.init(chartRef.current);
chartInstance.current.setOption({
title: {text: labelList.realTime_rx_or_tx_rate, left: "center"},
tooltip: {
trigger: "axis",
formatter: params => {
//避免Tooltip显示undifiend
const validParams = params.filter(p => p.data != null && !Number.isNaN(p.data));
if (!validParams.length) return "";
let text = `${params[0].axisValue}<br/>`;
validParams.forEach(item => {
text += `${item.marker}${item.seriesName}: ${formatSpeedForStats(item.data, "bit")}<br/>`;
});
return text;
}
},
grid: {left: 120, right: 20, bottom: 40, top: 80},
xAxis: {type: "category", boundaryGap: false, data: []},
yAxis: {
type: "value",
axisLabel: {
formatter: value => formatSpeedForStats(value, "bit")
},
splitLine: {lineStyle: {type: "dashed"}}
},
series: [
{
name: labelList.tx,
type: "line",
smooth: false,
showSymbol: false,
connectNulls: true,
areaStyle: {color: "rgba(0, 180, 0, 0.2)"},
lineStyle: {color: "#00c853", width: 2},
data: []
},
{
name: labelList.rx,
type: "line",
smooth: false,
showSymbol: false,
connectNulls: true,
areaStyle: {color: "rgba(0, 102, 255, 0.2)"},
lineStyle: {color: "#0066ff", width: 2},
data: []
}
]
});
};
ini
echarts数据处理方法
const processDataPoints = result => {
if (!*Array*.isArray(result) || result.length < 2) return {newTimeData: [], newTxData: [], newRxData: []};
const newTimeData = [];
const newTxData = [];
const newRxData = [];
for (let i = 1; i < result.length; i++) {
const [t2, rx2, , tx2] = result[i];
const [t1, rx1, , tx1] = result[i - 1];
const txRate = ((tx2 - tx1) * 8) / 1024 / (t2 - t1);
const rxRate = ((rx2 - rx1) * 8) / 1024 / (t2 - t1);
const timeStr = new *Date*(t2 * 1000).toLocaleTimeString("zh-CN", {hour12: false});
newTimeData.push(timeStr);
newTxData.push(parseFloat(txRate.toFixed(2)));
newRxData.push(parseFloat(rxRate.toFixed(2)));
}
return {newTimeData, newTxData, newRxData};
};
第一次数据加载:填满180点窗口
ini
const loadData = async () => {
try {
const res = await *api*.request({
luci: {
getRealtimeStats: {mode: "interface", device}
}
});
const result = res?.result;
const {newTimeData, newTxData, newRxData} = processDataPoints(result);
// 初始化用 NaN 填充,避免折线断裂
const fullTime = *Array*(TOTAL_POINTS).fill("");
const fullTx = *Array*(TOTAL_POINTS).fill(*NaN*);
const fullRx = *Array*(TOTAL_POINTS).fill(*NaN*);
const len = newTimeData.length;
const start = TOTAL_POINTS - len;
for (let i = 0; i < len; i++) {
fullTime[start + i] = newTimeData[i];
fullTx[start + i] = newTxData[i];
fullRx[start + i] = newRxData[i];
}
setTimeData(fullTime);
setTxData(fullTx);
setRxData(fullRx);
} catch (err) {
*console*.error(err);
}
};
定时更新:每3s滑动窗口 删除旧的三个点,添加最新点
ini
const smoothAdvance = async () => {
try {
const res = await *api*.request({
luci: {
getRealtimeStats: {mode: "interface", device}
}
});
const result = res?.result;
const {newTimeData, newTxData, newRxData} = processDataPoints(result.slice(-4));
setTimeData(prev => {
const kept = prev.slice(3);
const lastTime = kept[kept.length - 1];
//避免重复点
const filtered = newTimeData.filter(t => t !== lastTime);
return [...kept, ...filtered];
});
//null值用 NaN 填充,避免折线断裂 同时避免在连线触发tooltip显示(应该在每个点显示)
setTxData(prev => {
const kept = prev.slice(3).map(v => (v == null ? "NaN" : v));
return [...kept, ...newTxData];
});
setRxData(prev => {
const kept = prev.slice(3).map(v => (v == null ? "NaN": v));
return [...kept, ...newRxData];
});
} catch (e) {
*console*.error("smoothAdvance error:", e);
}
};
图表渲染 只更新坐标轴 数据数组
ini
const updateChart = (timeArr, txArr, rxArr) => {
if (!chartInstance.current) return;
chartInstance.current.setOption(
{
xAxis: {data: timeArr},
series: [{data: txArr}, {data: rxArr}]
},
false
);
};
useEffect(() => {
if (chartInstance.current) {
updateChart(timeData, txData, rxData);
}
}, [timeData, txData, rxData]);
图表自适应 (ResizeObserver)
scss
useEffect(() => {
const observer = new ResizeObserver(() => {
chartInstance.current?.resize();
});
if (chartRef.current) observer.observe(chartRef.current);
return () => observer.disconnect();
}, []);
图表初始化渲染(第一次加载数据渲染)
scss
useEffect(() => {
if (!chartInstance.current) initChart();
loadData();
const timer = setInterval(smoothAdvance, 3000);
return () => clearInterval(timer);
}, []);