React + ECharts 动态折线图实现

上周项目接到一个新的需求,实现一个每3秒动态更新数据 的折线图,刚开始有点懵逼了,echarts如何能做成动态的,最终实现的效果如下:

  • 图表不断向左平滑滚动
  • xAxis 始终保持 180 个点的滑动窗口
  • 第一次加载一批历史数据,之后每 3 秒追加新数据
  • 新旧数据无缝衔接

实现思路:

  1. 初始化阶段:创建 ECharts 实例
  2. 第一次数据加载(loadData):填满 180 点窗口
  3. 定时更新(smoothAdvance):每 3 秒滑动窗口
  4. 图表渲染(updateChart)
  5. 图表自适应(ResizeObserver)

遇到的问题:

  1. 第一次初始化数据的最后一个点和后续第一个点之间断裂(不连线)

  2. 第一次初始化数据的最后的连接点跟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);  
}, []);
相关推荐
dlhto10 分钟前
前端登录验证码组件
前端
@万里挑一12 分钟前
vue中使用虚拟列表,封装虚拟列表
前端·javascript·vue.js
黑臂麒麟16 分钟前
Electron for OpenHarmony 跨平台实战开发:Electron 文件系统操作实战
前端·javascript·electron·openharmony
wordbaby16 分钟前
Tanstack Router 文件命名速查表
前端
1024肥宅23 分钟前
工程化工具类:模块化系统全解析与实践
前端·javascript·面试
软件技术NINI24 分钟前
如何学习前端
前端·学习
weixin_4225554225 分钟前
ezuikit-js官网使用示例
前端·javascript·vue·ezuikit-js
梓仁沐白35 分钟前
CSAPP-Attacklab
前端
郑州光合科技余经理1 小时前
海外国际版同城服务系统开发:PHP技术栈
java·大数据·开发语言·前端·人工智能·架构·php
一行注释1 小时前
前端数据加密:保护用户数据的第一道防线
前端