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);  
}, []);
相关推荐
brien99 分钟前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance24 分钟前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M31 分钟前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写33 分钟前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李34 分钟前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易1 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马1 小时前
前沿视界
前端
kyriewen1 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易1 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒1 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端