React?+?ECharts動(dòng)態(tài)折線圖完整實(shí)現(xiàn)代碼
上周項(xiàng)目接到一個(gè)新的需求,實(shí)現(xiàn)一個(gè)每3秒動(dòng)態(tài)更新數(shù)據(jù) 的折線圖,剛開始有點(diǎn)懵逼了,echarts如何能做成動(dòng)態(tài)的,最終實(shí)現(xiàn)的效果如下:
- 圖表不斷向左平滑滾動(dòng)
- xAxis 始終保持 180 個(gè)點(diǎn)的滑動(dòng)窗口
- 第一次加載一批歷史數(shù)據(jù),之后每 3 秒追加新數(shù)據(jù)
- 新舊數(shù)據(jù)無縫銜接

實(shí)現(xiàn)思路:
- 初始化階段:創(chuàng)建 ECharts 實(shí)例
- 第一次數(shù)據(jù)加載(loadData):填滿 180 點(diǎn)窗口
- 定時(shí)更新(smoothAdvance):每 3 秒滑動(dòng)窗口
- 圖表渲染(updateChart)
- 圖表自適應(yīng)(ResizeObserver)
遇到的問題:
1. 第一次初始化數(shù)據(jù)的最后一個(gè)點(diǎn)和后續(xù)第一個(gè)點(diǎn)之間斷裂(不連線)
2. 第一次初始化數(shù)據(jù)的最后的連接點(diǎn)跟3s后更新數(shù)據(jù)的第一個(gè)點(diǎn)的連接線Tooltip顯示undifiend
代碼實(shí)現(xiàn)如下:
初始化階段:創(chuàng)建 ECharts 實(shí)例
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}
`;
validParams.forEach(item => {
text += `${item.marker}${item.seriesName}: ${formatSpeedForStats(item.data, "bit")}
`;
});
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: []
}
]
});
};
echarts數(shù)據(jù)處理方法
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};
};
第一次數(shù)據(jù)加載:填滿180點(diǎn)窗口
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);
}
};
定時(shí)更新:每3s滑動(dòng)窗口 刪除舊的三個(gè)點(diǎn),添加最新點(diǎn)
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];
//避免重復(fù)點(diǎn)
const filtered = newTimeData.filter(t => t !== lastTime);
return [...kept, ...filtered];
});
//null值用 NaN 填充,避免折線斷裂 同時(shí)避免在連線觸發(fā)tooltip顯示(應(yīng)該在每個(gè)點(diǎn)顯示)
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);
}
};
圖表渲染 只更新坐標(biāo)軸 數(shù)據(jù)數(shù)組
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]);
圖表自適應(yīng) (ResizeObserver)
useEffect(() => {
const observer = new ResizeObserver(() => {
chartInstance.current?.resize();
});
if (chartRef.current) observer.observe(chartRef.current);
return () => observer.disconnect();
}, []);
圖表初始化渲染(第一次加載數(shù)據(jù)渲染)
useEffect(() => {
if (!chartInstance.current) initChart();
loadData();
const timer = setInterval(smoothAdvance, 3000);
return () => clearInterval(timer);
}, []);
總結(jié)
到此這篇關(guān)于React + ECharts動(dòng)態(tài)折線圖完整實(shí)現(xiàn)代碼的文章就介紹到這了,更多相關(guān)React ECharts動(dòng)態(tài)折線圖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Taro?React自定義TabBar使用useContext解決底部選中異常
這篇文章主要為大家介紹了Taro?React底部自定義TabBar使用React?useContext解決底部選中異常(需要點(diǎn)兩次才能選中的問題)示例詳解,有需要的朋友可以借鑒參考下2023-08-08
react中路由跳轉(zhuǎn)及傳參的實(shí)現(xiàn)
本文主要介紹了react中路由跳轉(zhuǎn)及傳參的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
淺談React-router v6 實(shí)現(xiàn)登錄驗(yàn)證流程
本文主要介紹了React-router v6 實(shí)現(xiàn)登錄驗(yàn)證流程,主要介紹了公共頁面、受保護(hù)頁面和登錄頁面,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05
ReactQuery系列React?Query?實(shí)踐示例詳解
這篇文章主要為大家介紹了ReactQuery系列React?Query?實(shí)踐示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
React配置Redux并結(jié)合本地存儲設(shè)置token方式
這篇文章主要介紹了React配置Redux并結(jié)合本地存儲設(shè)置token方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01

