diff --git a/app/(main)/ClientComponents/detail/NetworkChart.tsx b/app/(main)/ClientComponents/detail/NetworkChart.tsx index 083a98d64..2210d5ba6 100644 --- a/app/(main)/ClientComponents/detail/NetworkChart.tsx +++ b/app/(main)/ClientComponents/detail/NetworkChart.tsx @@ -160,12 +160,48 @@ export const NetworkChart = React.memo(function NetworkChart({ return activeChart === defaultChart ? formattedData : chartData[activeChart] } - // 如果开启了削峰,对数据进行处理 const data = ( activeChart === defaultChart ? formattedData : chartData[activeChart] ) as ResultItem[] - const windowSize = 7 // 增加到7个点的移动平均 - const weights = [0.1, 0.1, 0.15, 0.3, 0.15, 0.1, 0.1] // 加权平均的权重 + + const windowSize = 11 // 增加窗口大小以获取更好的统计效果 + const alpha = 0.3 // EWMA平滑因子 + + // 辅助函数:计算中位数 + const getMedian = (arr: number[]) => { + const sorted = [...arr].sort((a, b) => a - b) + const mid = Math.floor(sorted.length / 2) + return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2 + } + + // 辅助函数:异常值处理 + const processValues = (values: number[]) => { + if (values.length === 0) return null + + const median = getMedian(values) + const deviations = values.map((v) => Math.abs(v - median)) + const medianDeviation = getMedian(deviations) * 1.4826 // MAD估计器 + + // 使用中位数绝对偏差(MAD)进行异常值检测 + const validValues = values.filter( + (v) => + Math.abs(v - median) <= 3 * medianDeviation && // 更严格的异常值判定 + v <= median * 3, // 限制最大值不超过中位数的3倍 + ) + + if (validValues.length === 0) return median // 如果没有有效值,返回中位数 + + // 计算EWMA + let ewma = validValues[0] + for (let i = 1; i < validValues.length; i++) { + ewma = alpha * validValues[i] + (1 - alpha) * ewma + } + + return ewma + } + + // 初始化EWMA历史值 + const ewmaHistory: { [key: string]: number } = {} return data.map((point, index) => { if (index < windowSize - 1) return point @@ -174,22 +210,40 @@ export const NetworkChart = React.memo(function NetworkChart({ const smoothed = { ...point } as ResultItem if (activeChart === defaultChart) { - // 处理所有线路的数据 chartDataKey.forEach((key) => { const values = window .map((w) => w[key]) .filter((v) => v !== undefined && v !== null) as number[] - if (values.length === windowSize) { - smoothed[key] = values.reduce((acc, val, idx) => acc + val * weights[idx], 0) + + if (values.length > 0) { + const processed = processValues(values) + if (processed !== null) { + // 应用EWMA平滑 + if (ewmaHistory[key] === undefined) { + ewmaHistory[key] = processed + } else { + ewmaHistory[key] = alpha * processed + (1 - alpha) * ewmaHistory[key] + } + smoothed[key] = ewmaHistory[key] + } } }) } else { - // 处理单条线路的数据 const values = window .map((w) => w.avg_delay) .filter((v) => v !== undefined && v !== null) as number[] - if (values.length === windowSize) { - smoothed.avg_delay = values.reduce((acc, val, idx) => acc + val * weights[idx], 0) + + if (values.length > 0) { + const processed = processValues(values) + if (processed !== null) { + // 应用EWMA平滑 + if (ewmaHistory["current"] === undefined) { + ewmaHistory["current"] = processed + } else { + ewmaHistory["current"] = alpha * processed + (1 - alpha) * ewmaHistory["current"] + } + smoothed.avg_delay = ewmaHistory["current"] + } } } diff --git a/app/(main)/ClientComponents/main/ServerOverviewClient.tsx b/app/(main)/ClientComponents/main/ServerOverviewClient.tsx index 48c244b72..c1d777a39 100644 --- a/app/(main)/ClientComponents/main/ServerOverviewClient.tsx +++ b/app/(main)/ClientComponents/main/ServerOverviewClient.tsx @@ -141,11 +141,11 @@ export default function ServerOverviewClient() { {data?.result ? ( <> -
-

+

+

↑{formatBytes(data?.total_out_bandwidth)}

-

+

↓{formatBytes(data?.total_in_bandwidth)}

diff --git a/bun.lockb b/bun.lockb index 6ca7f0ea6..3989802cc 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index c6031819d..086cc0694 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nezha-dash", - "version": "1.8.2", + "version": "1.8.4", "private": true, "scripts": { "dev": "next dev -p 3040", @@ -15,22 +15,22 @@ "dependencies": { "@ducanh2912/next-pwa": "^10.2.9", "@heroicons/react": "^2.2.0", - "@radix-ui/react-dialog": "^1.1.3", - "@radix-ui/react-dropdown-menu": "^2.1.3", + "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-navigation-menu": "^1.2.2", - "@radix-ui/react-popover": "^1.1.3", + "@radix-ui/react-navigation-menu": "^1.2.3", + "@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", - "@radix-ui/react-tooltip": "^1.1.5", + "@radix-ui/react-tooltip": "^1.1.6", "@trivago/prettier-plugin-sort-imports": "^5.2.0", "@turf/turf": "^7.1.0", "@types/d3-geo": "^3.1.0", "@types/luxon": "^3.4.2", - "@typescript-eslint/eslint-plugin": "^8.18.0", - "caniuse-lite": "^1.0.30001688", + "@typescript-eslint/eslint-plugin": "^8.18.1", + "caniuse-lite": "^1.0.30001689", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "country-flag-icons": "^1.5.13", @@ -42,7 +42,7 @@ "lucide-react": "^0.454.0", "luxon": "^3.5.0", "maxmind": "^4.3.23", - "next": "^15.1.0", + "next": "^15.1.1", "next-auth": "^5.0.0-beta.25", "next-intl": "^3.26.1", "next-runtime-env": "^3.2.2", @@ -57,22 +57,22 @@ "swr": "^2.2.6-beta.5", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", - "typescript-eslint": "^8.18.0" + "typescript-eslint": "^8.18.1" }, "devDependencies": { - "@next/bundle-analyzer": "^15.1.0", - "@tailwindcss/postcss": "^4.0.0-beta.7", + "@next/bundle-analyzer": "^15.1.1", + "@tailwindcss/postcss": "^4.0.0-beta.8", "@types/node": "^22.10.2", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.2", "eslint": "^9.17.0", - "eslint-config-next": "^15.1.0", + "eslint-config-next": "^15.1.1", "eslint-plugin-turbo": "^2.3.3", "eslint-plugin-unused-imports": "^4.1.4", "postcss": "^8.4.49", "prettier": "^3.4.2", "prettier-plugin-tailwindcss": "^0.6.9", - "tailwindcss": "^4.0.0-beta.7", + "tailwindcss": "^4.0.0-beta.8", "typescript": "^5.7.2", "vercel": "^39.2.2" },