feat: replace traffic chart rendering component for performance improvement and React 19 compatibility

This commit is contained in:
wonfen
2025-05-14 12:16:59 +08:00
parent 1993e5dd51
commit becc51bcd2
3 changed files with 299 additions and 196 deletions

View File

@@ -47,6 +47,7 @@
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"axios": "^1.8.3", "axios": "^1.8.3",
"chart.js": "^4.4.9",
"cli-color": "^2.0.4", "cli-color": "^2.0.4",
"d3-shape": "^3.2.0", "d3-shape": "^3.2.0",
"dayjs": "1.11.13", "dayjs": "1.11.13",
@@ -61,6 +62,7 @@
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"peggy": "^5.0.0", "peggy": "^5.0.0",
"react": "19.1.0", "react": "19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-error-boundary": "6.0.0", "react-error-boundary": "6.0.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
@@ -69,7 +71,6 @@
"react-monaco-editor": "0.58.0", "react-monaco-editor": "0.58.0",
"react-router-dom": "7.6.0", "react-router-dom": "7.6.0",
"react-virtuoso": "^4.12.7", "react-virtuoso": "^4.12.7",
"recharts": "^2.15.1",
"sockette": "^2.0.6", "sockette": "^2.0.6",
"swr": "^2.3.3", "swr": "^2.3.3",
"tar": "^7.4.3", "tar": "^7.4.3",

32
pnpm-lock.yaml generated
View File

@@ -80,6 +80,9 @@ importers:
axios: axios:
specifier: ^1.8.3 specifier: ^1.8.3
version: 1.9.0 version: 1.9.0
chart.js:
specifier: ^4.4.9
version: 4.4.9
cli-color: cli-color:
specifier: ^2.0.4 specifier: ^2.0.4
version: 2.0.4 version: 2.0.4
@@ -122,6 +125,9 @@ importers:
react: react:
specifier: 19.1.0 specifier: 19.1.0
version: 19.1.0 version: 19.1.0
react-chartjs-2:
specifier: ^5.3.0
version: 5.3.0(chart.js@4.4.9)(react@19.1.0)
react-dom: react-dom:
specifier: 19.1.0 specifier: 19.1.0
version: 19.1.0(react@19.1.0) version: 19.1.0(react@19.1.0)
@@ -147,7 +153,7 @@ importers:
specifier: ^4.12.7 specifier: ^4.12.7
version: 4.12.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 4.12.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
recharts: recharts:
specifier: ^2.15.1 specifier: ^2.15.3
version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
sockette: sockette:
specifier: ^2.0.6 specifier: ^2.0.6
@@ -1006,6 +1012,9 @@ packages:
'@juggle/resize-observer@3.4.0': '@juggle/resize-observer@3.4.0':
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
'@mui/core-downloads-tracker@7.1.0': '@mui/core-downloads-tracker@7.1.0':
resolution: {integrity: sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==} resolution: {integrity: sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==}
@@ -1786,6 +1795,10 @@ packages:
character-reference-invalid@2.0.1: character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
chart.js@4.4.9:
resolution: {integrity: sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==}
engines: {pnpm: '>=8'}
chokidar@4.0.3: chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'} engines: {node: '>= 14.16.0'}
@@ -2615,6 +2628,12 @@ packages:
proxy-from-env@1.1.0: proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
react-chartjs-2@5.3.0:
resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==}
peerDependencies:
chart.js: ^4.1.1
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom@19.1.0: react-dom@19.1.0:
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
peerDependencies: peerDependencies:
@@ -4020,6 +4039,8 @@ snapshots:
'@juggle/resize-observer@3.4.0': {} '@juggle/resize-observer@3.4.0': {}
'@kurkle/color@0.3.4': {}
'@mui/core-downloads-tracker@7.1.0': {} '@mui/core-downloads-tracker@7.1.0': {}
'@mui/icons-material@7.1.0(@mui/material@7.1.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.4)(react@19.1.0)': '@mui/icons-material@7.1.0(@mui/material@7.1.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.4)(react@19.1.0)':
@@ -4740,6 +4761,10 @@ snapshots:
character-reference-invalid@2.0.1: {} character-reference-invalid@2.0.1: {}
chart.js@4.4.9:
dependencies:
'@kurkle/color': 0.3.4
chokidar@4.0.3: chokidar@4.0.3:
dependencies: dependencies:
readdirp: 4.1.2 readdirp: 4.1.2
@@ -5687,6 +5712,11 @@ snapshots:
proxy-from-env@1.1.0: {} proxy-from-env@1.1.0: {}
react-chartjs-2@5.3.0(chart.js@4.4.9)(react@19.1.0):
dependencies:
chart.js: 4.4.9
react: 19.1.0
react-dom@19.1.0(react@19.1.0): react-dom@19.1.0(react@19.1.0):
dependencies: dependencies:
react: 19.1.0 react: 19.1.0

