feat: 优化图表显示

This commit is contained in:
Montia37
2025-09-09 16:01:56 +08:00
parent de86264262
commit 8b3e7b8f40
4 changed files with 161 additions and 91 deletions

View File

@@ -132,10 +132,12 @@
@layer utilities {
.dark .radix-themes {
--theme-text-muted-color: rgb(from var(--accent-4) r g b / 0.8);
--theme-line-muted-color: rgb(from var(--accent-2) r g b / 0.5);
}
.radix-themes {
--theme-text-muted-color: rgb(from var(--accent-12) r g b / 0.8);
--theme-line-muted-color: rgb(from var(--accent-10) r g b / 0.5);
}
.dark .rt-Badge-tag-transparent {

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useRef } from "react";
import { memo, useRef } from "react";
import {
AreaChart,
Area,
@@ -17,6 +17,7 @@ import { Flex } from "@radix-ui/themes";
import Loading from "@/components/loading";
import { useLoadCharts } from "@/hooks/useLoadCharts";
import { CustomTooltip } from "@/components/ui/tooltip";
import { lableFormatter, loadChartTimeFormatter } from "@/utils/chartHelper";
interface LoadChartsProps {
node: NodeData;
@@ -33,38 +34,6 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
const chartDataLengthRef = useRef(0);
chartDataLengthRef.current = chartData.length;
// 格式化函数
const timeFormatter = useCallback((value: any, index: number) => {
if (chartDataLengthRef.current === 0) return "";
if (index === 0 || index === chartDataLengthRef.current - 1) {
return new Date(value).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
}
return "";
}, []);
const labelFormatter = useCallback(
(value: any) => {
const date = new Date(value);
if (hours === 0) {
return date.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
return date.toLocaleString([], {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
},
[hours]
);
// 样式和颜色
const cn = "flex flex-col w-full overflow-hidden";
const chartMargin = { top: 8, right: 16, bottom: 8, left: 16 };
@@ -262,7 +231,7 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
{...chartProps}>
<CartesianGrid
strokeDasharray="2 4"
stroke="var(--muted-foreground)"
stroke="var(--theme-line-muted-color)"
vertical={false}
/>
<XAxis
@@ -274,7 +243,13 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
tick={{
fill: "var(--theme-text-muted-color)",
}}
tickFormatter={timeFormatter}
tickFormatter={(value, index) =>
loadChartTimeFormatter(
value,
index,
chartDataLengthRef.current
)
}
interval={0}
height={20}
/>
@@ -298,7 +273,7 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
<CustomTooltip
{...props}
chartConfig={config}
labelFormatter={labelFormatter}
labelFormatter={(value) => lableFormatter(value, hours)}
/>
)}
/>

View File

@@ -1,7 +1,7 @@
import { memo, useState, useMemo, useCallback, useEffect } from "react";
import { memo, useState, useMemo, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { useIsMobile } from "@/hooks/useMobile";
import { Eye, EyeOff } from "lucide-react";
import { Eye, EyeOff, ArrowRightToLine, RefreshCw } from "lucide-react";
import {
LineChart,
Line,
@@ -26,6 +26,7 @@ import fillMissingTimePoints, {
import { useConfigItem } from "@/config";
import { CustomTooltip } from "@/components/ui/tooltip";
import Tips from "@/components/ui/tips";
import { generateColor, lableFormatter } from "@/utils/chartHelper";
interface PingChartProps {
node: NodeData;
@@ -36,10 +37,15 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
const { loading, error, pingHistory } = usePingChart(node, hours);
const [visiblePingTasks, setVisiblePingTasks] = useState<number[]>([]);
const [timeRange, setTimeRange] = useState<[number, number] | null>(null);
const [brushIndices, setBrushIndices] = useState<{
startIndex?: number;
endIndex?: number;
}>({});
const [cutPeak, setCutPeak] = useState(false);
const [connectBreaks, setConnectBreaks] = useState(
useConfigItem("enableConnectBreaks")
);
const [isResetting, setIsResetting] = useState(false);
const maxPointsToRender = useConfigItem("pingChartMaxPoints") || 0; // 0表示不限制
const isMobile = useIsMobile();
@@ -55,25 +61,13 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
}
}, [pingHistory?.tasks]);
const lableFormatter = useCallback(
(value: any) => {
const date = new Date(value);
if (hours === 0) {
return date.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
return date.toLocaleString([], {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
},
[hours]
);
useEffect(() => {
if (isResetting) {
setTimeRange(null);
setBrushIndices({});
setIsResetting(false);
}
}, [isResetting]);
const chartMargin = { top: 8, right: 16, bottom: 8, left: 16 };
@@ -170,32 +164,6 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
return [...pingHistory.tasks].sort((a, b) => a.id - b.id);
}, [pingHistory?.tasks]);
const generateColor = useCallback(
(taskName: string, total: number) => {
const index = sortedTasks.findIndex((t) => t.name === taskName);
if (index === -1) return "#000000"; // Fallback color
const hue = (index * (360 / total)) % 360;
// 使用OKLCH色彩空间优化折线图的颜色区分度
// L=0.7 (较高亮度,便于在图表背景上清晰显示)
// C=0.2 (较高饱和度,增强颜色区分度)
const oklchColor = `oklch(0.6 0.2 ${hue} / .8)`;
// 为不支持OKLCH的浏览器提供HSL备用色
// 使用更高的饱和度和适中的亮度来匹配OKLCH的视觉效果
const hslFallback = `hsl(${hue}, 50%, 60%)`;
// 检查浏览器是否支持OKLCH
if (CSS.supports("color", oklchColor)) {
return oklchColor;
} else {
return hslFallback;
}
},
[sortedTasks]
);
const breakPoints = useMemo(() => {
if (!connectBreaks || !chartData || chartData.length < 2) {
return [];
@@ -219,13 +187,13 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
if (isBreak) {
points.push({
x: currentPoint.time,
color: generateColor(task.name, sortedTasks.length),
color: generateColor(task.name, sortedTasks),
});
}
}
}
return points;
}, [chartData, sortedTasks, visiblePingTasks, generateColor, connectBreaks]);
}, [chartData, sortedTasks, visiblePingTasks, connectBreaks]);
const taskStats = useMemo(() => {
if (!pingHistory?.records || !sortedTasks.length) return [];
@@ -241,10 +209,10 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
value: latestValue,
time: latestTime,
loss: loss,
color: generateColor(task.name, sortedTasks.length),
color: generateColor(task.name, sortedTasks),
};
});
}, [pingHistory?.records, sortedTasks, generateColor, timeRange]);
}, [pingHistory?.records, sortedTasks, timeRange]);
return (
<div className="relative space-y-4">
@@ -342,7 +310,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
</Tips>
</div>
</div>
<div className={isMobile ? "w-full mt-2" : ""}>
<div className={`flex gap-2 ${isMobile ? "w-full mt-2" : ""}`}>
<Button variant="secondary" onClick={handleToggleAll} size="sm">
{pingHistory?.tasks &&
visiblePingTasks.length === pingHistory.tasks.length ? (
@@ -357,6 +325,38 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
</>
)}
</Button>
<Button
variant="secondary"
onClick={() => {
if (timeRange) {
if (chartData.length > 1) {
const endIndex = chartData.length - 1;
const startIndex = 0;
setTimeRange([
chartData[startIndex].time,
chartData[endIndex].time,
]);
setBrushIndices({ startIndex, endIndex });
setIsResetting(true);
}
} else if (chartData.length > 1) {
const endIndex = chartData.length - 1;
const startIndex = Math.floor(endIndex * 0.75);
setTimeRange([
chartData[startIndex].time,
chartData[endIndex].time,
]);
setBrushIndices({ startIndex, endIndex });
}
}}
size="sm">
{timeRange ? (
<RefreshCw size={16} />
) : (
<ArrowRightToLine size={16} />
)}
{timeRange ? "重置范围" : "四分之一"}
</Button>
</div>
</div>
</CardHeader>
@@ -366,7 +366,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
<LineChart data={chartData} margin={chartMargin}>
<CartesianGrid
strokeDasharray="2 4"
stroke="var(--muted-foreground)"
stroke="var(--theme-line-muted-color)"
vertical={false}
/>
<XAxis
@@ -390,16 +390,26 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
});
}}
tick={{ fill: "var(--theme-text-muted-color)" }}
axisLine={{
stroke: "var(--theme-line-muted-color)",
}}
scale="time"
/>
<YAxis
mirror={true}
width={30}
tick={{ fill: "var(--theme-text-muted-color)" }}
axisLine={{
stroke: "var(--theme-line-muted-color)",
}}
/>
<Tooltip
cursor={false}
content={<CustomTooltip labelFormatter={lableFormatter} />}
content={
<CustomTooltip
labelFormatter={(value) => lableFormatter(value, hours)}
/>
}
/>
{connectBreaks &&
breakPoints.map((point, index) => (
@@ -417,7 +427,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
type={"monotone"}
dataKey={String(task.id)}
name={task.name}
stroke={generateColor(task.name, sortedTasks.length)}
stroke={generateColor(task.name, sortedTasks)}
strokeWidth={2}
hide={!visiblePingTasks.includes(task.id)}
dot={false}
@@ -425,6 +435,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
/>
))}
<Brush
{...brushIndices}
dataKey="time"
height={30}
stroke="var(--accent-track)"
@@ -457,8 +468,13 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
chartData[e.startIndex].time,
chartData[e.endIndex].time,
]);
setBrushIndices({
startIndex: e.startIndex,
endIndex: e.endIndex,
});
} else {
setTimeRange(null);
setBrushIndices({});
}
}}
/>

