Files
wireguard-ui/src/pages/admin/(partials)/traffic-chart.tsx
T
2026-09-13 06:53:33 +01:00

131 lines
4.2 KiB
TypeScript

import { useMemo } from "react";
import type { TrafficPoint } from "../(data)/dashboard-mock-data";
type Props = {
data: TrafficPoint[];
};
const W = 800;
const H = 240;
const PAD_L = 46;
const PAD_R = 12;
const PAD_T = 12;
const PAD_B = 28;
function formatAxis(value: number) {
return value >= 1 ? `${value}G` : `${Math.round(value * 1000)}M`;
}
export default function TrafficChart({ data }: Props) {
const { max, downPath, downArea, upPath, yTicks, xTicks } = useMemo(() => {
const plotW = W - PAD_L - PAD_R;
const plotH = H - PAD_T - PAD_B;
const max = Math.ceil(
Math.max(...data.flatMap((p) => [p.up, p.down])),
);
const x = (i: number) => PAD_L + (i / (data.length - 1)) * plotW;
const y = (v: number) => PAD_T + (1 - v / max) * plotH;
const toPath = (key: "up" | "down") =>
data
.map(
(p, i) =>
`${i == 0 ? "M" : "L"}${x(i).toFixed(2)},${y(
p[key],
).toFixed(2)}`,
)
.join(" ");
const downPath = toPath("down");
const upPath = toPath("up");
const baseY = (H - PAD_B).toFixed(2);
const downArea = `${downPath} L${x(data.length - 1).toFixed(
2,
)},${baseY} L${PAD_L},${baseY} Z`;
const yTicks = Array.from({ length: 5 }, (_, i) => {
const v = (max / 4) * i;
return { v, label: formatAxis(v), y: y(v) };
});
const xTicks: { label: string; x: number }[] = [];
for (let i = 0; i < data.length; i += 4) {
xTicks.push({ label: data[i]!.label, x: x(i) });
}
return { max, downPath, downArea, upPath, yTicks, xTicks };
}, [data]);
return (
<svg
viewBox={`0 0 ${W} ${H}`}
className="w-full h-auto text-secondary"
role="img"
aria-label="Network traffic over the last 24 hours"
>
<defs>
<linearGradient id="traffic-down" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="currentColor" stopOpacity="0.14" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
<linearGradient id="traffic-up" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#94a3b8" stopOpacity="0.1" />
<stop offset="100%" stopColor="#94a3b8" stopOpacity="0" />
</linearGradient>
</defs>
{yTicks.map((tick, i) => (
<g key={i}>
<line
x1={PAD_L}
y1={tick.y}
x2={W - PAD_R}
y2={tick.y}
stroke="#94a3b8"
strokeOpacity="0.18"
/>
<text
x={PAD_L - 8}
y={tick.y + 3}
textAnchor="end"
className="fill-current text-zinc-500 dark:text-zinc-600 text-[10.5px] tabular"
>
{tick.label}
</text>
</g>
))}
<path d={downArea} fill="url(#traffic-down)" />
<path
d={upPath}
fill="none"
stroke="#94a3b8"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d={downPath}
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
{xTicks.map((tick, i) => (
<text
key={i}
x={tick.x}
y={H - PAD_B + 16}
textAnchor="middle"
className="fill-current text-zinc-500 dark:text-zinc-600 text-[10.5px] tabular"
>
{tick.label}
</text>
))}
</svg>
);
}