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);  
}, []);
相关推荐
银月流苏1 小时前
Vue 深度选择器 `:deep` 使用说明
前端
广州华水科技2 小时前
单北斗GNSS变形监测在地质灾害与基础设施安全中的应用与优势分析
前端
程序员鱼皮2 小时前
又被 Cursor 烧了 1 万块,我麻了。。。
前端·后端·ai·程序员·大模型·编程
孟祥_成都2 小时前
nextjs 16 基础完全指南!(一) - 初步安装
前端·next.js
程序员爱钓鱼2 小时前
使用简单 JSON + 自定义 t 函数实现轻量多语言国际化(无需 next-intl)
前端·javascript·trae
一 乐2 小时前
助农平台|基于SprinBoot+vue的助农服务系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·ecmascript·springboot
vivo互联网技术2 小时前
浅谈 AI 搜索前端打字机效果的实现方案演进
前端·vue·dom
●VON2 小时前
Electron 小游戏实战:太空打砖块(Space Breakout)
前端·javascript·electron
重铸码农荣光2 小时前
深入理解 JavaScript 原型机制:从“如何拿到小米 SU7”说起
前端·javascript