feat: replace traffic chart rendering component for performance improvement and React 19 compatibility
This commit is contained in:
@@ -47,6 +47,7 @@
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.8.3",
|
||||
"chart.js": "^4.4.9",
|
||||
"cli-color": "^2.0.4",
|
||||
"d3-shape": "^3.2.0",
|
||||
"dayjs": "1.11.13",
|
||||
@@ -61,6 +62,7 @@
|
||||
"nanoid": "^5.1.5",
|
||||
"peggy": "^5.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
@@ -69,7 +71,6 @@
|
||||
"react-monaco-editor": "0.58.0",
|
||||
"react-router-dom": "7.6.0",
|
||||
"react-virtuoso": "^4.12.7",
|
||||
"recharts": "^2.15.1",
|
||||
"sockette": "^2.0.6",
|
||||
"swr": "^2.3.3",
|
||||
"tar": "^7.4.3",
|
||||
|
||||
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@@ -80,6 +80,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.8.3
|
||||
version: 1.9.0
|
||||
chart.js:
|
||||
specifier: ^4.4.9
|
||||
version: 4.4.9
|
||||
cli-color:
|
||||
specifier: ^2.0.4
|
||||
version: 2.0.4
|
||||
@@ -122,6 +125,9 @@ importers:
|
||||
react:
|
||||
specifier: 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:
|
||||
specifier: 19.1.0
|
||||
version: 19.1.0(react@19.1.0)
|
||||
@@ -147,7 +153,7 @@ importers:
|
||||
specifier: ^4.12.7
|
||||
version: 4.12.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
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)
|
||||
sockette:
|
||||
specifier: ^2.0.6
|
||||
@@ -1006,6 +1012,9 @@ packages:
|
||||
'@juggle/resize-observer@3.4.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==}
|
||||
|
||||
@@ -1786,6 +1795,10 @@ packages:
|
||||
character-reference-invalid@2.0.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
@@ -2615,6 +2628,12 @@ packages:
|
||||
proxy-from-env@1.1.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
|
||||
peerDependencies:
|
||||
@@ -4020,6 +4039,8 @@ snapshots:
|
||||
|
||||
'@juggle/resize-observer@3.4.0': {}
|
||||
|
||||
'@kurkle/color@0.3.4': {}
|
||||
|
||||
'@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)':
|
||||
@@ -4740,6 +4761,10 @@ snapshots:
|
||||
|
||||
character-reference-invalid@2.0.1: {}
|
||||
|
||||
chart.js@4.4.9:
|
||||
dependencies:
|
||||
'@kurkle/color': 0.3.4
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
@@ -5687,6 +5712,11 @@ snapshots:
|
||||
|
||||
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):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
@@ -11,17 +11,27 @@ import {
|
||||
import { Box, useTheme } from "@mui/material";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Line as ChartJsLine } from "react-chartjs-2";
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
AreaChart,
|
||||
Area,
|
||||
} from "recharts";
|
||||
Filler,
|
||||
Scale,
|
||||
Tick,
|
||||
} from "chart.js";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Tooltip,
|
||||
Filler
|
||||
);
|
||||
|
||||
// 流量数据项接口
|
||||
export interface ITrafficItem {
|
||||
@@ -30,13 +40,12 @@ export interface ITrafficItem {
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// 组件对外暴露的方法
|
||||
// 对外暴露的接口
|
||||
export interface EnhancedTrafficGraphRef {
|
||||
appendData: (data: ITrafficItem) => void;
|
||||
toggleStyle: () => void;
|
||||
}
|
||||
|
||||
// 时间范围类型
|
||||
type TimeRange = 1 | 5 | 10; // 分钟
|
||||
|
||||
// 数据点类型
|
||||
@@ -76,23 +85,30 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
up: theme.palette.secondary.main,
|
||||
down: theme.palette.primary.main,
|
||||
grid: theme.palette.divider,
|
||||
tooltip: theme.palette.background.paper,
|
||||
tooltipBg: theme.palette.background.paper,
|
||||
text: theme.palette.text.primary,
|
||||
tooltipBorder: theme.palette.divider,
|
||||
}),
|
||||
[theme]
|
||||
);
|
||||
|
||||
// 切换时间范围
|
||||
const handleTimeRangeClick = useCallback(() => {
|
||||
const handleTimeRangeClick = useCallback((event: React.MouseEvent<SVGTextElement>) => {
|
||||
event.stopPropagation();
|
||||
setTimeRange((prevRange) => {
|
||||
// 在1、5、10分钟之间循环切换
|
||||
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(() => {
|
||||
// 创建初始空数据
|
||||
const now = Date.now();
|
||||
const tenMinutesAgo = now - 10 * 60 * 1000;
|
||||
|
||||
@@ -102,17 +118,29 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
const pointTime =
|
||||
tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
|
||||
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 {
|
||||
up: 0,
|
||||
down: 0,
|
||||
timestamp: pointTime,
|
||||
name: date.toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}),
|
||||
name: nameValue,
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -122,45 +150,54 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
// 更新显示数据
|
||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||
setDisplayData(initialBuffer.slice(-pointsToShow));
|
||||
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]);
|
||||
|
||||
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange, timeRange]);
|
||||
// 添加数据点方法
|
||||
const appendData = useCallback((data: ITrafficItem) => {
|
||||
// 安全处理数据
|
||||
const safeData = {
|
||||
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
|
||||
down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
|
||||
};
|
||||
|
||||
// 使用提供的时间戳或当前时间
|
||||
const timestamp = data.timestamp || Date.now();
|
||||
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 = {
|
||||
...safeData,
|
||||
name: date.toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}),
|
||||
name: nameValue,
|
||||
timestamp: timestamp,
|
||||
};
|
||||
|
||||
// 更新缓冲区,保持原数组大小
|
||||
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
||||
dataBufferRef.current = newBuffer;
|
||||
|
||||
// 更新显示数据
|
||||
|
||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||
setDisplayData(newBuffer.slice(-pointsToShow));
|
||||
}, [timeRange, getMaxPointsByTimeRange]);
|
||||
|
||||
// 监听时间范围变化,更新显示数据
|
||||
// 监听时间范围变化
|
||||
useEffect(() => {
|
||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||
setDisplayData(dataBufferRef.current.slice(-pointsToShow));
|
||||
if (dataBufferRef.current.length > 0) {
|
||||
setDisplayData(dataBufferRef.current.slice(-pointsToShow));
|
||||
}
|
||||
}, [timeRange, getMaxPointsByTimeRange]);
|
||||
|
||||
// 切换图表样式
|
||||
@@ -178,47 +215,166 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
[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) => {
|
||||
const formatYAxis = useCallback((value: number | string): string => {
|
||||
if (typeof value !== 'number') return String(value);
|
||||
const [num, unit] = parseTraffic(value);
|
||||
return `${num}${unit}`;
|
||||
}, []);
|
||||
|
||||
// 格式化X轴标签
|
||||
const formatXLabel = useCallback((value: string) => {
|
||||
if (!value) return "";
|
||||
const parts = value.split(":");
|
||||
return `${parts[0]}:${parts[1]}`;
|
||||
}, []);
|
||||
const formatXLabel = useCallback((tickValue: string | number, index: number, ticks: any[]) => {
|
||||
const dataPoint = displayData[index as number];
|
||||
if (dataPoint && dataPoint.name) {
|
||||
const parts = dataPoint.name.split(":");
|
||||
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(() => {
|
||||
return t("{{time}} Minutes", { time: timeRange });
|
||||
}, [timeRange, t]);
|
||||
|
||||
// 共享图表配置
|
||||
const chartConfig = useMemo(() => ({
|
||||
data: displayData,
|
||||
margin: { top: 20, right: 10, left: 0, bottom: -10 },
|
||||
}), [displayData]);
|
||||
const chartData = useMemo(() => {
|
||||
const labels = displayData.map(d => d.name);
|
||||
return {
|
||||
labels,
|
||||
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 commonLineProps = useMemo(() => ({
|
||||
dot: false,
|
||||
strokeWidth: 2,
|
||||
connectNulls: false,
|
||||
activeDot: { r: 4, strokeWidth: 1 },
|
||||
isAnimationActive: false, // 禁用动画以减少CPU使用
|
||||
}), []);
|
||||
const chartOptions = useMemo(() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false as false,
|
||||
scales: {
|
||||
x: {
|
||||
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 (
|
||||
<Box
|
||||
@@ -230,141 +386,57 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
borderRadius: 1,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={toggleStyle}
|
||||
onClick={handleToggleStyleClick}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{/* 根据chartStyle动态选择图表类型 */}
|
||||
{(() => {
|
||||
// 创建共享的图表组件
|
||||
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}
|
||||
/>
|
||||
|
||||
{/* 可点击的时间范围标签 */}
|
||||
<text
|
||||
x="1%"
|
||||
y="11%"
|
||||
textAnchor="start"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={11}
|
||||
fontWeight="bold"
|
||||
onClick={handleTimeRangeClick}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{getTimeRangeText()}
|
||||
</text>
|
||||
|
||||
{/* 上传标签 - 右上角 */}
|
||||
<text
|
||||
x="99%"
|
||||
y="11%"
|
||||
textAnchor="end"
|
||||
fill={colors.up}
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
onClick={toggleStyle}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("Upload")}
|
||||
</text>
|
||||
<div style={{ width: "100%", height: "100%", position: "relative" }}>
|
||||
{displayData.length > 0 && (
|
||||
<ChartJsLine data={chartData} options={chartOptions} />
|
||||
)}
|
||||
|
||||
<svg width="100%" height="100%" style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}>
|
||||
<text
|
||||
x="3.5%"
|
||||
y="10%"
|
||||
textAnchor="start"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={11}
|
||||
fontWeight="bold"
|
||||
onClick={handleTimeRangeClick}
|
||||
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||
>
|
||||
{getTimeRangeText()}
|
||||
</text>
|
||||
|
||||
<text
|
||||
x="99%"
|
||||
y="10%"
|
||||
textAnchor="end"
|
||||
fill={colors.up}
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
onClick={handleToggleStyleClick}
|
||||
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||
>
|
||||
{t("Upload")}
|
||||
</text>
|
||||
|
||||
{/* 下载标签 - 右上角下方 */}
|
||||
<text
|
||||
x="99%"
|
||||
y="19%"
|
||||
textAnchor="end"
|
||||
fill={colors.down}
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
onClick={toggleStyle}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("Download")}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
|
||||
// 根据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>
|
||||
<text
|
||||
x="99%"
|
||||
y="19%"
|
||||
textAnchor="end"
|
||||
fill={colors.down}
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
onClick={handleToggleStyleClick}
|
||||
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||
>
|
||||
{t("Download")}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
));
|
||||
|
||||
// 添加显示名称以便调试
|
||||
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";
|
||||
|
||||
Reference in New Issue
Block a user