View File

@@ -11,17 +11,27 @@ import {
import { Box, useTheme } from "@mui/material"; import { Box, useTheme } from "@mui/material";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Line as ChartJsLine } from "react-chartjs-2";
import { import {
LineChart, Chart as ChartJS,
Line, CategoryScale,
XAxis, LinearScale,
YAxis, PointElement,
CartesianGrid, LineElement,
Tooltip, Tooltip,
ResponsiveContainer, Filler,
AreaChart, Scale,
Area, Tick,
} from "recharts"; } from "chart.js";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Tooltip,
Filler
);
// 流量数据项接口 // 流量数据项接口
export interface ITrafficItem { export interface ITrafficItem {
@@ -30,13 +40,12 @@ export interface ITrafficItem {
timestamp?: number; timestamp?: number;
} }
// 组件对外暴露的方法 // 对外暴露的接口
export interface EnhancedTrafficGraphRef { export interface EnhancedTrafficGraphRef {
appendData: (data: ITrafficItem) => void; appendData: (data: ITrafficItem) => void;
toggleStyle: () => void; toggleStyle: () => void;
} }
// 时间范围类型
type TimeRange = 1 | 5 | 10; // 分钟 type TimeRange = 1 | 5 | 10; // 分钟
// 数据点类型 // 数据点类型
@@ -76,23 +85,30 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
up: theme.palette.secondary.main, up: theme.palette.secondary.main,
down: theme.palette.primary.main, down: theme.palette.primary.main,
grid: theme.palette.divider, grid: theme.palette.divider,
tooltip: theme.palette.background.paper, tooltipBg: theme.palette.background.paper,
text: theme.palette.text.primary, text: theme.palette.text.primary,
tooltipBorder: theme.palette.divider,
}), }),
[theme] [theme]
); );
// 切换时间范围 // 切换时间范围
const handleTimeRangeClick = useCallback(() => { const handleTimeRangeClick = useCallback((event: React.MouseEvent<SVGTextElement>) => {
event.stopPropagation();
setTimeRange((prevRange) => { setTimeRange((prevRange) => {
// 在1、5、10分钟之间循环切换
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1; return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
}); });
}, []); }, []);
// 点击图表主体或图例时切换样式
const handleToggleStyleClick = useCallback((event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
event.stopPropagation();
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
}, []);
// 初始化数据缓冲区 // 初始化数据缓冲区
useEffect(() => { useEffect(() => {
// 创建初始空数据
const now = Date.now(); const now = Date.now();
const tenMinutesAgo = now - 10 * 60 * 1000; const tenMinutesAgo = now - 10 * 60 * 1000;
@@ -102,17 +118,29 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
const pointTime = const pointTime =
tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE); tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
const date = new Date(pointTime); const date = new Date(pointTime);
let nameValue: string;
try {
if (isNaN(date.getTime())) {
console.warn(`Initial data generation: Invalid date for timestamp ${pointTime}`);
nameValue = "??:??:??";
} else {
nameValue = date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
} catch (e) {
console.error("Error in toLocaleTimeString during initial data gen:", e, "Date:", date, "Timestamp:", pointTime);
nameValue = "Err:Time";
}
return { return {
up: 0, up: 0,
down: 0, down: 0,
timestamp: pointTime, timestamp: pointTime,
name: date.toLocaleTimeString("en-US", { name: nameValue,
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
}; };
} }
); );
@@ -122,45 +150,54 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
// 更新显示数据 // 更新显示数据
const pointsToShow = getMaxPointsByTimeRange(timeRange); const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(initialBuffer.slice(-pointsToShow)); setDisplayData(initialBuffer.slice(-pointsToShow));
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]); }, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange, timeRange]);
// 添加数据点方法 // 添加数据点方法
const appendData = useCallback((data: ITrafficItem) => { const appendData = useCallback((data: ITrafficItem) => {
// 安全处理数据
const safeData = { const safeData = {
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0, up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0, down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
}; };
// 使用提供的时间戳或当前时间
const timestamp = data.timestamp || Date.now(); const timestamp = data.timestamp || Date.now();
const date = new Date(timestamp); const date = new Date(timestamp);
let nameValue: string;
try {
if (isNaN(date.getTime())) {
console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
nameValue = "??:??:??";
} else {
nameValue = date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
} catch (e) {
console.error("Error in toLocaleTimeString in appendData:", e, "Date:", date, "Timestamp:", timestamp);
nameValue = "Err:Time";
}
// 带时间标签的新数据点 // 带时间标签的新数据点
const newPoint: DataPoint = { const newPoint: DataPoint = {
...safeData, ...safeData,
name: date.toLocaleTimeString("en-US", { name: nameValue,
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
timestamp: timestamp, timestamp: timestamp,
}; };
// 更新缓冲区,保持原数组大小
const newBuffer = [...dataBufferRef.current.slice(1), newPoint]; const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
dataBufferRef.current = newBuffer; dataBufferRef.current = newBuffer;
// 更新显示数据
const pointsToShow = getMaxPointsByTimeRange(timeRange); const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(newBuffer.slice(-pointsToShow)); setDisplayData(newBuffer.slice(-pointsToShow));
}, [timeRange, getMaxPointsByTimeRange]); }, [timeRange, getMaxPointsByTimeRange]);
// 监听时间范围变化,更新显示数据 // 监听时间范围变化
useEffect(() => { useEffect(() => {
const pointsToShow = getMaxPointsByTimeRange(timeRange); const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(dataBufferRef.current.slice(-pointsToShow)); if (dataBufferRef.current.length > 0) {
setDisplayData(dataBufferRef.current.slice(-pointsToShow));
}
}, [timeRange, getMaxPointsByTimeRange]); }, [timeRange, getMaxPointsByTimeRange]);
// 切换图表样式 // 切换图表样式
@@ -178,47 +215,166 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
[appendData, toggleStyle] [appendData, toggleStyle]
); );
// 格式化工具提示内容
const formatTooltip = useCallback((value: number, name: string, props: any) => {
const [num, unit] = parseTraffic(value);
return [`${num} ${unit}/s`, props?.dataKey === "up" ? t("Upload") : t("Download")];
}, [t]);
// Y轴刻度格式化 const formatYAxis = useCallback((value: number | string): string => {
const formatYAxis = useCallback((value: number) => { if (typeof value !== 'number') return String(value);
const [num, unit] = parseTraffic(value); const [num, unit] = parseTraffic(value);
return `${num}${unit}`; return `${num}${unit}`;
}, []); }, []);
// 格式化X轴标签 const formatXLabel = useCallback((tickValue: string | number, index: number, ticks: any[]) => {
const formatXLabel = useCallback((value: string) => { const dataPoint = displayData[index as number];
if (!value) return ""; if (dataPoint && dataPoint.name) {
const parts = value.split(":"); const parts = dataPoint.name.split(":");
return `${parts[0]}:${parts[1]}`; return `${parts[0]}:${parts[1]}`;
}, []); }
if(typeof tickValue === 'string') {
const parts = tickValue.split(":");
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
return tickValue;
}
return '';
}, [displayData]);
// 获取当前时间范围文本 // 获取当前时间范围文本
const getTimeRangeText = useCallback(() => { const getTimeRangeText = useCallback(() => {
return t("{{time}} Minutes", { time: timeRange }); return t("{{time}} Minutes", { time: timeRange });
}, [timeRange, t]); }, [timeRange, t]);
// 共享图表配置 const chartData = useMemo(() => {
const chartConfig = useMemo(() => ({ const labels = displayData.map(d => d.name);
data: displayData, return {
margin: { top: 20, right: 10, left: 0, bottom: -10 }, labels,
}), [displayData]); datasets: [
{
label: t("Upload"),
data: displayData.map(d => d.up),
borderColor: colors.up,
backgroundColor: chartStyle === "area" ? colors.up : colors.up,
fill: chartStyle === "area",
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2,
},
{
label: t("Download"),
data: displayData.map(d => d.down),
borderColor: colors.down,
backgroundColor: chartStyle === "area" ? colors.down : colors.down,
fill: chartStyle === "area",
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2,
}
]
};
}, [displayData, colors.up, colors.down, t, chartStyle]);
// 共享的线条/区域配置 const chartOptions = useMemo(() => ({
const commonLineProps = useMemo(() => ({ responsive: true,
dot: false, maintainAspectRatio: false,
strokeWidth: 2, animation: false as false,
connectNulls: false, scales: {
activeDot: { r: 4, strokeWidth: 1 }, x: {
isAnimationActive: false, // 禁用动画以减少CPU使用 display: true,
}), []); type: 'category' as const,
labels: displayData.map(d => d.name),
ticks: {
display: true,
color: colors.text,
font: { size: 10 },
callback: function(this: Scale, tickValue: string | number, index: number, ticks: Tick[]): string | undefined {
let labelToFormat: string | undefined = undefined;
const currentDisplayTick = ticks[index];
if (currentDisplayTick && typeof currentDisplayTick.label === 'string') {
labelToFormat = currentDisplayTick.label;
} else {
const sourceLabels = displayData.map(d => d.name);
if (typeof tickValue === 'number' && tickValue >= 0 && tickValue < sourceLabels.length) {
labelToFormat = sourceLabels[tickValue];
} else if (typeof tickValue === 'string') {
labelToFormat = tickValue;
}
}
if (typeof labelToFormat !== 'string') {
return undefined;
}
const parts: string[] = labelToFormat.split(':');
return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : labelToFormat;
},
autoSkip: true,
maxTicksLimit: Math.max(5, Math.floor(displayData.length / (timeRange * 2))),
minRotation: 0,
maxRotation: 0,
},
grid: {
display: true,
drawOnChartArea: false,
drawTicks: true,
tickLength: 2,
color: colors.text,
},
},
y: {
beginAtZero: true,
ticks: {
color: colors.text,
font: { size: 10 },
callback: formatYAxis,
},
grid: {
display: true,
drawTicks: true,
tickLength: 3,
color: colors.grid,
},
}
},
plugins: {
tooltip: {
enabled: true,
mode: 'index' as const,
intersect: false,
backgroundColor: colors.tooltipBg,
titleColor: colors.text,
bodyColor: colors.text,
borderColor: colors.tooltipBorder,
borderWidth: 1,
cornerRadius: 4,
padding: 8,
callbacks: {
title: (tooltipItems: any[]) => {
return `${t("Time")}: ${tooltipItems[0].label}`;
},
label: (context: any): string => {
const label = context.dataset.label || '';
const value = context.parsed.y;
const [num, unit] = parseTraffic(value);
return `${label}: ${num} ${unit}/s`;
}
}
},
legend: {
display: false
}
},
layout: {
padding: {
top: 16,
right: 7,
left: 3,
}
}
}), [colors, t, formatYAxis, timeRange, displayData]);
// 曲线类型
const curveType = "monotone";
return ( return (
<Box <Box
@@ -230,141 +386,57 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
borderRadius: 1, borderRadius: 1,
cursor: "pointer", cursor: "pointer",
}} }}
onClick={toggleStyle} onClick={handleToggleStyleClick}
> >
<ResponsiveContainer width="100%" height="100%"> <div style={{ width: "100%", height: "100%", position: "relative" }}>
{/* 根据chartStyle动态选择图表类型 */} {displayData.length > 0 && (
{(() => { <ChartJsLine data={chartData} options={chartOptions} />
// 创建共享的图表组件 )}
const commonChartComponents = (
<>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={colors.grid} opacity={0.3} />
<XAxis
dataKey="name"
tick={{ fontSize: 10, fill: colors.text }}
tickLine={{ stroke: colors.grid }}
axisLine={{ stroke: colors.grid }}
interval="preserveStart"
tickFormatter={formatXLabel}
minTickGap={30}
/>
<YAxis
tickFormatter={formatYAxis}
tick={{ fontSize: 10, fill: colors.text }}
tickLine={{ stroke: colors.grid }}
axisLine={{ stroke: colors.grid }}
width={44}
domain={[0, "auto"]}
padding={{ top: 8, bottom: 0 }}
/>
<Tooltip
formatter={formatTooltip}
labelFormatter={(label) => `${t("Time")}: ${label}`}
contentStyle={{
backgroundColor: colors.tooltip,
borderColor: colors.grid,
borderRadius: 4,
}}
itemStyle={{ color: colors.text }}
isAnimationActive={false}
/>
{/* 可点击的时间范围标签 */} <svg width="100%" height="100%" style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}>
<text <text
x="1%" x="3.5%"
y="11%" y="10%"
textAnchor="start" textAnchor="start"
fill={theme.palette.text.secondary} fill={theme.palette.text.secondary}
fontSize={11} fontSize={11}
fontWeight="bold" fontWeight="bold"
onClick={handleTimeRangeClick} onClick={handleTimeRangeClick}
style={{ cursor: "pointer" }} style={{ cursor: "pointer", pointerEvents: 'all' }}
> >
{getTimeRangeText()} {getTimeRangeText()}
</text> </text>
{/* 上传标签 - 右上角 */} <text
<text x="99%"
x="99%" y="10%"
y="11%" textAnchor="end"
textAnchor="end" fill={colors.up}
fill={colors.up} fontSize={12}
fontSize={12} fontWeight="bold"
fontWeight="bold" onClick={handleToggleStyleClick}
onClick={toggleStyle} style={{ cursor: "pointer", pointerEvents: 'all' }}
style={{ cursor: "pointer" }} >
> {t("Upload")}
{t("Upload")} </text>
</text>
{/* 下载标签 - 右上角下方 */} <text
<text x="99%"
x="99%" y="19%"
y="19%" textAnchor="end"
textAnchor="end" fill={colors.down}
fill={colors.down} fontSize={12}
fontSize={12} fontWeight="bold"
fontWeight="bold" onClick={handleToggleStyleClick}
onClick={toggleStyle} style={{ cursor: "pointer", pointerEvents: 'all' }}
style={{ cursor: "pointer" }} >
> {t("Download")}
{t("Download")} </text>
</text> </svg>
</> </div>
);
// 根据chartStyle返回相应的图表类型
if (chartStyle === "line") {
return (
<LineChart {...chartConfig}>
{commonChartComponents}
<Line
type={curveType}
{...commonLineProps}
dataKey="up"
name={t("Upload")}
stroke={colors.up}
/>
<Line
type={curveType}
{...commonLineProps}
dataKey="down"
name={t("Download")}
stroke={colors.down}
/>
</LineChart>
);
} else {
return (
<AreaChart {...chartConfig}>
{commonChartComponents}
<Area
type={curveType}
{...commonLineProps}
dataKey="up"
name={t("Upload")}
stroke={colors.up}
fill={colors.up}
fillOpacity={0.2}
/>
<Area
type={curveType}
{...commonLineProps}
dataKey="down"
name={t("Download")}
stroke={colors.down}
fill={colors.down}
fillOpacity={0.3}
/>
</AreaChart>
);
}
})()}
</ResponsiveContainer>
</Box> </Box>
); );
}, },
)); ));
// 添加显示名称以便调试
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph"; EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";