77
src/utils/chartHelper.ts Normal file
View File

@@ -0,0 +1,77 @@
import type { PingTask } from "@/types/node";
/**
* 根据任务名称和任务列表生成颜色
* @param taskName - 任务名称
* @param sortedTasks - 已排序的任务列表
* @returns CSS 颜色字符串
*/
export const generateColor = (taskName: string, sortedTasks: PingTask[]) => {
const index = sortedTasks.findIndex((t) => t.name === taskName);
if (index === -1) return "#000000"; // Fallback color
const total = sortedTasks.length;
const hue = (index * (360 / total)) % 360;
// 使用OKLCH色彩空间优化折线图的颜色区分度
const oklchColor = `oklch(0.6 0.2 ${hue} / .8)`;
// 为不支持OKLCH的浏览器提供HSL备用色
const hslFallback = `hsl(${hue}, 50%, 60%)`;
// 检查浏览器是否支持OKLCH
if (
typeof window !== "undefined" &&
window.CSS &&
CSS.supports("color", oklchColor)
) {
return oklchColor;
} else {
return hslFallback;
}
};
/**
* 格式化图表X轴的标签
* @param value - 时间戳
* @param hours - 当前选择的时间范围(小时)
* @returns 格式化后的时间字符串
*/
export const lableFormatter = (value: any, hours: number) => {
const date = new Date(value);
if (hours === 0) {
return date.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
return date.toLocaleString([], {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
};
/**
* 格式化负载图表X轴的时间标签
* @param value - 时间戳
* @param index - 索引
* @param dataLength - 数据总长度
* @returns 格式化后的时间字符串 (只显示首尾)
*/
export const loadChartTimeFormatter = (
value: any,
index: number,
dataLength: number
) => {
if (dataLength === 0) return "";
if (index === 0 || index === dataLength - 1) {
return new Date(value).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
}
return "";
};